From 2d8edf85ade6aff92bc40b642c57b3e26d5745da Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 16:48:36 +0000 Subject: [PATCH] refactor(test): share onboarding and model auth test helpers --- src/commands/auth-choice.e2e.test.ts | 10 +- src/commands/auth-choice.moonshot.e2e.test.ts | 35 ++--- src/commands/model-picker.e2e.test.ts | 12 +- src/commands/models.set.e2e.test.ts | 29 ++-- src/commands/models/list.status.e2e.test.ts | 135 ++++++++++-------- src/commands/onboard-auth.e2e.test.ts | 128 ++++++++--------- src/commands/onboard-channels.e2e.test.ts | 29 ++-- src/commands/onboard-custom.e2e.test.ts | 19 ++- src/commands/onboard-hooks.e2e.test.ts | 84 +++++------ .../onboarding/plugin-install.e2e.test.ts | 16 ++- 10 files changed, 253 insertions(+), 244 deletions(-) diff --git a/src/commands/auth-choice.e2e.test.ts b/src/commands/auth-choice.e2e.test.ts index 1e5843b6c4b..d4014cf0e3b 100644 --- a/src/commands/auth-choice.e2e.test.ts +++ b/src/commands/auth-choice.e2e.test.ts @@ -235,13 +235,10 @@ describe("applyAuthChoice", () => { } return "default"; }); - const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter = createPrompter({ + const { prompter, runtime } = createApiKeyPromptHarness({ select: select as WizardPrompter["select"], - multiselect, text, }); - const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "zai-api-key", @@ -265,13 +262,10 @@ describe("applyAuthChoice", () => { const text = vi.fn().mockResolvedValue("zai-test-key"); const select = vi.fn(async () => "default"); - const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter = createPrompter({ + const { prompter, runtime } = createApiKeyPromptHarness({ select: select as WizardPrompter["select"], - multiselect, text, }); - const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "zai-coding-global", diff --git a/src/commands/auth-choice.moonshot.e2e.test.ts b/src/commands/auth-choice.moonshot.e2e.test.ts index 64b1aa860f6..647694c9ce4 100644 --- a/src/commands/auth-choice.moonshot.e2e.test.ts +++ b/src/commands/auth-choice.moonshot.e2e.test.ts @@ -34,6 +34,23 @@ describe("applyAuthChoice (moonshot)", () => { }>(requireOpenClawAgentDir()); } + async function runMoonshotCnFlow(params: { + config: Record; + setDefaultModel: boolean; + }) { + const text = vi.fn().mockResolvedValue("sk-moonshot-cn-test"); + const prompter = createPrompter({ text: text as unknown as WizardPrompter["text"] }); + const runtime = createExitThrowingRuntime(); + const result = await applyAuthChoice({ + authChoice: "moonshot-api-key-cn", + config: params.config, + prompter, + runtime, + setDefaultModel: params.setDefaultModel, + }); + return { result, text }; + } + afterEach(async () => { await lifecycle.cleanup(); }); @@ -41,12 +58,7 @@ describe("applyAuthChoice (moonshot)", () => { it("keeps the .cn baseUrl when setDefaultModel is false", async () => { await setupTempState(); - const text = vi.fn().mockResolvedValue("sk-moonshot-cn-test"); - const prompter = createPrompter({ text: text as unknown as WizardPrompter["text"] }); - const runtime = createExitThrowingRuntime(); - - const result = await applyAuthChoice({ - authChoice: "moonshot-api-key-cn", + const { result, text } = await runMoonshotCnFlow({ config: { agents: { defaults: { @@ -54,8 +66,6 @@ describe("applyAuthChoice (moonshot)", () => { }, }, }, - prompter, - runtime, setDefaultModel: false, }); @@ -73,15 +83,8 @@ describe("applyAuthChoice (moonshot)", () => { it("sets the default model when setDefaultModel is true", async () => { await setupTempState(); - const text = vi.fn().mockResolvedValue("sk-moonshot-cn-test"); - const prompter = createPrompter({ text: text as unknown as WizardPrompter["text"] }); - const runtime = createExitThrowingRuntime(); - - const result = await applyAuthChoice({ - authChoice: "moonshot-api-key-cn", + const { result } = await runMoonshotCnFlow({ config: {}, - prompter, - runtime, setDefaultModel: true, }); diff --git a/src/commands/model-picker.e2e.test.ts b/src/commands/model-picker.e2e.test.ts index 7003da57d38..241f4770774 100644 --- a/src/commands/model-picker.e2e.test.ts +++ b/src/commands/model-picker.e2e.test.ts @@ -56,6 +56,10 @@ function expectRouterModelFiltering(options: Array<{ value: string }>) { ); } +function createSelectAllMultiselect() { + return vi.fn(async (params) => params.options.map((option: { value: string }) => option.value)); +} + describe("promptDefaultModel", () => { it("filters internal router models from the selection list", async () => { loadModelCatalog.mockResolvedValue(OPENROUTER_CATALOG); @@ -132,9 +136,7 @@ describe("promptModelAllowlist", () => { it("filters internal router models from the selection list", async () => { loadModelCatalog.mockResolvedValue(OPENROUTER_CATALOG); - const multiselect = vi.fn(async (params) => - params.options.map((option: { value: string }) => option.value), - ); + const multiselect = createSelectAllMultiselect(); const prompter = makePrompter({ multiselect }); const config = { agents: { defaults: {} } } as OpenClawConfig; @@ -163,9 +165,7 @@ describe("promptModelAllowlist", () => { }, ]); - const multiselect = vi.fn(async (params) => - params.options.map((option: { value: string }) => option.value), - ); + const multiselect = createSelectAllMultiselect(); const prompter = makePrompter({ multiselect }); const config = { agents: { defaults: {} } } as OpenClawConfig; diff --git a/src/commands/models.set.e2e.test.ts b/src/commands/models.set.e2e.test.ts index 1ccfdbe2bbe..0a40b1e8a31 100644 --- a/src/commands/models.set.e2e.test.ts +++ b/src/commands/models.set.e2e.test.ts @@ -32,6 +32,17 @@ function getWrittenConfig() { return writeConfigFile.mock.calls[0]?.[0] as Record; } +function expectWrittenPrimaryModel(model: string) { + expect(writeConfigFile).toHaveBeenCalledTimes(1); + const written = getWrittenConfig(); + expect(written.agents).toEqual({ + defaults: { + model: { primary: model }, + models: { [model]: {} }, + }, + }); +} + describe("models set + fallbacks", () => { beforeEach(() => { readConfigFileSnapshot.mockReset(); @@ -45,14 +56,7 @@ describe("models set + fallbacks", () => { await modelsSetCommand("z.ai/glm-4.7", runtime); - expect(writeConfigFile).toHaveBeenCalledTimes(1); - const written = getWrittenConfig(); - expect(written.agents).toEqual({ - defaults: { - model: { primary: "zai/glm-4.7" }, - models: { "zai/glm-4.7": {} }, - }, - }); + expectWrittenPrimaryModel("zai/glm-4.7"); }); it("normalizes z-ai provider in models fallbacks add", async () => { @@ -79,13 +83,6 @@ describe("models set + fallbacks", () => { await modelsSetCommand("Z.AI/glm-4.7", runtime); - expect(writeConfigFile).toHaveBeenCalledTimes(1); - const written = getWrittenConfig(); - expect(written.agents).toEqual({ - defaults: { - model: { primary: "zai/glm-4.7" }, - models: { "zai/glm-4.7": {} }, - }, - }); + expectWrittenPrimaryModel("zai/glm-4.7"); }); }); diff --git a/src/commands/models/list.status.e2e.test.ts b/src/commands/models/list.status.e2e.test.ts index 2da6b3764fa..7a8a44be747 100644 --- a/src/commands/models/list.status.e2e.test.ts +++ b/src/commands/models/list.status.e2e.test.ts @@ -123,6 +123,41 @@ const runtime = { exit: vi.fn(), }; +function createRuntime() { + return { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; +} + +async function withAgentScopeOverrides( + overrides: { + primary?: string; + fallbacks?: string[]; + agentDir?: string; + }, + run: () => Promise, +) { + const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation(); + const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation(); + const originalAgentDir = mocks.resolveAgentDir.getMockImplementation(); + + mocks.resolveAgentModelPrimary.mockReturnValue(overrides.primary); + mocks.resolveAgentModelFallbacksOverride.mockReturnValue(overrides.fallbacks); + if (overrides.agentDir) { + mocks.resolveAgentDir.mockReturnValue(overrides.agentDir); + } + + try { + return await run(); + } finally { + mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary); + mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks); + mocks.resolveAgentDir.mockImplementation(originalAgentDir); + } +} + describe("modelsStatusCommand auth overview", () => { it("includes masked auth sources in JSON output", async () => { await modelsStatusCommand({ json: true }, runtime as never); @@ -160,69 +195,49 @@ describe("modelsStatusCommand auth overview", () => { }); it("uses agent overrides and reports sources", async () => { - const localRuntime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation(); - const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation(); - const originalAgentDir = mocks.resolveAgentDir.getMockImplementation(); - - mocks.resolveAgentModelPrimary.mockReturnValue("openai/gpt-4"); - mocks.resolveAgentModelFallbacksOverride.mockReturnValue(["openai/gpt-3.5"]); - mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent-custom"); - - try { - await modelsStatusCommand({ json: true, agent: "Jeremiah" }, localRuntime as never); - expect(mocks.resolveAgentDir).toHaveBeenCalledWith(expect.anything(), "jeremiah"); - const payload = JSON.parse(String((localRuntime.log as vi.Mock).mock.calls[0][0])); - expect(payload.agentId).toBe("jeremiah"); - expect(payload.agentDir).toBe("/tmp/openclaw-agent-custom"); - expect(payload.defaultModel).toBe("openai/gpt-4"); - expect(payload.fallbacks).toEqual(["openai/gpt-3.5"]); - expect(payload.modelConfig).toEqual({ - defaultSource: "agent", - fallbacksSource: "agent", - }); - } finally { - mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary); - mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks); - mocks.resolveAgentDir.mockImplementation(originalAgentDir); - } + const localRuntime = createRuntime(); + await withAgentScopeOverrides( + { + primary: "openai/gpt-4", + fallbacks: ["openai/gpt-3.5"], + agentDir: "/tmp/openclaw-agent-custom", + }, + async () => { + await modelsStatusCommand({ json: true, agent: "Jeremiah" }, localRuntime as never); + expect(mocks.resolveAgentDir).toHaveBeenCalledWith(expect.anything(), "jeremiah"); + const payload = JSON.parse(String((localRuntime.log as vi.Mock).mock.calls[0][0])); + expect(payload.agentId).toBe("jeremiah"); + expect(payload.agentDir).toBe("/tmp/openclaw-agent-custom"); + expect(payload.defaultModel).toBe("openai/gpt-4"); + expect(payload.fallbacks).toEqual(["openai/gpt-3.5"]); + expect(payload.modelConfig).toEqual({ + defaultSource: "agent", + fallbacksSource: "agent", + }); + }, + ); }); it("labels defaults when --agent has no overrides", async () => { - const localRuntime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation(); - const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation(); - - mocks.resolveAgentModelPrimary.mockReturnValue(undefined); - mocks.resolveAgentModelFallbacksOverride.mockReturnValue(undefined); - - try { - await modelsStatusCommand({ agent: "main" }, localRuntime as never); - const output = (localRuntime.log as vi.Mock).mock.calls - .map((call) => String(call[0])) - .join("\n"); - expect(output).toContain("Default (defaults)"); - expect(output).toContain("Fallbacks (0) (defaults)"); - } finally { - mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary); - mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks); - } + const localRuntime = createRuntime(); + await withAgentScopeOverrides( + { + primary: undefined, + fallbacks: undefined, + }, + async () => { + await modelsStatusCommand({ agent: "main" }, localRuntime as never); + const output = (localRuntime.log as vi.Mock).mock.calls + .map((call) => String(call[0])) + .join("\n"); + expect(output).toContain("Default (defaults)"); + expect(output).toContain("Fallbacks (0) (defaults)"); + }, + ); }); it("throws when agent id is unknown", async () => { - const localRuntime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; + const localRuntime = createRuntime(); await expect(modelsStatusCommand({ agent: "unknown" }, localRuntime as never)).rejects.toThrow( 'Unknown agent id "unknown".', ); @@ -230,11 +245,7 @@ describe("modelsStatusCommand auth overview", () => { it("exits non-zero when auth is missing", async () => { const originalProfiles = { ...mocks.store.profiles }; mocks.store.profiles = {}; - const localRuntime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; + const localRuntime = createRuntime(); const originalEnvImpl = mocks.resolveEnvApiKey.getMockImplementation(); mocks.resolveEnvApiKey.mockImplementation(() => null); diff --git a/src/commands/onboard-auth.e2e.test.ts b/src/commands/onboard-auth.e2e.test.ts index 023546bb042..81ccf99d404 100644 --- a/src/commands/onboard-auth.e2e.test.ts +++ b/src/commands/onboard-auth.e2e.test.ts @@ -39,13 +39,15 @@ function createLegacyProviderConfig(params: { api: string; modelId?: string; modelName?: string; + baseUrl?: string; + apiKey?: string; }) { return { models: { providers: { [params.providerId]: { - baseUrl: "https://old.example.com", - apiKey: "old-key", + baseUrl: params.baseUrl ?? "https://old.example.com", + apiKey: params.apiKey ?? "old-key", api: params.api, models: [ { @@ -64,6 +66,42 @@ function createLegacyProviderConfig(params: { }; } +const EXPECTED_FALLBACKS = ["anthropic/claude-opus-4-5"] as const; + +function createConfigWithFallbacks() { + return { + agents: { + defaults: { + model: { fallbacks: [...EXPECTED_FALLBACKS] }, + }, + }, + }; +} + +function expectFallbacksPreserved(cfg: ReturnType) { + expect(cfg.agents?.defaults?.model?.fallbacks).toEqual([...EXPECTED_FALLBACKS]); +} + +function expectPrimaryModelPreserved(cfg: ReturnType) { + expect(cfg.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5"); +} + +function expectAllowlistContains( + cfg: ReturnType, + key: string, +) { + const models = cfg.agents?.defaults?.models ?? {}; + expect(Object.keys(models)).toContain(key); +} + +function expectAliasPreserved( + cfg: ReturnType, + key: string, + alias: string, +) { + expect(cfg.agents?.defaults?.models?.[key]?.alias).toBe(alias); +} + describe("writeOAuthCredentials", () => { const lifecycle = createAuthTestLifecycle([ "OPENCLAW_STATE_DIR", @@ -177,14 +215,8 @@ describe("applyMinimaxApiConfig", () => { }); it("preserves existing model fallbacks", () => { - const cfg = applyMinimaxApiConfig({ - agents: { - defaults: { - model: { fallbacks: ["anthropic/claude-opus-4-5"] }, - }, - }, - }); - expect(cfg.agents?.defaults?.model?.fallbacks).toEqual(["anthropic/claude-opus-4-5"]); + const cfg = applyMinimaxApiConfig(createConfigWithFallbacks()); + expectFallbacksPreserved(cfg); }); it("adds model alias", () => { @@ -270,7 +302,7 @@ describe("applyMinimaxApiProviderConfig", () => { const cfg = applyMinimaxApiProviderConfig({ agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } }, }); - expect(cfg.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5"); + expectPrimaryModelPreserved(cfg); }); }); @@ -312,7 +344,7 @@ describe("applyZaiProviderConfig", () => { const cfg = applyZaiProviderConfig({ agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } }, }); - expect(cfg.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5"); + expectPrimaryModelPreserved(cfg); }); }); @@ -387,14 +419,8 @@ describe("applyXaiConfig", () => { }); it("preserves existing model fallbacks", () => { - const cfg = applyXaiConfig({ - agents: { - defaults: { - model: { fallbacks: ["anthropic/claude-opus-4-5"] }, - }, - }, - }); - expect(cfg.agents?.defaults?.model?.fallbacks).toEqual(["anthropic/claude-opus-4-5"]); + const cfg = applyXaiConfig(createConfigWithFallbacks()); + expectFallbacksPreserved(cfg); }); }); @@ -424,8 +450,7 @@ describe("applyXaiProviderConfig", () => { describe("applyOpencodeZenProviderConfig", () => { it("adds allowlist entry for the default model", () => { const cfg = applyOpencodeZenProviderConfig({}); - const models = cfg.agents?.defaults?.models ?? {}; - expect(Object.keys(models)).toContain("opencode/claude-opus-4-6"); + expectAllowlistContains(cfg, "opencode/claude-opus-4-6"); }); it("preserves existing alias for the default model", () => { @@ -438,7 +463,7 @@ describe("applyOpencodeZenProviderConfig", () => { }, }, }); - expect(cfg.agents?.defaults?.models?.["opencode/claude-opus-4-6"]?.alias).toBe("My Opus"); + expectAliasPreserved(cfg, "opencode/claude-opus-4-6", "My Opus"); }); }); @@ -449,22 +474,15 @@ describe("applyOpencodeZenConfig", () => { }); it("preserves existing model fallbacks", () => { - const cfg = applyOpencodeZenConfig({ - agents: { - defaults: { - model: { fallbacks: ["anthropic/claude-opus-4-5"] }, - }, - }, - }); - expect(cfg.agents?.defaults?.model?.fallbacks).toEqual(["anthropic/claude-opus-4-5"]); + const cfg = applyOpencodeZenConfig(createConfigWithFallbacks()); + expectFallbacksPreserved(cfg); }); }); describe("applyOpenrouterProviderConfig", () => { it("adds allowlist entry for the default model", () => { const cfg = applyOpenrouterProviderConfig({}); - const models = cfg.agents?.defaults?.models ?? {}; - expect(Object.keys(models)).toContain(OPENROUTER_DEFAULT_MODEL_REF); + expectAllowlistContains(cfg, OPENROUTER_DEFAULT_MODEL_REF); }); it("preserves existing alias for the default model", () => { @@ -477,34 +495,22 @@ describe("applyOpenrouterProviderConfig", () => { }, }, }); - expect(cfg.agents?.defaults?.models?.[OPENROUTER_DEFAULT_MODEL_REF]?.alias).toBe("Router"); + expectAliasPreserved(cfg, OPENROUTER_DEFAULT_MODEL_REF, "Router"); }); }); describe("applyLitellmProviderConfig", () => { it("preserves existing baseUrl and api key while adding the default model", () => { - const cfg = applyLitellmProviderConfig({ - models: { - providers: { - litellm: { - baseUrl: "https://litellm.example/v1", - apiKey: " old-key ", - api: "anthropic-messages", - models: [ - { - id: "custom-model", - name: "Custom", - reasoning: false, - input: ["text"], - cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1000, - maxTokens: 100, - }, - ], - }, - }, - }, - }); + const cfg = applyLitellmProviderConfig( + createLegacyProviderConfig({ + providerId: "litellm", + api: "anthropic-messages", + modelId: "custom-model", + modelName: "Custom", + baseUrl: "https://litellm.example/v1", + apiKey: " old-key ", + }), + ); expect(cfg.models?.providers?.litellm?.baseUrl).toBe("https://litellm.example/v1"); expect(cfg.models?.providers?.litellm?.api).toBe("openai-completions"); @@ -523,13 +529,7 @@ describe("applyOpenrouterConfig", () => { }); it("preserves existing model fallbacks", () => { - const cfg = applyOpenrouterConfig({ - agents: { - defaults: { - model: { fallbacks: ["anthropic/claude-opus-4-5"] }, - }, - }, - }); - expect(cfg.agents?.defaults?.model?.fallbacks).toEqual(["anthropic/claude-opus-4-5"]); + const cfg = applyOpenrouterConfig(createConfigWithFallbacks()); + expectFallbacksPreserved(cfg); }); }); diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index 827ea313a81..833f1a28d33 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -15,6 +15,17 @@ function createPrompter(overrides: Partial): WizardPrompter { ); } +function createUnexpectedPromptGuards() { + return { + multiselect: vi.fn(async () => { + throw new Error("unexpected multiselect"); + }), + text: vi.fn(async ({ message }: { message: string }) => { + throw new Error(`unexpected text prompt: ${message}`); + }) as unknown as WizardPrompter["text"], + }; +} + vi.mock("node:fs/promises", () => ({ default: { access: vi.fn(async () => { @@ -73,18 +84,13 @@ describe("setupChannels", () => { it("shows explicit dmScope config command in channel primer", async () => { const note = vi.fn(async () => {}); const select = vi.fn(async () => "__done__"); - const multiselect = vi.fn(async () => { - throw new Error("unexpected multiselect"); - }); - const text = vi.fn(async ({ message }: { message: string }) => { - throw new Error(`unexpected text prompt: ${message}`); - }); + const { multiselect, text } = createUnexpectedPromptGuards(); const prompter = createPrompter({ note, select, multiselect, - text: text as unknown as WizardPrompter["text"], + text, }); const runtime = createExitThrowingRuntime(); @@ -112,17 +118,12 @@ describe("setupChannels", () => { } throw new Error(`unexpected select prompt: ${message}`); }); - const multiselect = vi.fn(async () => { - throw new Error("unexpected multiselect"); - }); - const text = vi.fn(async ({ message }: { message: string }) => { - throw new Error(`unexpected text prompt: ${message}`); - }); + const { multiselect, text } = createUnexpectedPromptGuards(); const prompter = createPrompter({ select, multiselect, - text: text as unknown as WizardPrompter["text"], + text, }); const runtime = createExitThrowingRuntime(); diff --git a/src/commands/onboard-custom.e2e.test.ts b/src/commands/onboard-custom.e2e.test.ts index 302ae2338a6..f360b018c59 100644 --- a/src/commands/onboard-custom.e2e.test.ts +++ b/src/commands/onboard-custom.e2e.test.ts @@ -64,6 +64,17 @@ async function runPromptCustomApi( }); } +function expectOpenAiCompatResult(params: { + prompter: ReturnType; + textCalls: number; + selectCalls: number; + result: Awaited>; +}) { + expect(params.prompter.text).toHaveBeenCalledTimes(params.textCalls); + expect(params.prompter.select).toHaveBeenCalledTimes(params.selectCalls); + expect(params.result.config.models?.providers?.custom?.api).toBe("openai-completions"); +} + describe("promptCustomApiConfig", () => { afterEach(() => { vi.unstubAllGlobals(); @@ -78,9 +89,7 @@ describe("promptCustomApiConfig", () => { stubFetchSequence([{ ok: true }]); const result = await runPromptCustomApi(prompter); - expect(prompter.text).toHaveBeenCalledTimes(5); - expect(prompter.select).toHaveBeenCalledTimes(1); - expect(result.config.models?.providers?.custom?.api).toBe("openai-completions"); + expectOpenAiCompatResult({ prompter, textCalls: 5, selectCalls: 1, result }); expect(result.config.agents?.defaults?.models?.["custom/llama3"]?.alias).toBe("local"); }); @@ -104,9 +113,7 @@ describe("promptCustomApiConfig", () => { stubFetchSequence([{ ok: true }]); const result = await runPromptCustomApi(prompter); - expect(prompter.text).toHaveBeenCalledTimes(5); - expect(prompter.select).toHaveBeenCalledTimes(1); - expect(result.config.models?.providers?.custom?.api).toBe("openai-completions"); + expectOpenAiCompatResult({ prompter, textCalls: 5, selectCalls: 1, result }); }); it("re-prompts base url when unknown detection fails", async () => { diff --git a/src/commands/onboard-hooks.e2e.test.ts b/src/commands/onboard-hooks.e2e.test.ts index 212b9366346..02b82b22fc5 100644 --- a/src/commands/onboard-hooks.e2e.test.ts +++ b/src/commands/onboard-hooks.e2e.test.ts @@ -112,16 +112,28 @@ describe("onboard-hooks", () => { ], }); + async function runSetupInternalHooks(params: { + selected: string[]; + cfg?: OpenClawConfig; + eligible?: boolean; + }) { + const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); + vi.mocked(buildWorkspaceHookStatus).mockReturnValue( + createMockHookReport(params.eligible ?? true), + ); + + const cfg = params.cfg ?? {}; + const prompter = createMockPrompter(params.selected); + const runtime = createMockRuntime(); + const result = await setupInternalHooks(cfg, runtime, prompter); + return { result, cfg, prompter }; + } + describe("setupInternalHooks", () => { it("should enable hooks when user selects them", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport()); - - const cfg: OpenClawConfig = {}; - const prompter = createMockPrompter(["session-memory"]); - const runtime = createMockRuntime(); - - const result = await setupInternalHooks(cfg, runtime, prompter); + const { result, prompter } = await runSetupInternalHooks({ + selected: ["session-memory"], + }); expect(result.hooks?.internal?.enabled).toBe(true); expect(result.hooks?.internal?.entries).toEqual({ @@ -147,28 +159,19 @@ describe("onboard-hooks", () => { }); it("should not enable hooks when user skips", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport()); - - const cfg: OpenClawConfig = {}; - const prompter = createMockPrompter(["__skip__"]); - const runtime = createMockRuntime(); - - const result = await setupInternalHooks(cfg, runtime, prompter); + const { result, prompter } = await runSetupInternalHooks({ + selected: ["__skip__"], + }); expect(result.hooks?.internal).toBeUndefined(); expect(prompter.note).toHaveBeenCalledTimes(1); }); it("should handle no eligible hooks", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport(false)); - - const cfg: OpenClawConfig = {}; - const prompter = createMockPrompter([]); - const runtime = createMockRuntime(); - - const result = await setupInternalHooks(cfg, runtime, prompter); + const { result, cfg, prompter } = await runSetupInternalHooks({ + selected: [], + eligible: false, + }); expect(result).toEqual(cfg); expect(prompter.multiselect).not.toHaveBeenCalled(); @@ -179,9 +182,6 @@ describe("onboard-hooks", () => { }); it("should preserve existing hooks config when enabled", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport()); - const cfg: OpenClawConfig = { hooks: { enabled: true, @@ -189,10 +189,10 @@ describe("onboard-hooks", () => { token: "existing-token", }, }; - const prompter = createMockPrompter(["session-memory"]); - const runtime = createMockRuntime(); - - const result = await setupInternalHooks(cfg, runtime, prompter); + const { result } = await runSetupInternalHooks({ + selected: ["session-memory"], + cfg, + }); expect(result.hooks?.enabled).toBe(true); expect(result.hooks?.path).toBe("/webhook"); @@ -204,30 +204,22 @@ describe("onboard-hooks", () => { }); it("should preserve existing config when user skips", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport()); - const cfg: OpenClawConfig = { agents: { defaults: { workspace: "/workspace" } }, }; - const prompter = createMockPrompter(["__skip__"]); - const runtime = createMockRuntime(); - - const result = await setupInternalHooks(cfg, runtime, prompter); + const { result } = await runSetupInternalHooks({ + selected: ["__skip__"], + cfg, + }); expect(result).toEqual(cfg); expect(result.agents?.defaults?.workspace).toBe("/workspace"); }); it("should show informative notes to user", async () => { - const { buildWorkspaceHookStatus } = await import("../hooks/hooks-status.js"); - vi.mocked(buildWorkspaceHookStatus).mockReturnValue(createMockHookReport()); - - const cfg: OpenClawConfig = {}; - const prompter = createMockPrompter(["session-memory"]); - const runtime = createMockRuntime(); - - await setupInternalHooks(cfg, runtime, prompter); + const { prompter } = await runSetupInternalHooks({ + selected: ["session-memory"], + }); const noteCalls = (prompter.note as ReturnType).mock.calls; expect(noteCalls).toHaveLength(2); diff --git a/src/commands/onboarding/plugin-install.e2e.test.ts b/src/commands/onboarding/plugin-install.e2e.test.ts index e198e38bddb..7da28ca9402 100644 --- a/src/commands/onboarding/plugin-install.e2e.test.ts +++ b/src/commands/onboarding/plugin-install.e2e.test.ts @@ -67,6 +67,14 @@ async function runInitialValueForChannel(channel: "dev" | "beta") { return select.mock.calls[0]?.[0]?.initialValue; } +function expectPluginLoadedFromLocalPath( + result: Awaited>, +) { + const expectedPath = path.resolve(process.cwd(), "extensions/zalo"); + expect(result.installed).toBe(true); + expect(result.cfg.plugins?.load?.paths).toContain(expectedPath); +} + describe("ensureOnboardingPluginInstalled", () => { it("installs from npm and enables the plugin", async () => { const runtime = makeRuntime(); @@ -115,9 +123,7 @@ describe("ensureOnboardingPluginInstalled", () => { runtime, }); - const expectedPath = path.resolve(process.cwd(), "extensions/zalo"); - expect(result.installed).toBe(true); - expect(result.cfg.plugins?.load?.paths).toContain(expectedPath); + expectPluginLoadedFromLocalPath(result); expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true); }); @@ -152,9 +158,7 @@ describe("ensureOnboardingPluginInstalled", () => { runtime, }); - const expectedPath = path.resolve(process.cwd(), "extensions/zalo"); - expect(result.installed).toBe(true); - expect(result.cfg.plugins?.load?.paths).toContain(expectedPath); + expectPluginLoadedFromLocalPath(result); expect(note).toHaveBeenCalled(); expect(runtime.error).not.toHaveBeenCalled(); });