Skip to content

Commit cbf9692

Browse files
committed
test(backup): model tar archive streams
1 parent 59fbe5a commit cbf9692

3 files changed

Lines changed: 49 additions & 36 deletions

File tree

src/commands/backup.atomic.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
66
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
77
import {
88
backupVerifyCommandMock,
9+
createMockTarStream,
910
createBackupTestRuntime,
1011
mockStateOnlyBackupPlan,
1112
resetBackupTempHome,
@@ -70,7 +71,7 @@ describe("backupCreateCommand atomic archive write", () => {
7071
archivePrefix: "openclaw-backup-failure-",
7172
});
7273
try {
73-
tarCreateMock.mockRejectedValueOnce(new Error("disk full"));
74+
tarCreateMock.mockReturnValueOnce(createMockTarStream({ error: new Error("disk full") }));
7475

7576
await expect(
7677
backupCreateCommand(runtime, {
@@ -93,9 +94,7 @@ describe("backupCreateCommand atomic archive write", () => {
9394
const realLink = fs.link.bind(fs);
9495
const linkSpy = vi.spyOn(fs, "link");
9596
try {
96-
tarCreateMock.mockImplementationOnce(async ({ file }: { file: string }) => {
97-
await fs.writeFile(file, "archive-bytes", "utf8");
98-
});
97+
tarCreateMock.mockReturnValueOnce(createMockTarStream());
9998
linkSpy.mockImplementationOnce(async (existingPath, newPath) => {
10099
await fs.writeFile(newPath, "concurrent-archive", "utf8");
101100
return await realLink(existingPath, newPath);
@@ -120,9 +119,7 @@ describe("backupCreateCommand atomic archive write", () => {
120119
});
121120
const linkSpy = vi.spyOn(fs, "link");
122121
try {
123-
tarCreateMock.mockImplementationOnce(async ({ file }: { file: string }) => {
124-
await fs.writeFile(file, "archive-bytes", "utf8");
125-
});
122+
tarCreateMock.mockReturnValueOnce(createMockTarStream());
126123
linkSpy.mockRejectedValueOnce(
127124
Object.assign(new Error("hard links not supported"), { code: "EOPNOTSUPP" }),
128125
);

src/commands/backup.test-support.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Backup test support provides temp config/state fixtures and mocked backup runtime helpers.
22
import fs from "node:fs/promises";
33
import path from "node:path";
4+
import { Readable } from "node:stream";
45
import { vi } from "vitest";
56
import type { RuntimeEnv } from "../runtime.js";
67
import { deleteTestEnvValue } from "../test-utils/env.js";
@@ -14,6 +15,24 @@ const backupTestMocks = vi.hoisted(() => ({
1415

1516
export const { backupVerifyCommandMock, tarCreateMock } = backupTestMocks;
1617

18+
export function createMockTarStream(
19+
params: {
20+
beforeRead?: () => Promise<void> | void;
21+
contents?: string;
22+
error?: Error;
23+
} = {},
24+
): Readable {
25+
return Readable.from(
26+
(async function* () {
27+
await params.beforeRead?.();
28+
if (params.error) {
29+
throw params.error;
30+
}
31+
yield params.contents ?? "archive-bytes";
32+
})(),
33+
);
34+
}
35+
1736
vi.mock("tar", () => ({
1837
c: backupTestMocks.tarCreateMock,
1938
}));

src/commands/backup.test.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "./backup-shared.js";
1818
import {
1919
backupVerifyCommandMock,
20+
createMockTarStream,
2021
createBackupTestRuntime,
2122
mockStateOnlyBackupPlan,
2223
resetBackupTempHome,
@@ -78,9 +79,7 @@ describe("backup commands", () => {
7879
beforeEach(async () => {
7980
await resetBackupTempHome(tempHome);
8081
tarCreateMock.mockReset();
81-
tarCreateMock.mockImplementation(async ({ file }: { file: string }) => {
82-
await fs.writeFile(file, "archive-bytes", "utf8");
83-
});
82+
tarCreateMock.mockImplementation(() => createMockTarStream());
8483
backupVerifyCommandMock.mockReset();
8584
backupVerifyCommandMock.mockResolvedValue({
8685
ok: true,
@@ -268,17 +267,16 @@ describe("backup commands", () => {
268267
}),
269268
);
270269
tarCreateMock.mockImplementationOnce(
271-
async (
272-
options: { file: string; onWriteEntry?: (entry: { path: string }) => void },
273-
entryPaths: string[],
274-
) => {
275-
capturedManifest = JSON.parse(
276-
await fs.readFile(entryPaths[0], "utf8"),
277-
) as CapturedBackupManifest;
278-
capturedEntryPaths = entryPaths;
279-
capturedOnWriteEntry = options.onWriteEntry ?? null;
280-
await fs.writeFile(options.file, "archive-bytes", "utf8");
281-
},
270+
(options: { onWriteEntry?: (entry: { path: string }) => void }, entryPaths: string[]) =>
271+
createMockTarStream({
272+
beforeRead: async () => {
273+
capturedManifest = JSON.parse(
274+
await fs.readFile(entryPaths[0], "utf8"),
275+
) as CapturedBackupManifest;
276+
capturedEntryPaths = entryPaths;
277+
capturedOnWriteEntry = options.onWriteEntry ?? null;
278+
},
279+
}),
282280
);
283281
const result = await backupCreateCommand(runtime, {
284282
output: backupDir,
@@ -367,21 +365,20 @@ describe("backup commands", () => {
367365
const runtime = createBackupTestRuntime();
368366
await mockStateOnlyBackupPlan(stateDir);
369367
tarCreateMock.mockImplementationOnce(
370-
async (
371-
options: { file: string; filter?: (entryPath: string) => boolean },
372-
entryPaths: string[],
373-
) => {
374-
const manifestPath = entryPaths[0];
375-
const stateRoot = entryPaths[1];
376-
if (!manifestPath || !stateRoot) {
377-
throw new Error("backup test expected manifest and state entries");
378-
}
379-
expect(options.filter?.(manifestPath)).toBe(true);
380-
expect(
381-
options.filter?.(path.join(stateRoot, "agents", "main", "sessions", "s.jsonl")),
382-
).toBe(false);
383-
await fs.writeFile(options.file, "archive-bytes", "utf8");
384-
},
368+
(options: { filter?: (entryPath: string) => boolean }, entryPaths: string[]) =>
369+
createMockTarStream({
370+
beforeRead: () => {
371+
const manifestPath = entryPaths[0];
372+
const stateRoot = entryPaths[1];
373+
if (!manifestPath || !stateRoot) {
374+
throw new Error("backup test expected manifest and state entries");
375+
}
376+
expect(options.filter?.(manifestPath)).toBe(true);
377+
expect(
378+
options.filter?.(path.join(stateRoot, "agents", "main", "sessions", "s.jsonl")),
379+
).toBe(false);
380+
},
381+
}),
385382
);
386383

387384
const result = await backupCreateCommand(runtime, {

0 commit comments

Comments
 (0)