feat(doctor): repair sandbox images

This commit is contained in:
Peter Steinberger
2026-01-04 16:02:24 +00:00
parent e80bd1882f
commit 718299b25a
3 changed files with 363 additions and 14 deletions

View File

@@ -1,12 +1,22 @@
import { describe, expect, it, vi } from "vitest";
const readConfigFileSnapshot = vi.fn();
const confirm = vi.fn().mockResolvedValue(true);
const writeConfigFile = vi.fn().mockResolvedValue(undefined);
const migrateLegacyConfig = vi.fn((raw: unknown) => ({
config: raw as Record<string, unknown>,
changes: ["Moved routing.allowFrom → whatsapp.allowFrom."],
}));
const runExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" });
const runCommandWithTimeout = vi.fn().mockResolvedValue({
stdout: "",
stderr: "",
code: 0,
signal: null,
killed: false,
});
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
path: "/tmp/clawdis.json",
exists: false,
@@ -32,7 +42,7 @@ const serviceRestart = vi.fn().mockResolvedValue(undefined);
const serviceUninstall = vi.fn().mockResolvedValue(undefined);
vi.mock("@clack/prompts", () => ({
confirm: vi.fn().mockResolvedValue(true),
confirm,
intro: vi.fn(),
note: vi.fn(),
outro: vi.fn(),
@@ -42,13 +52,17 @@ vi.mock("../agents/skills-status.js", () => ({
buildWorkspaceSkillStatus: () => ({ skills: [] }),
}));
vi.mock("../config/config.js", () => ({
CONFIG_PATH_CLAWDBOT: "/tmp/clawdbot.json",
createConfigIO,
readConfigFileSnapshot,
writeConfigFile,
migrateLegacyConfig,
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
CONFIG_PATH_CLAWDBOT: "/tmp/clawdbot.json",
createConfigIO,
readConfigFileSnapshot,
writeConfigFile,
migrateLegacyConfig,
};
});
vi.mock("../daemon/legacy.js", () => ({
findLegacyGatewayServices,
@@ -59,6 +73,11 @@ vi.mock("../daemon/program-args.js", () => ({
resolveGatewayProgramArguments,
}));
vi.mock("../process/exec.js", () => ({
runExec,
runCommandWithTimeout,
}));
vi.mock("../daemon/service.js", () => ({
resolveGatewayService: () => ({
label: "LaunchAgent",
@@ -82,10 +101,14 @@ vi.mock("../runtime.js", () => ({
},
}));
vi.mock("../utils.js", () => ({
resolveUserPath: (value: string) => value,
sleep: vi.fn(),
}));
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),
@@ -302,4 +325,75 @@ describe("doctor", () => {
expect(docker.image).toBe("clawdbot-sandbox");
expect(docker.containerPrefix).toBe("clawdbot-sbx");
});
it("falls back to legacy sandbox image when missing", async () => {
readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/clawdbot.json",
exists: true,
raw: "{}",
parsed: {
agent: {
sandbox: {
mode: "non-main",
docker: {
image: "clawdbot-sandbox-common:bookworm-slim",
},
},
},
},
valid: true,
config: {
agent: {
sandbox: {
mode: "non-main",
docker: {
image: "clawdbot-sandbox-common:bookworm-slim",
},
},
},
},
issues: [],
legacyIssues: [],
});
runExec.mockImplementation((command: string, args: string[]) => {
if (command !== "docker") {
return Promise.resolve({ stdout: "", stderr: "" });
}
if (args[0] === "version") {
return Promise.resolve({ stdout: "1", stderr: "" });
}
if (args[0] === "image" && args[1] === "inspect") {
const image = args[2];
if (image === "clawdbot-sandbox-common:bookworm-slim") {
return Promise.reject(new Error("missing"));
}
if (image === "clawdis-sandbox-common:bookworm-slim") {
return Promise.resolve({ stdout: "ok", stderr: "" });
}
}
return Promise.resolve({ stdout: "", stderr: "" });
});
confirm.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
const { doctorCommand } = await import("./doctor.js");
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await doctorCommand(runtime);
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<
string,
unknown
>;
const agent = written.agent as Record<string, unknown>;
const sandbox = agent.sandbox as Record<string, unknown>;
const docker = sandbox.docker as Record<string, unknown>;
expect(docker.image).toBe("clawdis-sandbox-common:bookworm-slim");
expect(runCommandWithTimeout).not.toHaveBeenCalled();
});
});