diff --git a/src/infra/provider-usage.fetch.zai.test.ts b/src/infra/provider-usage.fetch.zai.test.ts index 2dafaccca9f..d952495e90f 100644 --- a/src/infra/provider-usage.fetch.zai.test.ts +++ b/src/infra/provider-usage.fetch.zai.test.ts @@ -25,6 +25,20 @@ describe("fetchZaiUsage", () => { expect(result.windows).toHaveLength(0); }); + it("falls back to a generic API error for blank unsuccessful messages", async () => { + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + success: false, + code: 500, + msg: " ", + }), + ); + + const result = await fetchZaiUsage("key", 5000, mockFetch); + expect(result.error).toBe("API error"); + expect(result.windows).toHaveLength(0); + }); + it("parses token and monthly windows with reset times", async () => { const tokenReset = "2026-01-08T00:00:00Z"; const minuteReset = "2026-01-08T00:30:00Z"; @@ -83,4 +97,47 @@ describe("fetchZaiUsage", () => { }, ]); }); + + it("clamps invalid percentages and falls back to alternate plan fields", async () => { + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + success: true, + code: 200, + data: { + plan: "Pro", + limits: [ + { + type: "TOKENS_LIMIT", + percentage: -5, + unit: 99, + }, + { + type: "TIME_LIMIT", + percentage: 140, + }, + { + type: "OTHER_LIMIT", + percentage: 50, + }, + ], + }, + }), + ); + + const result = await fetchZaiUsage("key", 5000, mockFetch); + + expect(result.plan).toBe("Pro"); + expect(result.windows).toEqual([ + { + label: "Tokens (Limit)", + usedPercent: 0, + resetAt: undefined, + }, + { + label: "Monthly", + usedPercent: 100, + resetAt: undefined, + }, + ]); + }); }); diff --git a/src/infra/provider-usage.fetch.zai.ts b/src/infra/provider-usage.fetch.zai.ts index 1ab1fd14764..d6f4970f0b7 100644 --- a/src/infra/provider-usage.fetch.zai.ts +++ b/src/infra/provider-usage.fetch.zai.ts @@ -46,11 +46,12 @@ export async function fetchZaiUsage( const data = (await res.json()) as ZaiUsageResponse; if (!data.success || data.code !== 200) { + const errorMessage = typeof data.msg === "string" ? data.msg.trim() : ""; return { provider: "zai", displayName: PROVIDER_LABELS.zai, windows: [], - error: data.msg || "API error", + error: errorMessage || "API error", }; } diff --git a/src/infra/ssh-config.test.ts b/src/infra/ssh-config.test.ts index 318f2dab973..cd722f51203 100644 --- a/src/infra/ssh-config.test.ts +++ b/src/infra/ssh-config.test.ts @@ -58,6 +58,17 @@ describe("ssh-config", () => { expect(parsed.identityFiles).toEqual(["/tmp/id"]); }); + it("ignores invalid ports and blank lines in ssh -G output", () => { + const parsed = parseSshConfigOutput( + "user bob\nhostname example.com\nport not-a-number\nidentityfile none\nidentityfile \n", + ); + + expect(parsed.user).toBe("bob"); + expect(parsed.host).toBe("example.com"); + expect(parsed.port).toBeUndefined(); + expect(parsed.identityFiles).toEqual([]); + }); + it("resolves ssh config via ssh -G", async () => { const config = await resolveSshConfig({ user: "me", host: "alias", port: 22 }); expect(config?.user).toBe("steipete"); @@ -68,6 +79,16 @@ describe("ssh-config", () => { expect(args?.slice(-2)).toEqual(["--", "me@alias"]); }); + it("adds non-default port and trimmed identity arguments", async () => { + await resolveSshConfig( + { user: "me", host: "alias", port: 2022 }, + { identity: " /tmp/custom_id " }, + ); + + const args = spawnMock.mock.calls.at(-1)?.[1] as string[] | undefined; + expect(args).toEqual(["-G", "-p", "2022", "-i", "/tmp/custom_id", "--", "me@alias"]); + }); + it("returns null when ssh -G fails", async () => { spawnMock.mockImplementationOnce( (_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => { @@ -82,4 +103,18 @@ describe("ssh-config", () => { const config = await resolveSshConfig({ user: "me", host: "bad-host", port: 22 }); expect(config).toBeNull(); }); + + it("returns null when the ssh process emits an error", async () => { + spawnMock.mockImplementationOnce( + (_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => { + const { child } = createMockSpawnChild(); + process.nextTick(() => { + child.emit("error", new Error("spawn boom")); + }); + return child as unknown as ChildProcess; + }, + ); + + await expect(resolveSshConfig({ user: "me", host: "bad-host", port: 22 })).resolves.toBeNull(); + }); });