From 17a4345004a3a72818acd0dceb1cb74524cbcf1f Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:31:36 +0000 Subject: [PATCH] test(providers): add tests for Z.AI/Zhipu multi-configuration Update existing test files to cover all four GLM provider variants: - auth-choice-options.test.ts: Group test for zai, zai-coding, zhipu, zhipu-coding auth choice menu presence (follows MiniMax/Moonshot pattern) - program.smoke.test.ts: Add CLI flag wiring tests for --zai-coding-api-key, --zhipu-api-key, --zhipu-coding-api-key - model-auth.test.ts: Add env var resolution tests for: - zai-coding with fallback to ZAI_API_KEY - zhipu with ZHIPU_API_KEY - zhipu-coding with fallback to ZHIPU_API_KEY - provider-usage.test.ts: Add test for zhipu bigmodel.cn endpoint (will fail until fetchZhipuUsage is implemented) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/agents/model-auth.test.ts | 126 +++++++++++++++++++++++ src/cli/program.smoke.test.ts | 18 ++++ src/commands/auth-choice-options.test.ts | 7 +- src/infra/provider-usage.test.ts | 43 ++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 7219e128d..d8445d546 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -233,6 +233,132 @@ describe("getApiKeyForModel", () => { } }); + it("resolves zai-coding API key with fallback to ZAI_API_KEY", async () => { + const previous = { + coding: process.env.ZAI_CODING_API_KEY, + zai: process.env.ZAI_API_KEY, + legacy: process.env.Z_AI_API_KEY, + }; + + try { + delete process.env.ZAI_CODING_API_KEY; + process.env.ZAI_API_KEY = "zai-fallback-key"; + delete process.env.Z_AI_API_KEY; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zai-coding", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zai-fallback-key"); + expect(resolved.source).toContain("ZAI_API_KEY"); + } finally { + if (previous.coding === undefined) { + delete process.env.ZAI_CODING_API_KEY; + } else { + process.env.ZAI_CODING_API_KEY = previous.coding; + } + if (previous.zai === undefined) { + delete process.env.ZAI_API_KEY; + } else { + process.env.ZAI_API_KEY = previous.zai; + } + if (previous.legacy === undefined) { + delete process.env.Z_AI_API_KEY; + } else { + process.env.Z_AI_API_KEY = previous.legacy; + } + } + }); + + it("throws when zhipu API key is missing", async () => { + const previous = process.env.ZHIPU_API_KEY; + + try { + delete process.env.ZHIPU_API_KEY; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + let error: unknown = null; + try { + await resolveApiKeyForProvider({ + provider: "zhipu", + store: { version: 1, profiles: {} }, + }); + } catch (err) { + error = err; + } + + expect(String(error)).toContain('No API key found for provider "zhipu".'); + } finally { + if (previous === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous; + } + } + }); + + it("resolves zhipu API key from ZHIPU_API_KEY", async () => { + const previous = process.env.ZHIPU_API_KEY; + + try { + process.env.ZHIPU_API_KEY = "zhipu-test-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zhipu", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zhipu-test-key"); + expect(resolved.source).toContain("ZHIPU_API_KEY"); + } finally { + if (previous === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous; + } + } + }); + + it("resolves zhipu-coding API key with fallback to ZHIPU_API_KEY", async () => { + const previous = { + coding: process.env.ZHIPU_CODING_API_KEY, + zhipu: process.env.ZHIPU_API_KEY, + }; + + try { + delete process.env.ZHIPU_CODING_API_KEY; + process.env.ZHIPU_API_KEY = "zhipu-fallback-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zhipu-coding", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zhipu-fallback-key"); + expect(resolved.source).toContain("ZHIPU_API_KEY"); + } finally { + if (previous.coding === undefined) { + delete process.env.ZHIPU_CODING_API_KEY; + } else { + process.env.ZHIPU_CODING_API_KEY = previous.coding; + } + if (previous.zhipu === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous.zhipu; + } + } + }); + it("resolves Synthetic API key from env", async () => { const previousSynthetic = process.env.SYNTHETIC_API_KEY; diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index f6b155554..35e76644d 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -182,6 +182,24 @@ describe("cli program (smoke)", () => { key: "sk-zai-test", field: "zaiApiKey", }, + { + authChoice: "zai-coding-api-key", + flag: "--zai-coding-api-key", + key: "sk-zai-coding-test", + field: "zaiCodingApiKey", + }, + { + authChoice: "zhipu-api-key", + flag: "--zhipu-api-key", + key: "sk-zhipu-test", + field: "zhipuApiKey", + }, + { + authChoice: "zhipu-coding-api-key", + flag: "--zhipu-coding-api-key", + key: "sk-zhipu-coding-test", + field: "zhipuCodingApiKey", + }, ] as const; for (const entry of cases) { diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 7bf917a27..3f216b220 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -23,14 +23,19 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "token")).toBe(true); }); - it("includes Z.AI (GLM) auth choice", () => { + it("includes Z.AI / Zhipu (GLM) auth choices", () => { const store: AuthProfileStore = { version: 1, profiles: {} }; const options = buildAuthChoiceOptions({ store, includeSkip: false, }); + // International variants (api.z.ai) expect(options.some((opt) => opt.value === "zai-api-key")).toBe(true); + expect(options.some((opt) => opt.value === "zai-coding-api-key")).toBe(true); + // China variants (bigmodel.cn) + expect(options.some((opt) => opt.value === "zhipu-api-key")).toBe(true); + expect(options.some((opt) => opt.value === "zhipu-coding-api-key")).toBe(true); }); it("includes MiniMax auth choice", () => { diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index 077e70918..f5f95749a 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -137,6 +137,49 @@ describe("provider usage loading", () => { expect(mockFetch).toHaveBeenCalled(); }); + // TODO: Implement fetchZhipuUsage in provider-usage.load.ts + it("loads zhipu usage from bigmodel.cn endpoint", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("open.bigmodel.cn")) { + return makeResponse(200, { + success: true, + code: 200, + data: { + planName: "Basic", + limits: [ + { + type: "TOKENS_LIMIT", + percentage: 30, + unit: 3, + number: 6, + nextResetTime: "2026-01-07T06:00:00Z", + }, + ], + }, + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zhipu", token: "token-1" }], + fetch: mockFetch, + }); + + const zhipu = summary.providers.find((p) => p.provider === "zhipu"); + expect(zhipu?.plan).toBe("Basic"); + expect(zhipu?.windows[0]?.usedPercent).toBe(30); + }); + it("handles nested MiniMax usage payloads", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body);