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