From 94a1568e58adcb1956c3842e996f15f851aa08ce Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Mon, 15 Jun 2026 17:16:56 -0700 Subject: [PATCH] test(snapshot): cover create and list flows --- src/lib/actions/sandbox/snapshot.test.ts | 84 +++++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/src/lib/actions/sandbox/snapshot.test.ts b/src/lib/actions/sandbox/snapshot.test.ts index 2dd22b61e1..afcec9b78c 100644 --- a/src/lib/actions/sandbox/snapshot.test.ts +++ b/src/lib/actions/sandbox/snapshot.test.ts @@ -7,8 +7,12 @@ const backupSandboxStateMock = vi.fn(); const captureOpenshellMock = vi.fn(() => ({ status: 0, output: "alpha Ready\n" })); const dockerInspectMock = vi.fn(() => ({ status: 0, stdout: "true\n" })); const getSandboxMock = vi.fn(() => null); +const findBackupMock = vi.fn(); const isGatewayHealthyMock = vi.fn(() => true); +const isShieldsDownMock = vi.fn(); +const listBackupsMock = vi.fn<() => Array>>(() => []); const parseLiveSandboxNamesMock = vi.fn(() => new Set(["alpha"])); +const restoreSandboxStateMock = vi.fn(); vi.mock("../../adapters/docker", () => ({ dockerCapture: vi.fn(() => ""), @@ -43,7 +47,8 @@ vi.mock("../../runtime-recovery", () => ({ })); vi.mock("../../shields", () => ({ - isShieldsDown: undefined, + isShieldsDown: isShieldsDownMock, + repairMutableConfigPerms: vi.fn(() => ({ applied: true, verified: true, errors: [] })), })); vi.mock("../../state/gateway", () => ({ @@ -58,8 +63,10 @@ vi.mock("../../state/registry", () => ({ vi.mock("../../state/sandbox", () => ({ backupSandboxState: backupSandboxStateMock, - findBackup: vi.fn(), - listBackups: vi.fn(() => []), + findBackup: findBackupMock, + getLatestBackup: vi.fn(() => null), + listBackups: listBackupsMock, + restoreSandboxState: restoreSandboxStateMock, })); vi.mock("./destroy", () => ({ @@ -72,8 +79,18 @@ describe("runSandboxSnapshot", () => { vi.clearAllMocks(); captureOpenshellMock.mockReturnValue({ status: 0, output: "alpha Ready\n" }); dockerInspectMock.mockReturnValue({ status: 0, stdout: "true\n" }); + findBackupMock.mockReturnValue({ match: null }); getSandboxMock.mockReturnValue(null); isGatewayHealthyMock.mockReturnValue(true); + isShieldsDownMock.mockReturnValue(true); + listBackupsMock.mockReturnValue([]); + restoreSandboxStateMock.mockReturnValue({ + success: true, + restoredDirs: [], + restoredFiles: [], + failedDirs: [], + failedFiles: [], + }); parseLiveSandboxNamesMock.mockReturnValue(new Set(["alpha"])); }); @@ -83,6 +100,8 @@ describe("runSandboxSnapshot", () => { it("refuses snapshot creation before backup when the shields gate helper is unavailable", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + const shields = await import("../../shields"); + vi.mocked(shields).isShieldsDown = undefined as never; const { runSandboxSnapshot } = await import("./snapshot"); await expect(runSandboxSnapshot("alpha", { kind: "create" })).rejects.toMatchObject({ @@ -93,5 +112,64 @@ describe("runSandboxSnapshot", () => { expect(consoleError.mock.calls.flat().join("\n")).toContain( "Cannot verify shields state. Refusing to create snapshot.", ); + vi.mocked(shields).isShieldsDown = isShieldsDownMock as never; + }); + + it("creates a named snapshot after gateway, liveness, and shields checks pass", async () => { + const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); + const manifest = { + timestamp: "2026-06-15T00:00:00.000Z", + backupPath: "/tmp/backup-alpha", + name: "before-upgrade", + }; + backupSandboxStateMock.mockReturnValue({ + success: true, + backedUpDirs: ["workspace"], + backedUpFiles: ["openclaw.json"], + failedDirs: [], + failedFiles: [], + manifest, + }); + findBackupMock.mockReturnValue({ + match: { ...manifest, snapshotVersion: 7, name: "before-upgrade" }, + }); + const { runSandboxSnapshot } = await import("./snapshot"); + + await runSandboxSnapshot("alpha", { kind: "create", name: "before-upgrade" }); + + expect(backupSandboxStateMock).toHaveBeenCalledWith("alpha", { name: "before-upgrade" }); + expect(findBackupMock).toHaveBeenCalledWith("alpha", manifest.timestamp); + const output = consoleLog.mock.calls.flat().join("\n"); + expect(output).toContain("Creating snapshot of 'alpha' (--name before-upgrade)"); + expect(output).toContain("Snapshot v7 name=before-upgrade created"); + expect(output).toContain("/tmp/backup-alpha"); + }); + + it("renders a stable snapshot list with versions, names, timestamps, and paths", async () => { + const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); + listBackupsMock.mockReturnValue([ + { + snapshotVersion: 1, + name: "initial", + timestamp: "2026-06-01T00:00:00.000Z", + backupPath: "/tmp/alpha/v1", + }, + { + snapshotVersion: 2, + name: null, + timestamp: "2026-06-02T00:00:00.000Z", + backupPath: "/tmp/alpha/v2", + }, + ]); + const { runSandboxSnapshot } = await import("./snapshot"); + + await runSandboxSnapshot("alpha", { kind: "list" }); + + const output = consoleLog.mock.calls.flat().join("\n"); + expect(output).toContain("Snapshots for 'alpha'"); + expect(output).toContain("v1"); + expect(output).toContain("initial"); + expect(output).toContain("/tmp/alpha/v2"); + expect(output).toContain("2 snapshot(s). Restore with:"); }); });