Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions src/lib/actions/sandbox/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>>>(() => []);
const parseLiveSandboxNamesMock = vi.fn(() => new Set(["alpha"]));
const restoreSandboxStateMock = vi.fn();

vi.mock("../../adapters/docker", () => ({
dockerCapture: vi.fn(() => ""),
Expand Down Expand Up @@ -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", () => ({
Expand All @@ -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", () => ({
Expand All @@ -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"]));
});

Expand All @@ -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({
Expand All @@ -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:");
});
Comment on lines +148 to 174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit timestamp assertions in the snapshot-list test.

The fixtures include timestamps (Lines 154 and 160), but the expectations never validate timestamp rendering. A regression in timestamp output could slip through unnoticed.

Suggested patch
     expect(output).toContain("Snapshots for 'alpha'");
     expect(output).toContain("v1");
     expect(output).toContain("initial");
+    expect(output).toContain("2026-06-01");
+    expect(output).toContain("2026-06-02");
     expect(output).toContain("/tmp/alpha/v2");
     expect(output).toContain("2 snapshot(s). Restore with:");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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:");
});
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("2026-06-01");
expect(output).toContain("2026-06-02");
expect(output).toContain("/tmp/alpha/v2");
expect(output).toContain("2 snapshot(s). Restore with:");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/sandbox/snapshot.test.ts` around lines 148 - 174, The test
fixture for the snapshot list includes timestamp data for both snapshots
(2026-06-01T00:00:00.000Z and 2026-06-02T00:00:00.000Z), but the test
expectations do not validate that these timestamps are actually rendered in the
console output. Add explicit expect assertions to verify that both timestamp
values are included in the output, similar to the existing assertions that check
for version names, snapshot names, and paths. This will ensure that any
regression in timestamp rendering will be caught by the test.

});
Loading