diff --git a/src/commands/doctor.e2e-harness.ts b/src/commands/doctor.e2e-harness.ts index 126d912f486..038e3a9979f 100644 --- a/src/commands/doctor.e2e-harness.ts +++ b/src/commands/doctor.e2e-harness.ts @@ -129,6 +129,17 @@ export const runLegacyStateMigrations = vi.fn().mockResolvedValue({ warnings: [], }) as unknown as MockFn; +const DEFAULT_CONFIG_SNAPSHOT = { + path: "/tmp/openclaw.json", + exists: true, + raw: "{}", + parsed: {}, + valid: true, + config: {}, + issues: [], + legacyIssues: [], +} as const; + vi.mock("@clack/prompts", () => ({ confirm, intro: vi.fn(), @@ -261,29 +272,43 @@ vi.mock("./doctor-state-migrations.js", () => ({ runLegacyStateMigrations, })); +export function mockDoctorConfigSnapshot( + params: { + config?: Record; + parsed?: Record; + valid?: boolean; + issues?: Array<{ path: string; message: string }>; + legacyIssues?: Array<{ path: string; message: string }>; + } = {}, +) { + readConfigFileSnapshot.mockResolvedValue({ + ...DEFAULT_CONFIG_SNAPSHOT, + config: params.config ?? DEFAULT_CONFIG_SNAPSHOT.config, + parsed: params.parsed ?? DEFAULT_CONFIG_SNAPSHOT.parsed, + valid: params.valid ?? DEFAULT_CONFIG_SNAPSHOT.valid, + issues: params.issues ?? DEFAULT_CONFIG_SNAPSHOT.issues, + legacyIssues: params.legacyIssues ?? DEFAULT_CONFIG_SNAPSHOT.legacyIssues, + }); +} + +export function createDoctorRuntime() { + return { + log: vi.fn() as unknown as MockFn, + error: vi.fn() as unknown as MockFn, + exit: vi.fn() as unknown as MockFn, + }; +} + export async function arrangeLegacyStateMigrationTest(): Promise<{ doctorCommand: unknown; runtime: { log: MockFn; error: MockFn; exit: MockFn }; detectLegacyStateMigrations: MockFn; runLegacyStateMigrations: MockFn; }> { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); + mockDoctorConfigSnapshot(); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn() as unknown as MockFn, - error: vi.fn() as unknown as MockFn, - exit: vi.fn() as unknown as MockFn, - }; + const runtime = createDoctorRuntime(); detectLegacyStateMigrations.mockClear(); runLegacyStateMigrations.mockClear(); diff --git a/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts b/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts index efd02ddf020..02897043ea3 100644 --- a/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts +++ b/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { + createDoctorRuntime, findLegacyGatewayServices, + migrateLegacyConfig, + mockDoctorConfigSnapshot, note, readConfigFileSnapshot, resolveOpenClawPackageRoot, @@ -9,39 +12,20 @@ import { serviceInstall, serviceIsLoaded, uninstallLegacyGatewayServices, - migrateLegacyConfig, writeConfigFile, } from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("migrates routing.allowFrom to channels.whatsapp.allowFrom", { timeout: 60_000 }, async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", + mockDoctorConfigSnapshot({ parsed: { routing: { allowFrom: ["+15555550123"] } }, valid: false, - config: {}, - issues: [ - { - path: "routing.allowFrom", - message: "legacy", - }, - ], - legacyIssues: [ - { - path: "routing.allowFrom", - message: "legacy", - }, - ], + issues: [{ path: "routing.allowFrom", message: "legacy" }], + legacyIssues: [{ path: "routing.allowFrom", message: "legacy" }], }); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; + const runtime = createDoctorRuntime(); migrateLegacyConfig.mockReturnValue({ config: { channels: { whatsapp: { allowFrom: ["+15555550123"] } } }, @@ -59,16 +43,7 @@ describe("doctor command", () => { }); it("skips legacy gateway services migration", { timeout: 60_000 }, async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); + mockDoctorConfigSnapshot(); findLegacyGatewayServices.mockResolvedValueOnce([ { @@ -81,13 +56,7 @@ describe("doctor command", () => { serviceInstall.mockClear(); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await doctorCommand(runtime); + await doctorCommand(createDoctorRuntime()); expect(uninstallLegacyGatewayServices).not.toHaveBeenCalled(); expect(serviceInstall).not.toHaveBeenCalled(); @@ -113,25 +82,10 @@ describe("doctor command", () => { durationMs: 1, }); - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); + mockDoctorConfigSnapshot(); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await doctorCommand(runtime); + await doctorCommand(createDoctorRuntime()); expect(runGatewayUpdate).toHaveBeenCalledWith(expect.objectContaining({ cwd: root })); expect(readConfigFileSnapshot).not.toHaveBeenCalled(); diff --git a/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts b/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts index 1df276f3da6..539b49b1cd4 100644 --- a/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts +++ b/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts @@ -1,9 +1,10 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { arrangeLegacyStateMigrationTest, confirm, + createDoctorRuntime, ensureAuthProfileStore, - readConfigFileSnapshot, + mockDoctorConfigSnapshot, serviceIsLoaded, serviceRestart, writeConfigFile, @@ -31,16 +32,7 @@ describe("doctor command", () => { }, 30_000); it("skips gateway restarts in non-interactive mode", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); + mockDoctorConfigSnapshot(); const { healthCommand } = await import("./health.js"); healthCommand.mockRejectedValueOnce(new Error("gateway closed")); @@ -50,25 +42,14 @@ describe("doctor command", () => { confirm.mockClear(); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await doctorCommand(runtime, { nonInteractive: true }); + await doctorCommand(createDoctorRuntime(), { nonInteractive: true }); expect(serviceRestart).not.toHaveBeenCalled(); expect(confirm).not.toHaveBeenCalled(); }); it("migrates anthropic oauth config profile id when only email profile exists", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, + mockDoctorConfigSnapshot({ config: { auth: { profiles: { @@ -76,8 +57,6 @@ describe("doctor command", () => { }, }, }, - issues: [], - legacyIssues: [], }); ensureAuthProfileStore.mockReturnValueOnce({ @@ -95,7 +74,7 @@ describe("doctor command", () => { }); const { doctorCommand } = await import("./doctor.js"); - await doctorCommand({ log: vi.fn(), error: vi.fn(), exit: vi.fn() }, { yes: true }); + await doctorCommand(createDoctorRuntime(), { yes: true }); const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record; const profiles = (written.auth as { profiles: Record }).profiles; diff --git a/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts b/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts index 822d5f813d8..73c728229e8 100644 --- a/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts +++ b/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts @@ -2,16 +2,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import { note, readConfigFileSnapshot } from "./doctor.e2e-harness.js"; +import { createDoctorRuntime, mockDoctorConfigSnapshot, note } from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("warns when per-agent sandbox docker/browser/prune overrides are ignored under shared scope", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, + mockDoctorConfigSnapshot({ config: { agents: { defaults: { @@ -35,20 +30,12 @@ describe("doctor command", () => { ], }, }, - issues: [], - legacyIssues: [], }); note.mockClear(); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await doctorCommand(runtime, { nonInteractive: true }); + await doctorCommand(createDoctorRuntime(), { nonInteractive: true }); expect( note.mock.calls.some(([message, title]) => { @@ -65,17 +52,10 @@ describe("doctor command", () => { }, 30_000); it("does not warn when only the active workspace is present", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, + mockDoctorConfigSnapshot({ config: { agents: { defaults: { workspace: "/Users/steipete/openclaw" } }, }, - issues: [], - legacyIssues: [], }); note.mockClear(); @@ -95,13 +75,7 @@ describe("doctor command", () => { }); const { doctorCommand } = await import("./doctor.js"); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - - await doctorCommand(runtime, { nonInteractive: true }); + await doctorCommand(createDoctorRuntime(), { nonInteractive: true }); expect(note.mock.calls.some(([_, title]) => title === "Extra workspace")).toBe(false); diff --git a/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts b/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts index 6f49bd6db2c..ceb318b42e0 100644 --- a/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts +++ b/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts @@ -1,21 +1,12 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; -import { note, readConfigFileSnapshot } from "./doctor.e2e-harness.js"; +import { describe, expect, it } from "vitest"; +import { createDoctorRuntime, mockDoctorConfigSnapshot, note } from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("warns when the state directory is missing", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); + mockDoctorConfigSnapshot(); const missingDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-missing-state-")); fs.rmSync(missingDir, { recursive: true, force: true }); @@ -23,10 +14,10 @@ describe("doctor command", () => { note.mockClear(); const { doctorCommand } = await import("./doctor.js"); - await doctorCommand( - { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, - { nonInteractive: true, workspaceSuggestions: false }, - ); + await doctorCommand(createDoctorRuntime(), { + nonInteractive: true, + workspaceSuggestions: false, + }); const stateNote = note.mock.calls.find((call) => call[1] === "State integrity"); expect(stateNote).toBeTruthy(); @@ -34,12 +25,7 @@ describe("doctor command", () => { }, 30_000); it("warns about opencode provider overrides", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, + mockDoctorConfigSnapshot({ config: { models: { providers: { @@ -50,15 +36,13 @@ describe("doctor command", () => { }, }, }, - issues: [], - legacyIssues: [], }); const { doctorCommand } = await import("./doctor.js"); - await doctorCommand( - { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, - { nonInteractive: true, workspaceSuggestions: false }, - ); + await doctorCommand(createDoctorRuntime(), { + nonInteractive: true, + workspaceSuggestions: false, + }); const warned = note.mock.calls.some( ([message, title]) => @@ -68,17 +52,10 @@ describe("doctor command", () => { }); it("skips gateway auth warning when OPENCLAW_GATEWAY_TOKEN is set", async () => { - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: true, - raw: "{}", - parsed: {}, - valid: true, + mockDoctorConfigSnapshot({ config: { gateway: { mode: "local" }, }, - issues: [], - legacyIssues: [], }); const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; @@ -87,10 +64,10 @@ describe("doctor command", () => { try { const { doctorCommand } = await import("./doctor.js"); - await doctorCommand( - { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, - { nonInteractive: true, workspaceSuggestions: false }, - ); + await doctorCommand(createDoctorRuntime(), { + nonInteractive: true, + workspaceSuggestions: false, + }); } finally { if (prevToken === undefined) { delete process.env.OPENCLAW_GATEWAY_TOKEN;