fix(doctor): add headless flags + auto-migrate sessions

This commit is contained in:
Peter Steinberger
2026-01-07 04:43:05 +01:00
parent 9c9ae5aa54
commit 6ffece68b0
9 changed files with 350 additions and 76 deletions

View File

@@ -1,4 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let originalIsTTY: boolean | undefined;
function setStdinTty(value: boolean | undefined) {
try {
Object.defineProperty(process.stdin, "isTTY", {
value,
configurable: true,
});
} catch {
// ignore
}
}
beforeEach(() => {
originalIsTTY = process.stdin.isTTY;
setStdinTty(true);
});
afterEach(() => {
setStdinTty(originalIsTTY);
});
const readConfigFileSnapshot = vi.fn();
const confirm = vi.fn().mockResolvedValue(true);
@@ -443,4 +465,153 @@ describe("doctor", () => {
expect(docker.image).toBe("clawdis-sandbox-common:bookworm-slim");
expect(runCommandWithTimeout).not.toHaveBeenCalled();
});
it("runs legacy state migrations in non-interactive mode without prompting", async () => {
readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/clawdbot.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
config: {},
issues: [],
legacyIssues: [],
});
const { doctorCommand } = await import("./doctor.js");
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const { detectLegacyStateMigrations, runLegacyStateMigrations } =
await import("./doctor-state-migrations.js");
detectLegacyStateMigrations.mockResolvedValueOnce({
targetAgentId: "main",
targetMainKey: "main",
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: true,
},
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: ["- Legacy sessions detected"],
});
runLegacyStateMigrations.mockResolvedValueOnce({
changes: ["migrated"],
warnings: [],
});
confirm.mockClear();
await doctorCommand(runtime, { nonInteractive: true });
expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1);
expect(confirm).not.toHaveBeenCalled();
});
it("runs legacy state migrations in yes mode without prompting", async () => {
readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/clawdbot.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
config: {},
issues: [],
legacyIssues: [],
});
const { doctorCommand } = await import("./doctor.js");
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const { detectLegacyStateMigrations, runLegacyStateMigrations } =
await import("./doctor-state-migrations.js");
detectLegacyStateMigrations.mockResolvedValueOnce({
targetAgentId: "main",
targetMainKey: "main",
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: true,
},
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: ["- Legacy sessions detected"],
});
runLegacyStateMigrations.mockResolvedValueOnce({
changes: ["migrated"],
warnings: [],
});
runLegacyStateMigrations.mockClear();
confirm.mockClear();
await doctorCommand(runtime, { yes: true });
expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1);
expect(confirm).not.toHaveBeenCalled();
});
it("skips gateway restarts in non-interactive mode", async () => {
readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/clawdbot.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
config: {},
issues: [],
legacyIssues: [],
});
const { healthCommand } = await import("./health.js");
healthCommand.mockRejectedValueOnce(new Error("gateway closed"));
serviceIsLoaded.mockResolvedValueOnce(true);
serviceRestart.mockClear();
confirm.mockClear();
const { doctorCommand } = await import("./doctor.js");
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await doctorCommand(runtime, { nonInteractive: true });
expect(serviceRestart).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
});
});