import { describe, expect, it, vi } from "vitest";
import { createHandleRestore } from "./rollback-orchestration.js";
function makeFakes() {
const rpc = {
snapshotList: vi.fn(async (_p) => ({
snapshots: [{ snapId: 5, label: "msg-5", committedAt: "now" }],
})),
snapshotRestore: vi.fn(async (_p) => ({
restored: true,
prunedSnapshots: 1,
prunedUndoRows: 3,
})),
unmount: vi.fn(async (_p) => ({ unmounted: true })),
mount: vi.fn(async (_p) => ({ hostPath: "/mnt", mounted: true })),
};
const sessionStore = {
load: vi.fn(async (_k) => ({ secafsRollback: { enabled: true } })),
patch: vi.fn(async (_k, _p) => undefined),
};
const truncate = vi.fn(async (_file, _id) => ({
fileExisted: true,
truncated: true,
bytesAfter: 100,
}));
const events = vi.fn();
return { rpc, sessionStore, truncate, events };
}
describe("handleRestore", () => {
it("runs all phases in order and emits completed event", async () => {
const { rpc, sessionStore, truncate, events } = makeFakes();
const handle = createHandleRestore({
rpc,
sessionStore,
truncate,
events,
sessionFileFor: () => "/p/s.jsonl",
trajectoryFor: () => "/p/s.trajectory.jsonl",
workspaceFor: () => "/mnt",
extractConvId: (k) => k.split(":")[1] ?? k,
});
const r = await handle({ sessionKey: "main:abc", snapId: 5 });
expect(r.restored).toBe(true);
expect(r.restoredMessageId).toBe("msg-5");
expect(rpc.unmount).toHaveBeenCalled();
expect(rpc.snapshotRestore).toHaveBeenCalled();
expect(truncate).toHaveBeenCalledTimes(2);
expect(truncate.mock.calls[1][2]).toEqual(
expect.objectContaining({ missingOk: true, idNotFoundOk: true }),
);
expect(rpc.mount).toHaveBeenCalled();
expect(events).toHaveBeenCalledWith(
expect.objectContaining({ type: "secafs.rollback.completed" }),
);
});
it("rejects when another restore is already in progress", async () => {
const fakes = makeFakes();
fakes.sessionStore.load = vi.fn(async () => ({
secafsRollback: {
enabled: true,
inProgress: {
targetSnapId: 9,
targetMessageId: "x",
fsRestored: false,
jsonlTruncated: false,
startedAt: 0,
},
},
}));
const handle = createHandleRestore({
...fakes,
sessionFileFor: () => "x",
trajectoryFor: () => "x",
workspaceFor: () => "x",
extractConvId: () => "v",
});
await expect(handle({ sessionKey: "k", snapId: 5 })).rejects.toThrow(/in progress/i);
});
it("throws when snapId is unknown", async () => {
const fakes = makeFakes();
fakes.rpc.snapshotList = vi.fn(async () => ({ snapshots: [] }));
const handle = createHandleRestore({
...fakes,
sessionFileFor: () => "x",
trajectoryFor: () => "x",
workspaceFor: () => "x",
extractConvId: () => "v",
});
await expect(handle({ sessionKey: "k", snapId: 99 })).rejects.toThrow(/not found/);
});
});