diff --git a/src/commands/configure.gateway-auth.e2e.test.ts b/src/commands/configure.gateway-auth.e2e.test.ts index 6fd2b3299..d050a0e90 100644 --- a/src/commands/configure.gateway-auth.e2e.test.ts +++ b/src/commands/configure.gateway-auth.e2e.test.ts @@ -1,6 +1,18 @@ import { describe, expect, it } from "vitest"; import { buildGatewayAuthConfig } from "./configure.js"; +function expectGeneratedTokenFromInput(token: string | undefined, literalToAvoid = "undefined") { + const result = buildGatewayAuthConfig({ + mode: "token", + token, + }); + expect(result?.mode).toBe("token"); + expect(result?.token).toBeDefined(); + expect(result?.token).not.toBe(literalToAvoid); + expect(typeof result?.token).toBe("string"); + expect(result?.token?.length).toBeGreaterThan(0); +} + describe("buildGatewayAuthConfig", () => { it("preserves allowTailscale when switching to token", () => { const result = buildGatewayAuthConfig({ @@ -54,68 +66,23 @@ describe("buildGatewayAuthConfig", () => { }); it("generates random token when token param is undefined", () => { - const result = buildGatewayAuthConfig({ - mode: "token", - token: undefined, - }); - - expect(result?.mode).toBe("token"); - expect(result?.token).toBeDefined(); - expect(result?.token).not.toBe("undefined"); - expect(typeof result?.token).toBe("string"); - expect(result?.token?.length).toBeGreaterThan(0); + expectGeneratedTokenFromInput(undefined); }); it("generates random token when token param is empty string", () => { - const result = buildGatewayAuthConfig({ - mode: "token", - token: "", - }); - - expect(result?.mode).toBe("token"); - expect(result?.token).toBeDefined(); - expect(result?.token).not.toBe("undefined"); - expect(typeof result?.token).toBe("string"); - expect(result?.token?.length).toBeGreaterThan(0); + expectGeneratedTokenFromInput(""); }); it("generates random token when token param is whitespace only", () => { - const result = buildGatewayAuthConfig({ - mode: "token", - token: " ", - }); - - expect(result?.mode).toBe("token"); - expect(result?.token).toBeDefined(); - expect(result?.token).not.toBe("undefined"); - expect(typeof result?.token).toBe("string"); - expect(result?.token?.length).toBeGreaterThan(0); + expectGeneratedTokenFromInput(" "); }); it('generates random token when token param is the literal string "undefined"', () => { - const result = buildGatewayAuthConfig({ - mode: "token", - token: "undefined", - }); - - expect(result?.mode).toBe("token"); - expect(result?.token).toBeDefined(); - expect(result?.token).not.toBe("undefined"); - expect(typeof result?.token).toBe("string"); - expect(result?.token?.length).toBeGreaterThan(0); + expectGeneratedTokenFromInput("undefined"); }); it('generates random token when token param is the literal string "null"', () => { - const result = buildGatewayAuthConfig({ - mode: "token", - token: "null", - }); - - expect(result?.mode).toBe("token"); - expect(result?.token).toBeDefined(); - expect(result?.token).not.toBe("null"); - expect(typeof result?.token).toBe("string"); - expect(result?.token?.length).toBeGreaterThan(0); + expectGeneratedTokenFromInput("null", "null"); }); it("builds trusted-proxy config with all options", () => { diff --git a/src/commands/configure.gateway.e2e.test.ts b/src/commands/configure.gateway.e2e.test.ts index 4aa6127a1..a4d784632 100644 --- a/src/commands/configure.gateway.e2e.test.ts +++ b/src/commands/configure.gateway.e2e.test.ts @@ -55,81 +55,71 @@ function makeRuntime(): RuntimeEnv { }; } -async function runTrustedProxyPrompt(textQueue: Array) { +async function runGatewayPrompt(params: { + selectQueue: string[]; + textQueue: Array; + randomToken?: string; + confirmResult?: boolean; + authConfigFactory?: (input: Record) => Record; +}) { vi.clearAllMocks(); mocks.resolveGatewayPort.mockReturnValue(18789); - const selectQueue = ["loopback", "trusted-proxy", "off"]; - mocks.select.mockImplementation(async () => selectQueue.shift()); - mocks.text.mockImplementation(async () => textQueue.shift()); - mocks.buildGatewayAuthConfig.mockImplementation(({ mode, trustedProxy }) => ({ - mode, - trustedProxy, - })); + mocks.select.mockImplementation(async () => params.selectQueue.shift()); + mocks.text.mockImplementation(async () => params.textQueue.shift()); + mocks.randomToken.mockReturnValue(params.randomToken ?? "generated-token"); + mocks.confirm.mockResolvedValue(params.confirmResult ?? true); + mocks.buildGatewayAuthConfig.mockImplementation((input) => + params.authConfigFactory ? params.authConfigFactory(input as Record) : input, + ); const result = await promptGatewayConfig({}, makeRuntime()); const call = mocks.buildGatewayAuthConfig.mock.calls[0]?.[0]; return { result, call }; } +async function runTrustedProxyPrompt(params: { + textQueue: Array; + tailscaleMode?: "off" | "serve"; +}) { + return runGatewayPrompt({ + selectQueue: ["loopback", "trusted-proxy", params.tailscaleMode ?? "off"], + textQueue: params.textQueue, + authConfigFactory: ({ mode, trustedProxy }) => ({ mode, trustedProxy }), + }); +} + describe("promptGatewayConfig", () => { it("generates a token when the prompt returns undefined", async () => { - mocks.resolveGatewayPort.mockReturnValue(18789); - const selectQueue = ["loopback", "token", "off"]; - mocks.select.mockImplementation(async () => selectQueue.shift()); - const textQueue = ["18789", undefined]; - mocks.text.mockImplementation(async () => textQueue.shift()); - mocks.randomToken.mockReturnValue("generated-token"); - mocks.buildGatewayAuthConfig.mockImplementation(({ mode, token, password }) => ({ - mode, - token, - password, - })); - - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - const result = await promptGatewayConfig({}, runtime); + const { result } = await runGatewayPrompt({ + selectQueue: ["loopback", "token", "off"], + textQueue: ["18789", undefined], + randomToken: "generated-token", + authConfigFactory: ({ mode, token, password }) => ({ mode, token, password }), + }); expect(result.token).toBe("generated-token"); }); + it("does not set password to literal 'undefined' when prompt returns undefined", async () => { - vi.clearAllMocks(); - mocks.resolveGatewayPort.mockReturnValue(18789); - // Flow: loopback bind → password auth → tailscale off - const selectQueue = ["loopback", "password", "off"]; - mocks.select.mockImplementation(async () => selectQueue.shift()); - // Port prompt → OK, then password prompt → returns undefined (simulating prompter edge case) - const textQueue = ["18789", undefined]; - mocks.text.mockImplementation(async () => textQueue.shift()); - mocks.randomToken.mockReturnValue("unused"); - mocks.buildGatewayAuthConfig.mockImplementation(({ mode, token, password }) => ({ - mode, - token, - password, - })); - - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await promptGatewayConfig({}, runtime); - const call = mocks.buildGatewayAuthConfig.mock.calls[0]?.[0]; + const { call } = await runGatewayPrompt({ + selectQueue: ["loopback", "password", "off"], + textQueue: ["18789", undefined], + randomToken: "unused", + authConfigFactory: ({ mode, token, password }) => ({ mode, token, password }), + }); expect(call?.password).not.toBe("undefined"); expect(call?.password).toBe(""); }); it("prompts for trusted-proxy configuration when trusted-proxy mode selected", async () => { - const { result, call } = await runTrustedProxyPrompt([ - "18789", - "x-forwarded-user", - "x-forwarded-proto,x-forwarded-host", - "nick@example.com", - "10.0.1.10,192.168.1.5", - ]); + const { result, call } = await runTrustedProxyPrompt({ + textQueue: [ + "18789", + "x-forwarded-user", + "x-forwarded-proto,x-forwarded-host", + "nick@example.com", + "10.0.1.10,192.168.1.5", + ], + }); expect(call?.mode).toBe("trusted-proxy"); expect(call?.trustedProxy).toEqual({ @@ -142,13 +132,9 @@ describe("promptGatewayConfig", () => { }); it("handles trusted-proxy with no optional fields", async () => { - const { result, call } = await runTrustedProxyPrompt([ - "18789", - "x-remote-user", - "", - "", - "10.0.0.1", - ]); + const { result, call } = await runTrustedProxyPrompt({ + textQueue: ["18789", "x-remote-user", "", "", "10.0.0.1"], + }); expect(call?.mode).toBe("trusted-proxy"); expect(call?.trustedProxy).toEqual({ @@ -160,25 +146,10 @@ describe("promptGatewayConfig", () => { }); it("forces tailscale off when trusted-proxy is selected", async () => { - vi.clearAllMocks(); - mocks.resolveGatewayPort.mockReturnValue(18789); - const selectQueue = ["loopback", "trusted-proxy", "serve"]; - mocks.select.mockImplementation(async () => selectQueue.shift()); - const textQueue = ["18789", "x-forwarded-user", "", "", "10.0.0.1"]; - mocks.text.mockImplementation(async () => textQueue.shift()); - mocks.confirm.mockResolvedValue(true); - mocks.buildGatewayAuthConfig.mockImplementation(({ mode, trustedProxy }) => ({ - mode, - trustedProxy, - })); - - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - const result = await promptGatewayConfig({}, runtime); + const { result } = await runTrustedProxyPrompt({ + tailscaleMode: "serve", + textQueue: ["18789", "x-forwarded-user", "", "", "10.0.0.1"], + }); expect(result.config.gateway?.bind).toBe("lan"); expect(result.config.gateway?.tailscale?.mode).toBe("off"); expect(result.config.gateway?.tailscale?.resetOnExit).toBe(false); diff --git a/src/commands/daemon-install-helpers.e2e.test.ts b/src/commands/daemon-install-helpers.e2e.test.ts index 83dff066e..2fb1645f9 100644 --- a/src/commands/daemon-install-helpers.e2e.test.ts +++ b/src/commands/daemon-install-helpers.e2e.test.ts @@ -42,20 +42,39 @@ describe("resolveGatewayDevMode", () => { }); }); +function mockNodeGatewayPlanFixture( + params: { + workingDirectory?: string; + version?: string; + supported?: boolean; + warning?: string; + serviceEnvironment?: Record; + } = {}, +) { + const { + workingDirectory = "/Users/me", + version = "22.0.0", + supported = true, + warning, + serviceEnvironment = { OPENCLAW_PORT: "3000" }, + } = params; + mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); + mocks.resolveGatewayProgramArguments.mockResolvedValue({ + programArguments: ["node", "gateway"], + workingDirectory, + }); + mocks.resolveSystemNodeInfo.mockResolvedValue({ + path: "/opt/node", + version, + supported, + }); + mocks.renderSystemNodeWarning.mockReturnValue(warning); + mocks.buildServiceEnvironment.mockReturnValue(serviceEnvironment); +} + describe("buildGatewayInstallPlan", () => { it("uses provided nodePath and returns plan", async () => { - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], - workingDirectory: "/Users/me", - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", - version: "22.0.0", - supported: true, - }); - mocks.renderSystemNodeWarning.mockReturnValue(undefined); - mocks.buildServiceEnvironment.mockReturnValue({ OPENCLAW_PORT: "3000" }); + mockNodeGatewayPlanFixture(); const plan = await buildGatewayInstallPlan({ env: {}, @@ -72,18 +91,13 @@ describe("buildGatewayInstallPlan", () => { it("emits warnings when renderSystemNodeWarning returns one", async () => { const warn = vi.fn(); - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], + mockNodeGatewayPlanFixture({ workingDirectory: undefined, - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", version: "18.0.0", supported: false, + warning: "Node too old", + serviceEnvironment: {}, }); - mocks.renderSystemNodeWarning.mockReturnValue("Node too old"); - mocks.buildServiceEnvironment.mockReturnValue({}); await buildGatewayInstallPlan({ env: {}, @@ -97,19 +111,11 @@ describe("buildGatewayInstallPlan", () => { }); it("merges config env vars into the environment", async () => { - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], - workingDirectory: "/Users/me", - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", - version: "22.0.0", - supported: true, - }); - mocks.buildServiceEnvironment.mockReturnValue({ - OPENCLAW_PORT: "3000", - HOME: "/Users/me", + mockNodeGatewayPlanFixture({ + serviceEnvironment: { + OPENCLAW_PORT: "3000", + HOME: "/Users/me", + }, }); const plan = await buildGatewayInstallPlan({ @@ -135,17 +141,7 @@ describe("buildGatewayInstallPlan", () => { }); it("does not include empty config env values", async () => { - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], - workingDirectory: "/Users/me", - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", - version: "22.0.0", - supported: true, - }); - mocks.buildServiceEnvironment.mockReturnValue({ OPENCLAW_PORT: "3000" }); + mockNodeGatewayPlanFixture(); const plan = await buildGatewayInstallPlan({ env: {}, @@ -166,17 +162,7 @@ describe("buildGatewayInstallPlan", () => { }); it("drops whitespace-only config env values", async () => { - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], - workingDirectory: "/Users/me", - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", - version: "22.0.0", - supported: true, - }); - mocks.buildServiceEnvironment.mockReturnValue({}); + mockNodeGatewayPlanFixture({ serviceEnvironment: {} }); const plan = await buildGatewayInstallPlan({ env: {}, @@ -197,19 +183,11 @@ describe("buildGatewayInstallPlan", () => { }); it("keeps service env values over config env vars", async () => { - mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node"); - mocks.resolveGatewayProgramArguments.mockResolvedValue({ - programArguments: ["node", "gateway"], - workingDirectory: "/Users/me", - }); - mocks.resolveSystemNodeInfo.mockResolvedValue({ - path: "/opt/node", - version: "22.0.0", - supported: true, - }); - mocks.buildServiceEnvironment.mockReturnValue({ - HOME: "/Users/service", - OPENCLAW_PORT: "3000", + mockNodeGatewayPlanFixture({ + serviceEnvironment: { + HOME: "/Users/service", + OPENCLAW_PORT: "3000", + }, }); const plan = await buildGatewayInstallPlan({ diff --git a/src/commands/model-picker.e2e.test.ts b/src/commands/model-picker.e2e.test.ts index 5d44e244f..7003da57d 100644 --- a/src/commands/model-picker.e2e.test.ts +++ b/src/commands/model-picker.e2e.test.ts @@ -36,20 +36,29 @@ vi.mock("../agents/model-auth.js", () => ({ getCustomProviderApiKey, })); +const OPENROUTER_CATALOG = [ + { + provider: "openrouter", + id: "auto", + name: "OpenRouter Auto", + }, + { + provider: "openrouter", + id: "meta-llama/llama-3.3-70b:free", + name: "Llama 3.3 70B", + }, +] as const; + +function expectRouterModelFiltering(options: Array<{ value: string }>) { + expect(options.some((opt) => opt.value === "openrouter/auto")).toBe(false); + expect(options.some((opt) => opt.value === "openrouter/meta-llama/llama-3.3-70b:free")).toBe( + true, + ); +} + describe("promptDefaultModel", () => { it("filters internal router models from the selection list", async () => { - loadModelCatalog.mockResolvedValue([ - { - provider: "openrouter", - id: "auto", - name: "OpenRouter Auto", - }, - { - provider: "openrouter", - id: "meta-llama/llama-3.3-70b:free", - name: "Llama 3.3 70B", - }, - ]); + loadModelCatalog.mockResolvedValue(OPENROUTER_CATALOG); const select = vi.fn(async (params) => { const first = params.options[0]; @@ -67,10 +76,7 @@ describe("promptDefaultModel", () => { }); const options = select.mock.calls[0]?.[0]?.options ?? []; - expect(options.some((opt) => opt.value === "openrouter/auto")).toBe(false); - expect(options.some((opt) => opt.value === "openrouter/meta-llama/llama-3.3-70b:free")).toBe( - true, - ); + expectRouterModelFiltering(options); }); it("supports configuring vLLM during onboarding", async () => { @@ -124,18 +130,7 @@ describe("promptDefaultModel", () => { describe("promptModelAllowlist", () => { it("filters internal router models from the selection list", async () => { - loadModelCatalog.mockResolvedValue([ - { - provider: "openrouter", - id: "auto", - name: "OpenRouter Auto", - }, - { - provider: "openrouter", - id: "meta-llama/llama-3.3-70b:free", - name: "Llama 3.3 70B", - }, - ]); + loadModelCatalog.mockResolvedValue(OPENROUTER_CATALOG); const multiselect = vi.fn(async (params) => params.options.map((option: { value: string }) => option.value), @@ -146,12 +141,7 @@ describe("promptModelAllowlist", () => { await promptModelAllowlist({ config, prompter }); const options = multiselect.mock.calls[0]?.[0]?.options ?? []; - expect(options.some((opt: { value: string }) => opt.value === "openrouter/auto")).toBe(false); - expect( - options.some( - (opt: { value: string }) => opt.value === "openrouter/meta-llama/llama-3.3-70b:free", - ), - ).toBe(true); + expectRouterModelFiltering(options as Array<{ value: string }>); }); it("filters to allowed keys when provided", async () => { diff --git a/src/commands/models.list.test.ts b/src/commands/models.list.test.ts index 0b75fb83e..01ae4711a 100644 --- a/src/commands/models.list.test.ts +++ b/src/commands/models.list.test.ts @@ -203,11 +203,8 @@ describe("models list/status", () => { } async function expectZaiProviderFilter(provider: string) { - setDefaultModel("z.ai/glm-4.7"); + setDefaultZaiRegistry(); const runtime = makeRuntime(); - const models = [ZAI_MODEL, OPENAI_MODEL]; - modelRegistryState.models = models; - modelRegistryState.available = models; await modelsListCommand({ all: true, provider, json: true }, runtime); @@ -216,22 +213,67 @@ describe("models list/status", () => { expect(payload.models[0]?.key).toBe("zai/glm-4.7"); } + function setDefaultZaiRegistry(params: { available?: boolean } = {}) { + const available = params.available ?? true; + setDefaultModel("z.ai/glm-4.7"); + modelRegistryState.models = [ZAI_MODEL, OPENAI_MODEL]; + modelRegistryState.available = available ? [ZAI_MODEL, OPENAI_MODEL] : []; + } + + function setupGoogleAntigravityTemplateCase(params: { + configuredModelId: string; + templateId: string; + templateName: string; + available?: boolean; + }) { + configureGoogleAntigravityModel(params.configuredModelId); + const template = makeGoogleAntigravityTemplate(params.templateId, params.templateName); + modelRegistryState.models = [template]; + modelRegistryState.available = params.available ? [template] : []; + return template; + } + + async function runGoogleAntigravityListCase(params: { + configuredModelId: string; + templateId: string; + templateName: string; + available?: boolean; + withAuthProfile?: boolean; + }) { + setupGoogleAntigravityTemplateCase(params); + if (params.withAuthProfile) { + enableGoogleAntigravityAuthProfile(); + } + const runtime = makeRuntime(); + await modelsListCommand({ json: true }, runtime); + return parseJsonLog(runtime); + } + + function expectAntigravityModel( + payload: Record, + params: { key: string; available: boolean; includesTags?: boolean }, + ) { + const model = (payload.models as Array>)[0] ?? {}; + expect(model.key).toBe(params.key); + expect(model.missing).toBe(false); + expect(model.available).toBe(params.available); + if (params.includesTags) { + expect(model.tags).toContain("default"); + expect(model.tags).toContain("configured"); + } + } + beforeAll(async () => { ({ modelsListCommand } = await import("./models/list.list-command.js")); }); it("models list outputs canonical zai key for configured z.ai model", async () => { - loadConfig.mockReturnValue({ - agents: { defaults: { model: "z.ai/glm-4.7" } }, - }); + setDefaultZaiRegistry(); const runtime = makeRuntime(); - modelRegistryState.models = [ZAI_MODEL]; - modelRegistryState.available = [ZAI_MODEL]; await modelsListCommand({ json: true }, runtime); - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); + const payload = parseJsonLog(runtime); expect(payload.models[0]?.key).toBe("zai/glm-4.7"); }); @@ -262,109 +304,78 @@ describe("models list/status", () => { }); it("models list marks auth as unavailable when ZAI key is missing", async () => { - loadConfig.mockReturnValue({ - agents: { defaults: { model: "z.ai/glm-4.7" } }, - }); + setDefaultZaiRegistry({ available: false }); const runtime = makeRuntime(); - modelRegistryState.models = [ZAI_MODEL]; - modelRegistryState.available = []; await modelsListCommand({ all: true, json: true }, runtime); - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); + const payload = parseJsonLog(runtime); expect(payload.models[0]?.available).toBe(false); }); it("models list resolves antigravity opus 4.6 thinking from 4.5 template", async () => { - configureGoogleAntigravityModel("claude-opus-4-6-thinking"); - const runtime = makeRuntime(); - - modelRegistryState.models = [ - makeGoogleAntigravityTemplate("claude-opus-4-5-thinking", "Claude Opus 4.5 Thinking"), - ]; - modelRegistryState.available = []; - await modelsListCommand({ json: true }, runtime); - - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); - expect(payload.models[0]?.key).toBe("google-antigravity/claude-opus-4-6-thinking"); - expect(payload.models[0]?.missing).toBe(false); - expect(payload.models[0]?.tags).toContain("default"); - expect(payload.models[0]?.tags).toContain("configured"); + const payload = await runGoogleAntigravityListCase({ + configuredModelId: "claude-opus-4-6-thinking", + templateId: "claude-opus-4-5-thinking", + templateName: "Claude Opus 4.5 Thinking", + }); + expectAntigravityModel(payload, { + key: "google-antigravity/claude-opus-4-6-thinking", + available: false, + includesTags: true, + }); }); it("models list resolves antigravity opus 4.6 (non-thinking) from 4.5 template", async () => { - configureGoogleAntigravityModel("claude-opus-4-6"); - const runtime = makeRuntime(); - - modelRegistryState.models = [ - makeGoogleAntigravityTemplate("claude-opus-4-5", "Claude Opus 4.5"), - ]; - modelRegistryState.available = []; - await modelsListCommand({ json: true }, runtime); - - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); - expect(payload.models[0]?.key).toBe("google-antigravity/claude-opus-4-6"); - expect(payload.models[0]?.missing).toBe(false); - expect(payload.models[0]?.tags).toContain("default"); - expect(payload.models[0]?.tags).toContain("configured"); + const payload = await runGoogleAntigravityListCase({ + configuredModelId: "claude-opus-4-6", + templateId: "claude-opus-4-5", + templateName: "Claude Opus 4.5", + }); + expectAntigravityModel(payload, { + key: "google-antigravity/claude-opus-4-6", + available: false, + includesTags: true, + }); }); it("models list marks synthesized antigravity opus 4.6 thinking as available when template is available", async () => { - configureGoogleAntigravityModel("claude-opus-4-6-thinking"); - const runtime = makeRuntime(); - - const template = makeGoogleAntigravityTemplate( - "claude-opus-4-5-thinking", - "Claude Opus 4.5 Thinking", - ); - modelRegistryState.models = [template]; - modelRegistryState.available = [template]; - await modelsListCommand({ json: true }, runtime); - - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); - expect(payload.models[0]?.key).toBe("google-antigravity/claude-opus-4-6-thinking"); - expect(payload.models[0]?.missing).toBe(false); - expect(payload.models[0]?.available).toBe(true); + const payload = await runGoogleAntigravityListCase({ + configuredModelId: "claude-opus-4-6-thinking", + templateId: "claude-opus-4-5-thinking", + templateName: "Claude Opus 4.5 Thinking", + available: true, + }); + expectAntigravityModel(payload, { + key: "google-antigravity/claude-opus-4-6-thinking", + available: true, + }); }); it("models list marks synthesized antigravity opus 4.6 (non-thinking) as available when template is available", async () => { - configureGoogleAntigravityModel("claude-opus-4-6"); - const runtime = makeRuntime(); - - const template = makeGoogleAntigravityTemplate("claude-opus-4-5", "Claude Opus 4.5"); - modelRegistryState.models = [template]; - modelRegistryState.available = [template]; - await modelsListCommand({ json: true }, runtime); - - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); - expect(payload.models[0]?.key).toBe("google-antigravity/claude-opus-4-6"); - expect(payload.models[0]?.missing).toBe(false); - expect(payload.models[0]?.available).toBe(true); + const payload = await runGoogleAntigravityListCase({ + configuredModelId: "claude-opus-4-6", + templateId: "claude-opus-4-5", + templateName: "Claude Opus 4.5", + available: true, + }); + expectAntigravityModel(payload, { + key: "google-antigravity/claude-opus-4-6", + available: true, + }); }); it("models list prefers registry availability over provider auth heuristics", async () => { - configureGoogleAntigravityModel("claude-opus-4-6-thinking"); - enableGoogleAntigravityAuthProfile(); - const runtime = makeRuntime(); - - const template = makeGoogleAntigravityTemplate( - "claude-opus-4-5-thinking", - "Claude Opus 4.5 Thinking", - ); - modelRegistryState.models = [template]; - modelRegistryState.available = []; - await modelsListCommand({ json: true }, runtime); - - expect(runtime.log).toHaveBeenCalledTimes(1); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])); - expect(payload.models[0]?.key).toBe("google-antigravity/claude-opus-4-6-thinking"); - expect(payload.models[0]?.missing).toBe(false); - expect(payload.models[0]?.available).toBe(false); + const payload = await runGoogleAntigravityListCase({ + configuredModelId: "claude-opus-4-6-thinking", + templateId: "claude-opus-4-5-thinking", + templateName: "Claude Opus 4.5 Thinking", + withAuthProfile: true, + }); + expectAntigravityModel(payload, { + key: "google-antigravity/claude-opus-4-6-thinking", + available: false, + }); listProfilesForProvider.mockReturnValue([]); }); diff --git a/src/commands/openai-model-default.e2e.test.ts b/src/commands/openai-model-default.e2e.test.ts index b270b1008..c4d427520 100644 --- a/src/commands/openai-model-default.e2e.test.ts +++ b/src/commands/openai-model-default.e2e.test.ts @@ -33,6 +33,22 @@ function makePrompter(): WizardPrompter { }; } +function expectPrimaryModelChanged( + applied: { changed: boolean; next: OpenClawConfig }, + primary: string, +) { + expect(applied.changed).toBe(true); + expect(applied.next.agents?.defaults?.model).toEqual({ primary }); +} + +function expectConfigUnchanged( + applied: { changed: boolean; next: OpenClawConfig }, + cfg: OpenClawConfig, +) { + expect(applied.changed).toBe(false); + expect(applied.next).toEqual(cfg); +} + describe("applyDefaultModelChoice", () => { it("ensures allowlist entry exists when returning an agent override", async () => { const defaultModel = "vercel-ai-gateway/anthropic/claude-opus-4.6"; @@ -97,10 +113,7 @@ describe("applyGoogleGeminiModelDefault", () => { it("sets gemini default when model is unset", () => { const cfg: OpenClawConfig = { agents: { defaults: {} } }; const applied = applyGoogleGeminiModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: GOOGLE_GEMINI_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, GOOGLE_GEMINI_DEFAULT_MODEL); }); it("overrides existing model", () => { @@ -108,10 +121,7 @@ describe("applyGoogleGeminiModelDefault", () => { agents: { defaults: { model: "anthropic/claude-opus-4-5" } }, }; const applied = applyGoogleGeminiModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: GOOGLE_GEMINI_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, GOOGLE_GEMINI_DEFAULT_MODEL); }); it("no-ops when already gemini default", () => { @@ -119,8 +129,7 @@ describe("applyGoogleGeminiModelDefault", () => { agents: { defaults: { model: GOOGLE_GEMINI_DEFAULT_MODEL } }, }; const applied = applyGoogleGeminiModelDefault(cfg); - expect(applied.changed).toBe(false); - expect(applied.next).toEqual(cfg); + expectConfigUnchanged(applied, cfg); }); }); @@ -162,10 +171,7 @@ describe("applyOpenAICodexModelDefault", () => { it("sets openai-codex default when model is unset", () => { const cfg: OpenClawConfig = { agents: { defaults: {} } }; const applied = applyOpenAICodexModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: OPENAI_CODEX_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, OPENAI_CODEX_DEFAULT_MODEL); }); it("sets openai-codex default when model is openai/*", () => { @@ -173,10 +179,7 @@ describe("applyOpenAICodexModelDefault", () => { agents: { defaults: { model: OPENAI_DEFAULT_MODEL } }, }; const applied = applyOpenAICodexModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: OPENAI_CODEX_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, OPENAI_CODEX_DEFAULT_MODEL); }); it("does not override openai-codex/*", () => { @@ -184,8 +187,7 @@ describe("applyOpenAICodexModelDefault", () => { agents: { defaults: { model: OPENAI_CODEX_DEFAULT_MODEL } }, }; const applied = applyOpenAICodexModelDefault(cfg); - expect(applied.changed).toBe(false); - expect(applied.next).toEqual(cfg); + expectConfigUnchanged(applied, cfg); }); it("does not override non-openai models", () => { @@ -193,8 +195,7 @@ describe("applyOpenAICodexModelDefault", () => { agents: { defaults: { model: "anthropic/claude-opus-4-5" } }, }; const applied = applyOpenAICodexModelDefault(cfg); - expect(applied.changed).toBe(false); - expect(applied.next).toEqual(cfg); + expectConfigUnchanged(applied, cfg); }); }); @@ -202,10 +203,7 @@ describe("applyOpencodeZenModelDefault", () => { it("sets opencode default when model is unset", () => { const cfg: OpenClawConfig = { agents: { defaults: {} } }; const applied = applyOpencodeZenModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: OPENCODE_ZEN_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, OPENCODE_ZEN_DEFAULT_MODEL); }); it("overrides existing model", () => { @@ -213,10 +211,7 @@ describe("applyOpencodeZenModelDefault", () => { agents: { defaults: { model: "anthropic/claude-opus-4-5" } }, } as OpenClawConfig; const applied = applyOpencodeZenModelDefault(cfg); - expect(applied.changed).toBe(true); - expect(applied.next.agents?.defaults?.model).toEqual({ - primary: OPENCODE_ZEN_DEFAULT_MODEL, - }); + expectPrimaryModelChanged(applied, OPENCODE_ZEN_DEFAULT_MODEL); }); it("no-ops when already opencode-zen default", () => { @@ -224,8 +219,7 @@ describe("applyOpencodeZenModelDefault", () => { agents: { defaults: { model: OPENCODE_ZEN_DEFAULT_MODEL } }, } as OpenClawConfig; const applied = applyOpencodeZenModelDefault(cfg); - expect(applied.changed).toBe(false); - expect(applied.next).toEqual(cfg); + expectConfigUnchanged(applied, cfg); }); it("no-ops when already legacy opencode-zen default", () => { @@ -233,8 +227,7 @@ describe("applyOpencodeZenModelDefault", () => { agents: { defaults: { model: "opencode-zen/claude-opus-4-5" } }, } as OpenClawConfig; const applied = applyOpencodeZenModelDefault(cfg); - expect(applied.changed).toBe(false); - expect(applied.next).toEqual(cfg); + expectConfigUnchanged(applied, cfg); }); it("preserves fallbacks when setting primary", () => {