diff --git a/src/commands/doctor.e2e-harness.ts b/src/commands/doctor.e2e-harness.ts new file mode 100644 index 000000000..8b60b65af --- /dev/null +++ b/src/commands/doctor.e2e-harness.ts @@ -0,0 +1,326 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, vi } from "vitest"; + +let originalIsTTY: boolean | undefined; +let originalStateDir: string | undefined; +let originalUpdateInProgress: string | undefined; +let tempStateDir: string | undefined; + +function setStdinTty(value: boolean | undefined) { + try { + Object.defineProperty(process.stdin, "isTTY", { + value, + configurable: true, + }); + } catch { + // ignore + } +} + +export const readConfigFileSnapshot = vi.fn(); +export const confirm = vi.fn().mockResolvedValue(true); +export const select = vi.fn().mockResolvedValue("node"); +export const note = vi.fn(); +export const writeConfigFile = vi.fn().mockResolvedValue(undefined); +export const resolveOpenClawPackageRoot = vi.fn().mockResolvedValue(null); +export const runGatewayUpdate = vi.fn().mockResolvedValue({ + status: "skipped", + mode: "unknown", + steps: [], + durationMs: 0, +}); +export const migrateLegacyConfig = vi.fn((raw: unknown) => ({ + config: raw as Record, + changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], +})); + +export const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); +export const runCommandWithTimeout = vi.fn().mockResolvedValue({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, +}); + +export const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} }); + +export const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({ + path: "/tmp/openclaw.json", + exists: false, + raw: null, + parsed: {}, + valid: true, + config: {}, + issues: [], + legacyIssues: [], +}); +export const createConfigIO = vi.fn(() => ({ + readConfigFileSnapshot: legacyReadConfigFileSnapshot, +})); + +export const findLegacyGatewayServices = vi.fn().mockResolvedValue([]); +export const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]); +export const findExtraGatewayServices = vi.fn().mockResolvedValue([]); +export const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]); +export const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ + programArguments: ["node", "cli", "gateway", "--port", "18789"], +}); +export const serviceInstall = vi.fn().mockResolvedValue(undefined); +export const serviceIsLoaded = vi.fn().mockResolvedValue(false); +export const serviceStop = vi.fn().mockResolvedValue(undefined); +export const serviceRestart = vi.fn().mockResolvedValue(undefined); +export const serviceUninstall = vi.fn().mockResolvedValue(undefined); +export const callGateway = vi.fn().mockRejectedValue(new Error("gateway closed")); + +vi.mock("@clack/prompts", () => ({ + confirm, + intro: vi.fn(), + note, + outro: vi.fn(), + select, +})); + +vi.mock("../agents/skills-status.js", () => ({ + buildWorkspaceSkillStatus: () => ({ skills: [] }), +})); + +vi.mock("../plugins/loader.js", () => ({ + loadOpenClawPlugins: () => ({ plugins: [], diagnostics: [] }), +})); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + CONFIG_PATH: "/tmp/openclaw.json", + createConfigIO, + readConfigFileSnapshot, + writeConfigFile, + migrateLegacyConfig, + }; +}); + +vi.mock("../daemon/legacy.js", () => ({ + findLegacyGatewayServices, + uninstallLegacyGatewayServices, +})); + +vi.mock("../daemon/inspect.js", () => ({ + findExtraGatewayServices, + renderGatewayServiceCleanupHints, +})); + +vi.mock("../daemon/program-args.js", () => ({ + resolveGatewayProgramArguments, +})); + +vi.mock("../gateway/call.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + callGateway, + }; +}); + +vi.mock("../process/exec.js", () => ({ + runExec, + runCommandWithTimeout, +})); + +vi.mock("../infra/openclaw-root.js", () => ({ + resolveOpenClawPackageRoot, +})); + +vi.mock("../infra/update-runner.js", () => ({ + runGatewayUpdate, +})); + +vi.mock("../agents/auth-profiles.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ensureAuthProfileStore, + }; +}); + +vi.mock("../daemon/service.js", () => ({ + resolveGatewayService: () => ({ + label: "LaunchAgent", + loadedText: "loaded", + notLoadedText: "not loaded", + install: serviceInstall, + uninstall: serviceUninstall, + stop: serviceStop, + restart: serviceRestart, + isLoaded: serviceIsLoaded, + readCommand: vi.fn(), + readRuntime: vi.fn().mockResolvedValue({ status: "running" }), + }), +})); + +vi.mock("../pairing/pairing-store.js", () => ({ + readChannelAllowFromStore: vi.fn().mockResolvedValue([]), + upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), +})); + +vi.mock("../telegram/token.js", () => ({ + resolveTelegramToken: vi.fn(() => ({ token: "", source: "none" })), +})); + +vi.mock("../runtime.js", () => ({ + defaultRuntime: { + log: () => {}, + error: () => {}, + exit: () => { + throw new Error("exit"); + }, + }, +})); + +vi.mock("../utils.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveUserPath: (value: string) => value, + sleep: vi.fn(), + }; +}); + +vi.mock("./health.js", () => ({ + healthCommand: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("./onboard-helpers.js", () => ({ + applyWizardMetadata: (cfg: Record) => cfg, + DEFAULT_WORKSPACE: "/tmp", + guardCancel: (value: unknown) => value, + printWizardHeader: vi.fn(), + randomToken: vi.fn(() => "test-gateway-token"), +})); + +vi.mock("./doctor-state-migrations.js", () => ({ + autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({ + migrated: false, + skipped: false, + changes: [], + warnings: [], + }), + detectLegacyStateMigrations: vi.fn().mockResolvedValue({ + targetAgentId: "main", + targetMainKey: "main", + targetScope: undefined, + stateDir: "/tmp/state", + oauthDir: "/tmp/oauth", + sessions: { + legacyDir: "/tmp/state/sessions", + legacyStorePath: "/tmp/state/sessions/sessions.json", + targetDir: "/tmp/state/agents/main/sessions", + targetStorePath: "/tmp/state/agents/main/sessions/sessions.json", + hasLegacy: false, + legacyKeys: [], + }, + agentDir: { + legacyDir: "/tmp/state/agent", + targetDir: "/tmp/state/agents/main/agent", + hasLegacy: false, + }, + whatsappAuth: { + legacyDir: "/tmp/oauth", + targetDir: "/tmp/oauth/whatsapp/default", + hasLegacy: false, + }, + preview: [], + }), + runLegacyStateMigrations: vi.fn().mockResolvedValue({ + changes: [], + warnings: [], + }), +})); + +beforeEach(() => { + confirm.mockReset().mockResolvedValue(true); + select.mockReset().mockResolvedValue("node"); + note.mockClear(); + + readConfigFileSnapshot.mockReset(); + writeConfigFile.mockReset().mockResolvedValue(undefined); + resolveOpenClawPackageRoot.mockReset().mockResolvedValue(null); + runGatewayUpdate.mockReset().mockResolvedValue({ + status: "skipped", + mode: "unknown", + steps: [], + durationMs: 0, + }); + legacyReadConfigFileSnapshot.mockReset().mockResolvedValue({ + path: "/tmp/openclaw.json", + exists: false, + raw: null, + parsed: {}, + valid: true, + config: {}, + issues: [], + legacyIssues: [], + }); + createConfigIO.mockReset().mockImplementation(() => ({ + readConfigFileSnapshot: legacyReadConfigFileSnapshot, + })); + runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); + runCommandWithTimeout.mockReset().mockResolvedValue({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + }); + ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} }); + migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({ + config: raw as Record, + changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], + })); + findLegacyGatewayServices.mockReset().mockResolvedValue([]); + uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]); + findExtraGatewayServices.mockReset().mockResolvedValue([]); + renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]); + resolveGatewayProgramArguments.mockReset().mockResolvedValue({ + programArguments: ["node", "cli", "gateway", "--port", "18789"], + }); + serviceInstall.mockReset().mockResolvedValue(undefined); + serviceIsLoaded.mockReset().mockResolvedValue(false); + serviceStop.mockReset().mockResolvedValue(undefined); + serviceRestart.mockReset().mockResolvedValue(undefined); + serviceUninstall.mockReset().mockResolvedValue(undefined); + callGateway.mockReset().mockRejectedValue(new Error("gateway closed")); + + originalIsTTY = process.stdin.isTTY; + setStdinTty(true); + originalStateDir = process.env.OPENCLAW_STATE_DIR; + originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; + process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; + tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), { + recursive: true, + }); + fs.mkdirSync(path.join(tempStateDir, "credentials"), { recursive: true }); +}); + +afterEach(() => { + setStdinTty(originalIsTTY); + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } + if (originalUpdateInProgress === undefined) { + delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; + } else { + process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress; + } + if (tempStateDir) { + fs.rmSync(tempStateDir, { recursive: true, force: true }); + tempStateDir = undefined; + } +}); 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 493bdd972..efd02ddf0 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,329 +1,17 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -let originalIsTTY: boolean | undefined; -let originalStateDir: string | undefined; -let originalUpdateInProgress: string | undefined; -let tempStateDir: string | undefined; - -function setStdinTty(value: boolean | undefined) { - try { - Object.defineProperty(process.stdin, "isTTY", { - value, - configurable: true, - }); - } catch { - // ignore - } -} - -beforeEach(() => { - confirm.mockReset().mockResolvedValue(true); - select.mockReset().mockResolvedValue("node"); - note.mockClear(); - - readConfigFileSnapshot.mockReset(); - writeConfigFile.mockReset().mockResolvedValue(undefined); - resolveOpenClawPackageRoot.mockReset().mockResolvedValue(null); - runGatewayUpdate.mockReset().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, - }); - legacyReadConfigFileSnapshot.mockReset().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); - createConfigIO.mockReset().mockImplementation(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, - })); - runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); - runCommandWithTimeout.mockReset().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, - }); - ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} }); - migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], - })); - findLegacyGatewayServices.mockReset().mockResolvedValue([]); - uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]); - findExtraGatewayServices.mockReset().mockResolvedValue([]); - renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]); - resolveGatewayProgramArguments.mockReset().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], - }); - serviceInstall.mockReset().mockResolvedValue(undefined); - serviceIsLoaded.mockReset().mockResolvedValue(false); - serviceStop.mockReset().mockResolvedValue(undefined); - serviceRestart.mockReset().mockResolvedValue(undefined); - serviceUninstall.mockReset().mockResolvedValue(undefined); - callGateway.mockReset().mockRejectedValue(new Error("gateway closed")); - - originalIsTTY = process.stdin.isTTY; - setStdinTty(true); - originalStateDir = process.env.OPENCLAW_STATE_DIR; - originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; - process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; - tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), { - recursive: true, - }); - fs.mkdirSync(path.join(tempStateDir, "credentials"), { recursive: true }); -}); - -afterEach(() => { - setStdinTty(originalIsTTY); - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } - if (originalUpdateInProgress === undefined) { - delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; - } else { - process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress; - } - if (tempStateDir) { - fs.rmSync(tempStateDir, { recursive: true, force: true }); - tempStateDir = undefined; - } -}); - -const readConfigFileSnapshot = vi.fn(); -const confirm = vi.fn().mockResolvedValue(true); -const select = vi.fn().mockResolvedValue("node"); -const note = vi.fn(); -const writeConfigFile = vi.fn().mockResolvedValue(undefined); -const resolveOpenClawPackageRoot = vi.fn().mockResolvedValue(null); -const runGatewayUpdate = vi.fn().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, -}); -const migrateLegacyConfig = vi.fn((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], -})); - -const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); -const runCommandWithTimeout = vi.fn().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, -}); - -const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} }); - -const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], -}); -const createConfigIO = vi.fn(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, -})); - -const findLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const findExtraGatewayServices = vi.fn().mockResolvedValue([]); -const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]); -const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], -}); -const serviceInstall = vi.fn().mockResolvedValue(undefined); -const serviceIsLoaded = vi.fn().mockResolvedValue(false); -const serviceStop = vi.fn().mockResolvedValue(undefined); -const serviceRestart = vi.fn().mockResolvedValue(undefined); -const serviceUninstall = vi.fn().mockResolvedValue(undefined); -const callGateway = vi.fn().mockRejectedValue(new Error("gateway closed")); - -vi.mock("@clack/prompts", () => ({ - confirm, - intro: vi.fn(), - note, - outro: vi.fn(), - select, -})); - -vi.mock("../agents/skills-status.js", () => ({ - buildWorkspaceSkillStatus: () => ({ skills: [] }), -})); - -vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: () => ({ plugins: [], diagnostics: [] }), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - CONFIG_PATH: "/tmp/openclaw.json", - createConfigIO, - readConfigFileSnapshot, - writeConfigFile, - migrateLegacyConfig, - }; -}); - -vi.mock("../daemon/legacy.js", () => ({ +import { describe, expect, it, vi } from "vitest"; +import { findLegacyGatewayServices, - uninstallLegacyGatewayServices, -})); - -vi.mock("../daemon/inspect.js", () => ({ - findExtraGatewayServices, - renderGatewayServiceCleanupHints, -})); - -vi.mock("../daemon/program-args.js", () => ({ - resolveGatewayProgramArguments, -})); - -vi.mock("../gateway/call.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - callGateway, - }; -}); - -vi.mock("../process/exec.js", () => ({ - runExec, - runCommandWithTimeout, -})); - -vi.mock("../infra/openclaw-root.js", () => ({ + note, + readConfigFileSnapshot, resolveOpenClawPackageRoot, -})); - -vi.mock("../infra/update-runner.js", () => ({ + runCommandWithTimeout, runGatewayUpdate, -})); - -vi.mock("../agents/auth-profiles.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ensureAuthProfileStore, - }; -}); - -vi.mock("../daemon/service.js", () => ({ - resolveGatewayService: () => ({ - label: "LaunchAgent", - loadedText: "loaded", - notLoadedText: "not loaded", - install: serviceInstall, - uninstall: serviceUninstall, - stop: serviceStop, - restart: serviceRestart, - isLoaded: serviceIsLoaded, - readCommand: vi.fn(), - readRuntime: vi.fn().mockResolvedValue({ status: "running" }), - }), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), -})); - -vi.mock("../telegram/token.js", () => ({ - resolveTelegramToken: vi.fn(() => ({ token: "", source: "none" })), -})); - -vi.mock("../runtime.js", () => ({ - defaultRuntime: { - log: () => {}, - error: () => {}, - exit: () => { - throw new Error("exit"); - }, - }, -})); - -vi.mock("../utils.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveUserPath: (value: string) => value, - sleep: vi.fn(), - }; -}); - -vi.mock("./health.js", () => ({ - healthCommand: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("./onboard-helpers.js", () => ({ - applyWizardMetadata: (cfg: Record) => cfg, - DEFAULT_WORKSPACE: "/tmp", - guardCancel: (value: unknown) => value, - printWizardHeader: vi.fn(), - randomToken: vi.fn(() => "test-gateway-token"), -})); - -vi.mock("./doctor-state-migrations.js", () => ({ - autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({ - migrated: false, - skipped: false, - changes: [], - warnings: [], - }), - detectLegacyStateMigrations: vi.fn().mockResolvedValue({ - targetAgentId: "main", - targetMainKey: "main", - targetScope: undefined, - stateDir: "/tmp/state", - oauthDir: "/tmp/oauth", - sessions: { - legacyDir: "/tmp/state/sessions", - legacyStorePath: "/tmp/state/sessions/sessions.json", - targetDir: "/tmp/state/agents/main/sessions", - targetStorePath: "/tmp/state/agents/main/sessions/sessions.json", - hasLegacy: false, - legacyKeys: [], - }, - agentDir: { - legacyDir: "/tmp/state/agent", - targetDir: "/tmp/state/agents/main/agent", - hasLegacy: false, - }, - whatsappAuth: { - legacyDir: "/tmp/oauth", - targetDir: "/tmp/oauth/whatsapp/default", - hasLegacy: false, - }, - preview: [], - }), - runLegacyStateMigrations: vi.fn().mockResolvedValue({ - changes: [], - warnings: [], - }), -})); + 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 () => { 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 5ecaaf2cd..6cf24b90e 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,329 +1,12 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -let originalIsTTY: boolean | undefined; -let originalStateDir: string | undefined; -let originalUpdateInProgress: string | undefined; -let tempStateDir: string | undefined; - -function setStdinTty(value: boolean | undefined) { - try { - Object.defineProperty(process.stdin, "isTTY", { - value, - configurable: true, - }); - } catch { - // ignore - } -} - -beforeEach(() => { - confirm.mockReset().mockResolvedValue(true); - select.mockReset().mockResolvedValue("node"); - note.mockClear(); - - readConfigFileSnapshot.mockReset(); - writeConfigFile.mockReset().mockResolvedValue(undefined); - resolveOpenClawPackageRoot.mockReset().mockResolvedValue(null); - runGatewayUpdate.mockReset().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, - }); - legacyReadConfigFileSnapshot.mockReset().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); - createConfigIO.mockReset().mockImplementation(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, - })); - runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); - runCommandWithTimeout.mockReset().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, - }); - ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} }); - migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], - })); - findLegacyGatewayServices.mockReset().mockResolvedValue([]); - uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]); - findExtraGatewayServices.mockReset().mockResolvedValue([]); - renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]); - resolveGatewayProgramArguments.mockReset().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], - }); - serviceInstall.mockReset().mockResolvedValue(undefined); - serviceIsLoaded.mockReset().mockResolvedValue(false); - serviceStop.mockReset().mockResolvedValue(undefined); - serviceRestart.mockReset().mockResolvedValue(undefined); - serviceUninstall.mockReset().mockResolvedValue(undefined); - callGateway.mockReset().mockRejectedValue(new Error("gateway closed")); - - originalIsTTY = process.stdin.isTTY; - setStdinTty(true); - originalStateDir = process.env.OPENCLAW_STATE_DIR; - originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; - process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; - tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), { - recursive: true, - }); - fs.mkdirSync(path.join(tempStateDir, "credentials"), { recursive: true }); -}); - -afterEach(() => { - setStdinTty(originalIsTTY); - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } - if (originalUpdateInProgress === undefined) { - delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; - } else { - process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress; - } - if (tempStateDir) { - fs.rmSync(tempStateDir, { recursive: true, force: true }); - tempStateDir = undefined; - } -}); - -const readConfigFileSnapshot = vi.fn(); -const confirm = vi.fn().mockResolvedValue(true); -const select = vi.fn().mockResolvedValue("node"); -const note = vi.fn(); -const writeConfigFile = vi.fn().mockResolvedValue(undefined); -const resolveOpenClawPackageRoot = vi.fn().mockResolvedValue(null); -const runGatewayUpdate = vi.fn().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, -}); -const migrateLegacyConfig = vi.fn((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], -})); - -const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); -const runCommandWithTimeout = vi.fn().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, -}); - -const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} }); - -const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], -}); -const createConfigIO = vi.fn(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, -})); - -const findLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const findExtraGatewayServices = vi.fn().mockResolvedValue([]); -const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]); -const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], -}); -const serviceInstall = vi.fn().mockResolvedValue(undefined); -const serviceIsLoaded = vi.fn().mockResolvedValue(false); -const serviceStop = vi.fn().mockResolvedValue(undefined); -const serviceRestart = vi.fn().mockResolvedValue(undefined); -const serviceUninstall = vi.fn().mockResolvedValue(undefined); -const callGateway = vi.fn().mockRejectedValue(new Error("gateway closed")); - -vi.mock("@clack/prompts", () => ({ +import { describe, expect, it, vi } from "vitest"; +import { confirm, - intro: vi.fn(), - note, - outro: vi.fn(), - select, -})); - -vi.mock("../agents/skills-status.js", () => ({ - buildWorkspaceSkillStatus: () => ({ skills: [] }), -})); - -vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: () => ({ plugins: [], diagnostics: [] }), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - CONFIG_PATH: "/tmp/openclaw.json", - createConfigIO, - readConfigFileSnapshot, - writeConfigFile, - migrateLegacyConfig, - }; -}); - -vi.mock("../daemon/legacy.js", () => ({ - findLegacyGatewayServices, - uninstallLegacyGatewayServices, -})); - -vi.mock("../daemon/inspect.js", () => ({ - findExtraGatewayServices, - renderGatewayServiceCleanupHints, -})); - -vi.mock("../daemon/program-args.js", () => ({ - resolveGatewayProgramArguments, -})); - -vi.mock("../gateway/call.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - callGateway, - }; -}); - -vi.mock("../process/exec.js", () => ({ - runExec, - runCommandWithTimeout, -})); - -vi.mock("../infra/openclaw-root.js", () => ({ - resolveOpenClawPackageRoot, -})); - -vi.mock("../infra/update-runner.js", () => ({ - runGatewayUpdate, -})); - -vi.mock("../agents/auth-profiles.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ensureAuthProfileStore, - }; -}); - -vi.mock("../daemon/service.js", () => ({ - resolveGatewayService: () => ({ - label: "LaunchAgent", - loadedText: "loaded", - notLoadedText: "not loaded", - install: serviceInstall, - uninstall: serviceUninstall, - stop: serviceStop, - restart: serviceRestart, - isLoaded: serviceIsLoaded, - readCommand: vi.fn(), - readRuntime: vi.fn().mockResolvedValue({ status: "running" }), - }), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), -})); - -vi.mock("../telegram/token.js", () => ({ - resolveTelegramToken: vi.fn(() => ({ token: "", source: "none" })), -})); - -vi.mock("../runtime.js", () => ({ - defaultRuntime: { - log: () => {}, - error: () => {}, - exit: () => { - throw new Error("exit"); - }, - }, -})); - -vi.mock("../utils.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveUserPath: (value: string) => value, - sleep: vi.fn(), - }; -}); - -vi.mock("./health.js", () => ({ - healthCommand: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("./onboard-helpers.js", () => ({ - applyWizardMetadata: (cfg: Record) => cfg, - DEFAULT_WORKSPACE: "/tmp", - guardCancel: (value: unknown) => value, - printWizardHeader: vi.fn(), - randomToken: vi.fn(() => "test-gateway-token"), -})); - -vi.mock("./doctor-state-migrations.js", () => ({ - autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({ - migrated: false, - skipped: false, - changes: [], - warnings: [], - }), - detectLegacyStateMigrations: vi.fn().mockResolvedValue({ - targetAgentId: "main", - targetMainKey: "main", - targetScope: undefined, - stateDir: "/tmp/state", - oauthDir: "/tmp/oauth", - sessions: { - legacyDir: "/tmp/state/sessions", - legacyStorePath: "/tmp/state/sessions/sessions.json", - targetDir: "/tmp/state/agents/main/sessions", - targetStorePath: "/tmp/state/agents/main/sessions/sessions.json", - hasLegacy: false, - legacyKeys: [], - }, - agentDir: { - legacyDir: "/tmp/state/agent", - targetDir: "/tmp/state/agents/main/agent", - hasLegacy: false, - }, - whatsappAuth: { - legacyDir: "/tmp/oauth", - targetDir: "/tmp/oauth/whatsapp/default", - hasLegacy: false, - }, - preview: [], - }), - runLegacyStateMigrations: vi.fn().mockResolvedValue({ - changes: [], - warnings: [], - }), -})); + ensureAuthProfileStore, + readConfigFileSnapshot, + serviceIsLoaded, + serviceRestart, + writeConfigFile, +} from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("runs legacy state migrations in yes mode without prompting", async () => { 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 3846b71d9..822d5f813 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 @@ -1,329 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -let originalIsTTY: boolean | undefined; -let originalStateDir: string | undefined; -let originalUpdateInProgress: string | undefined; -let tempStateDir: string | undefined; - -function setStdinTty(value: boolean | undefined) { - try { - Object.defineProperty(process.stdin, "isTTY", { - value, - configurable: true, - }); - } catch { - // ignore - } -} - -beforeEach(() => { - confirm.mockReset().mockResolvedValue(true); - select.mockReset().mockResolvedValue("node"); - note.mockClear(); - - readConfigFileSnapshot.mockReset(); - writeConfigFile.mockReset().mockResolvedValue(undefined); - resolveOpenClawPackageRoot.mockReset().mockResolvedValue(null); - runGatewayUpdate.mockReset().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, - }); - legacyReadConfigFileSnapshot.mockReset().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); - createConfigIO.mockReset().mockImplementation(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, - })); - runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); - runCommandWithTimeout.mockReset().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, - }); - ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} }); - migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], - })); - findLegacyGatewayServices.mockReset().mockResolvedValue([]); - uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]); - findExtraGatewayServices.mockReset().mockResolvedValue([]); - renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]); - resolveGatewayProgramArguments.mockReset().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], - }); - serviceInstall.mockReset().mockResolvedValue(undefined); - serviceIsLoaded.mockReset().mockResolvedValue(false); - serviceStop.mockReset().mockResolvedValue(undefined); - serviceRestart.mockReset().mockResolvedValue(undefined); - serviceUninstall.mockReset().mockResolvedValue(undefined); - callGateway.mockReset().mockRejectedValue(new Error("gateway closed")); - - originalIsTTY = process.stdin.isTTY; - setStdinTty(true); - originalStateDir = process.env.OPENCLAW_STATE_DIR; - originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; - process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; - tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), { - recursive: true, - }); - fs.mkdirSync(path.join(tempStateDir, "credentials"), { recursive: true }); -}); - -afterEach(() => { - setStdinTty(originalIsTTY); - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } - if (originalUpdateInProgress === undefined) { - delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; - } else { - process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress; - } - if (tempStateDir) { - fs.rmSync(tempStateDir, { recursive: true, force: true }); - tempStateDir = undefined; - } -}); - -const readConfigFileSnapshot = vi.fn(); -const confirm = vi.fn().mockResolvedValue(true); -const select = vi.fn().mockResolvedValue("node"); -const note = vi.fn(); -const writeConfigFile = vi.fn().mockResolvedValue(undefined); -const resolveOpenClawPackageRoot = vi.fn().mockResolvedValue(null); -const runGatewayUpdate = vi.fn().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, -}); -const migrateLegacyConfig = vi.fn((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], -})); - -const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); -const runCommandWithTimeout = vi.fn().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, -}); - -const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} }); - -const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], -}); -const createConfigIO = vi.fn(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, -})); - -const findLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const findExtraGatewayServices = vi.fn().mockResolvedValue([]); -const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]); -const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], -}); -const serviceInstall = vi.fn().mockResolvedValue(undefined); -const serviceIsLoaded = vi.fn().mockResolvedValue(false); -const serviceStop = vi.fn().mockResolvedValue(undefined); -const serviceRestart = vi.fn().mockResolvedValue(undefined); -const serviceUninstall = vi.fn().mockResolvedValue(undefined); -const callGateway = vi.fn().mockRejectedValue(new Error("gateway closed")); - -vi.mock("@clack/prompts", () => ({ - confirm, - intro: vi.fn(), - note, - outro: vi.fn(), - select, -})); - -vi.mock("../agents/skills-status.js", () => ({ - buildWorkspaceSkillStatus: () => ({ skills: [] }), -})); - -vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: () => ({ plugins: [], diagnostics: [] }), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - CONFIG_PATH: "/tmp/openclaw.json", - createConfigIO, - readConfigFileSnapshot, - writeConfigFile, - migrateLegacyConfig, - }; -}); - -vi.mock("../daemon/legacy.js", () => ({ - findLegacyGatewayServices, - uninstallLegacyGatewayServices, -})); - -vi.mock("../daemon/inspect.js", () => ({ - findExtraGatewayServices, - renderGatewayServiceCleanupHints, -})); - -vi.mock("../daemon/program-args.js", () => ({ - resolveGatewayProgramArguments, -})); - -vi.mock("../gateway/call.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - callGateway, - }; -}); - -vi.mock("../process/exec.js", () => ({ - runExec, - runCommandWithTimeout, -})); - -vi.mock("../infra/openclaw-root.js", () => ({ - resolveOpenClawPackageRoot, -})); - -vi.mock("../infra/update-runner.js", () => ({ - runGatewayUpdate, -})); - -vi.mock("../agents/auth-profiles.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ensureAuthProfileStore, - }; -}); - -vi.mock("../daemon/service.js", () => ({ - resolveGatewayService: () => ({ - label: "LaunchAgent", - loadedText: "loaded", - notLoadedText: "not loaded", - install: serviceInstall, - uninstall: serviceUninstall, - stop: serviceStop, - restart: serviceRestart, - isLoaded: serviceIsLoaded, - readCommand: vi.fn(), - readRuntime: vi.fn().mockResolvedValue({ status: "running" }), - }), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), -})); - -vi.mock("../telegram/token.js", () => ({ - resolveTelegramToken: vi.fn(() => ({ token: "", source: "none" })), -})); - -vi.mock("../runtime.js", () => ({ - defaultRuntime: { - log: () => {}, - error: () => {}, - exit: () => { - throw new Error("exit"); - }, - }, -})); - -vi.mock("../utils.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveUserPath: (value: string) => value, - sleep: vi.fn(), - }; -}); - -vi.mock("./health.js", () => ({ - healthCommand: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("./onboard-helpers.js", () => ({ - applyWizardMetadata: (cfg: Record) => cfg, - DEFAULT_WORKSPACE: "/tmp", - guardCancel: (value: unknown) => value, - printWizardHeader: vi.fn(), - randomToken: vi.fn(() => "test-gateway-token"), -})); - -vi.mock("./doctor-state-migrations.js", () => ({ - autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({ - migrated: false, - skipped: false, - changes: [], - warnings: [], - }), - detectLegacyStateMigrations: vi.fn().mockResolvedValue({ - targetAgentId: "main", - targetMainKey: "main", - targetScope: undefined, - stateDir: "/tmp/state", - oauthDir: "/tmp/oauth", - sessions: { - legacyDir: "/tmp/state/sessions", - legacyStorePath: "/tmp/state/sessions/sessions.json", - targetDir: "/tmp/state/agents/main/sessions", - targetStorePath: "/tmp/state/agents/main/sessions/sessions.json", - hasLegacy: false, - legacyKeys: [], - }, - agentDir: { - legacyDir: "/tmp/state/agent", - targetDir: "/tmp/state/agents/main/agent", - hasLegacy: false, - }, - whatsappAuth: { - legacyDir: "/tmp/oauth", - targetDir: "/tmp/oauth/whatsapp/default", - hasLegacy: false, - }, - preview: [], - }), - runLegacyStateMigrations: vi.fn().mockResolvedValue({ - changes: [], - warnings: [], - }), -})); +import { describe, expect, it, vi } from "vitest"; +import { note, readConfigFileSnapshot } from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("warns when per-agent sandbox docker/browser/prune overrides are ignored under shared scope", async () => { 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 fa3577ba1..6f49bd6db 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,333 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -let originalIsTTY: boolean | undefined; -let originalStateDir: string | undefined; -let originalUpdateInProgress: string | undefined; -let tempStateDir: string | undefined; - -function setStdinTty(value: boolean | undefined) { - try { - Object.defineProperty(process.stdin, "isTTY", { - value, - configurable: true, - }); - } catch { - // ignore - } -} - -beforeEach(() => { - confirm.mockReset().mockResolvedValue(true); - select.mockReset().mockResolvedValue("node"); - note.mockClear(); - - readConfigFileSnapshot.mockReset(); - writeConfigFile.mockReset().mockResolvedValue(undefined); - resolveOpenClawPackageRoot.mockReset().mockResolvedValue(null); - runGatewayUpdate.mockReset().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, - }); - legacyReadConfigFileSnapshot.mockReset().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], - }); - createConfigIO.mockReset().mockImplementation(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, - })); - runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); - runCommandWithTimeout.mockReset().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, - }); - ensureAuthProfileStore.mockReset().mockReturnValue({ version: 1, profiles: {} }); - migrateLegacyConfig.mockReset().mockImplementation((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], - })); - findLegacyGatewayServices.mockReset().mockResolvedValue([]); - uninstallLegacyGatewayServices.mockReset().mockResolvedValue([]); - findExtraGatewayServices.mockReset().mockResolvedValue([]); - renderGatewayServiceCleanupHints.mockReset().mockReturnValue(["cleanup"]); - resolveGatewayProgramArguments.mockReset().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], - }); - serviceInstall.mockReset().mockResolvedValue(undefined); - serviceIsLoaded.mockReset().mockResolvedValue(false); - serviceStop.mockReset().mockResolvedValue(undefined); - serviceRestart.mockReset().mockResolvedValue(undefined); - serviceUninstall.mockReset().mockResolvedValue(undefined); - callGateway.mockReset().mockRejectedValue(new Error("gateway closed")); - - originalIsTTY = process.stdin.isTTY; - setStdinTty(true); - originalStateDir = process.env.OPENCLAW_STATE_DIR; - originalUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS; - process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; - tempStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - fs.mkdirSync(path.join(tempStateDir, "agents", "main", "sessions"), { - recursive: true, - }); - fs.mkdirSync(path.join(tempStateDir, "credentials"), { recursive: true }); -}); - -afterEach(() => { - setStdinTty(originalIsTTY); - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } - if (originalUpdateInProgress === undefined) { - delete process.env.OPENCLAW_UPDATE_IN_PROGRESS; - } else { - process.env.OPENCLAW_UPDATE_IN_PROGRESS = originalUpdateInProgress; - } - if (tempStateDir) { - fs.rmSync(tempStateDir, { recursive: true, force: true }); - tempStateDir = undefined; - } -}); - -const readConfigFileSnapshot = vi.fn(); -const confirm = vi.fn().mockResolvedValue(true); -const select = vi.fn().mockResolvedValue("node"); -const note = vi.fn(); -const writeConfigFile = vi.fn().mockResolvedValue(undefined); -const resolveOpenClawPackageRoot = vi.fn().mockResolvedValue(null); -const runGatewayUpdate = vi.fn().mockResolvedValue({ - status: "skipped", - mode: "unknown", - steps: [], - durationMs: 0, -}); -const migrateLegacyConfig = vi.fn((raw: unknown) => ({ - config: raw as Record, - changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."], -})); - -const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); -const runCommandWithTimeout = vi.fn().mockResolvedValue({ - stdout: "", - stderr: "", - code: 0, - signal: null, - killed: false, -}); - -const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} }); - -const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({ - path: "/tmp/openclaw.json", - exists: false, - raw: null, - parsed: {}, - valid: true, - config: {}, - issues: [], - legacyIssues: [], -}); -const createConfigIO = vi.fn(() => ({ - readConfigFileSnapshot: legacyReadConfigFileSnapshot, -})); - -const findLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]); -const findExtraGatewayServices = vi.fn().mockResolvedValue([]); -const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]); -const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ - programArguments: ["node", "cli", "gateway", "--port", "18789"], -}); -const serviceInstall = vi.fn().mockResolvedValue(undefined); -const serviceIsLoaded = vi.fn().mockResolvedValue(false); -const serviceStop = vi.fn().mockResolvedValue(undefined); -const serviceRestart = vi.fn().mockResolvedValue(undefined); -const serviceUninstall = vi.fn().mockResolvedValue(undefined); -const callGateway = vi.fn().mockRejectedValue(new Error("gateway closed")); - -vi.mock("@clack/prompts", () => ({ - confirm, - intro: vi.fn(), - note, - outro: vi.fn(), - select, -})); - -vi.mock("../agents/skills-status.js", () => ({ - buildWorkspaceSkillStatus: () => ({ skills: [] }), -})); - -vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: () => ({ plugins: [], diagnostics: [] }), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - CONFIG_PATH: "/tmp/openclaw.json", - createConfigIO, - readConfigFileSnapshot, - writeConfigFile, - migrateLegacyConfig, - }; -}); - -vi.mock("../daemon/legacy.js", () => ({ - findLegacyGatewayServices, - uninstallLegacyGatewayServices, -})); - -vi.mock("../daemon/inspect.js", () => ({ - findExtraGatewayServices, - renderGatewayServiceCleanupHints, -})); - -vi.mock("../daemon/program-args.js", () => ({ - resolveGatewayProgramArguments, -})); - -vi.mock("../gateway/call.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - callGateway, - }; -}); - -vi.mock("../process/exec.js", () => ({ - runExec, - runCommandWithTimeout, -})); - -vi.mock("../infra/openclaw-root.js", () => ({ - resolveOpenClawPackageRoot, -})); - -vi.mock("../infra/update-runner.js", () => ({ - runGatewayUpdate, -})); - -vi.mock("../agents/auth-profiles.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ensureAuthProfileStore, - }; -}); - -vi.mock("../daemon/service.js", () => ({ - resolveGatewayService: () => ({ - label: "LaunchAgent", - loadedText: "loaded", - notLoadedText: "not loaded", - install: serviceInstall, - uninstall: serviceUninstall, - stop: serviceStop, - restart: serviceRestart, - isLoaded: serviceIsLoaded, - readCommand: vi.fn(), - readRuntime: vi.fn().mockResolvedValue({ status: "running" }), - }), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }), -})); - -vi.mock("../telegram/token.js", () => ({ - resolveTelegramToken: vi.fn(() => ({ token: "", source: "none" })), -})); - -vi.mock("../runtime.js", () => ({ - defaultRuntime: { - log: () => {}, - error: () => {}, - exit: () => { - throw new Error("exit"); - }, - }, -})); - -vi.mock("../utils.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveUserPath: (value: string) => value, - sleep: vi.fn(), - }; -}); - -vi.mock("./health.js", () => ({ - healthCommand: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("./onboard-helpers.js", () => ({ - applyWizardMetadata: (cfg: Record) => cfg, - DEFAULT_WORKSPACE: "/tmp", - guardCancel: (value: unknown) => value, - printWizardHeader: vi.fn(), - randomToken: vi.fn(() => "test-gateway-token"), -})); - -vi.mock("./doctor-state-migrations.js", () => ({ - autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({ - migrated: false, - skipped: false, - changes: [], - warnings: [], - }), - detectLegacyStateMigrations: vi.fn().mockResolvedValue({ - targetAgentId: "main", - targetMainKey: "main", - targetScope: undefined, - stateDir: "/tmp/state", - oauthDir: "/tmp/oauth", - sessions: { - legacyDir: "/tmp/state/sessions", - legacyStorePath: "/tmp/state/sessions/sessions.json", - targetDir: "/tmp/state/agents/main/sessions", - targetStorePath: "/tmp/state/agents/main/sessions/sessions.json", - hasLegacy: false, - legacyKeys: [], - }, - agentDir: { - legacyDir: "/tmp/state/agent", - targetDir: "/tmp/state/agents/main/agent", - hasLegacy: false, - }, - whatsappAuth: { - legacyDir: "/tmp/oauth", - targetDir: "/tmp/oauth/whatsapp/default", - hasLegacy: false, - }, - preview: [], - }), - runLegacyStateMigrations: vi.fn().mockResolvedValue({ - changes: [], - warnings: [], - }), -})); - -vi.mock("./doctor-update.js", () => ({ - maybeOfferUpdateBeforeDoctor: vi.fn().mockResolvedValue({ handled: false }), -})); +import { describe, expect, it, vi } from "vitest"; +import { note, readConfigFileSnapshot } from "./doctor.e2e-harness.js"; describe("doctor command", () => { it("warns when the state directory is missing", async () => {