From 23b9daee6fb57fa657b44ab9c831e824377b464b Mon Sep 17 00:00:00 2001 From: Marcus Castro Date: Tue, 24 Feb 2026 10:23:33 -0300 Subject: [PATCH] fix(doctor): improve sandbox warning when Docker unavailable --- src/commands/doctor-sandbox.ts | 11 +- ...rns-sandbox-enabled-without-docker.test.ts | 129 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index aa08fb867..90790e907 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -188,7 +188,16 @@ export async function maybeRepairSandboxImages( const dockerAvailable = await isDockerAvailable(); if (!dockerAvailable) { - note("Docker not available; skipping sandbox image checks.", "Sandbox"); + const lines = [ + `Sandbox mode is enabled (mode: "${mode}") but Docker is not available.`, + "Docker is required for sandbox mode to function.", + "Isolated sessions (cron jobs, sub-agents) will fail without Docker.", + "", + "Options:", + "- Install Docker and restart the gateway", + "- Disable sandbox mode: openclaw config set agents.defaults.sandbox.mode off", + ]; + note(lines.join("\n"), "Sandbox"); return cfg; } diff --git a/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts b/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts new file mode 100644 index 000000000..106066c51 --- /dev/null +++ b/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts @@ -0,0 +1,129 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { DoctorPrompter } from "./doctor-prompter.js"; + +const runExec = vi.fn(); +const note = vi.fn(); + +vi.mock("../process/exec.js", () => ({ + runExec, + runCommandWithTimeout: vi.fn(), +})); + +vi.mock("../terminal/note.js", () => ({ + note, +})); + +describe("maybeRepairSandboxImages", () => { + const mockRuntime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + const mockPrompter: DoctorPrompter = { + confirmSkipInNonInteractive: vi.fn().mockResolvedValue(false), + } as unknown as DoctorPrompter; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("warns when sandbox mode is enabled but Docker is not available", async () => { + // Simulate Docker not available (command fails) + runExec.mockRejectedValue(new Error("Docker not installed")); + + const config: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "non-main", + }, + }, + }, + }; + + const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js"); + await maybeRepairSandboxImages(config, mockRuntime, mockPrompter); + + // The warning should clearly indicate sandbox is enabled but won't work + expect(note).toHaveBeenCalled(); + const noteCall = note.mock.calls[0]; + const message = noteCall[0] as string; + + // The message should warn that sandbox mode won't function, not just "skipping checks" + expect(message).toMatch(/sandbox.*mode.*enabled|sandbox.*won.*work|docker.*required/i); + // Should NOT just say "skipping sandbox image checks" - that's too mild + expect(message).not.toBe("Docker not available; skipping sandbox image checks."); + }); + + it("warns when sandbox mode is 'all' but Docker is not available", async () => { + runExec.mockRejectedValue(new Error("Docker not installed")); + + const config: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + }, + }, + }, + }; + + const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js"); + await maybeRepairSandboxImages(config, mockRuntime, mockPrompter); + + expect(note).toHaveBeenCalled(); + const noteCall = note.mock.calls[0]; + const message = noteCall[0] as string; + + // Should warn about the impact on sandbox functionality + expect(message).toMatch(/sandbox|docker/i); + }); + + it("does not warn when sandbox mode is off", async () => { + runExec.mockRejectedValue(new Error("Docker not installed")); + + const config: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "off", + }, + }, + }, + }; + + const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js"); + await maybeRepairSandboxImages(config, mockRuntime, mockPrompter); + + // No warning needed when sandbox is off + expect(note).not.toHaveBeenCalled(); + }); + + it("does not warn when Docker is available", async () => { + // Simulate Docker available + runExec.mockResolvedValue({ stdout: "24.0.0", stderr: "" }); + + const config: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "non-main", + }, + }, + }, + }; + + const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js"); + await maybeRepairSandboxImages(config, mockRuntime, mockPrompter); + + // May have other notes about images, but not the Docker unavailable warning + const dockerUnavailableWarning = note.mock.calls.find( + (call) => + typeof call[0] === "string" && call[0].toLowerCase().includes("docker not available"), + ); + expect(dockerUnavailableWarning).toBeUndefined(); + }); +});