From 99e3d4a06941c28fa7a986ef9353ec508d29e547 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Apr 2026 09:42:45 +0100 Subject: [PATCH] test(commands): share atomic backup setup --- src/commands/backup.atomic.test.ts | 113 +++++++++++------------------ 1 file changed, 44 insertions(+), 69 deletions(-) diff --git a/src/commands/backup.atomic.test.ts b/src/commands/backup.atomic.test.ts index e9a966559cc..692f3ae7c3b 100644 --- a/src/commands/backup.atomic.test.ts +++ b/src/commands/backup.atomic.test.ts @@ -45,33 +45,48 @@ describe("backupCreateCommand atomic archive write", () => { await tempHome.restore(); }); - it("does not leave a partial final archive behind when tar creation fails", async () => { + async function prepareAtomicBackupScenario(params: { + archivePrefix: string; + outputName?: string; + }) { const stateDir = path.join(tempHome.home, ".openclaw"); - const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-failure-")); + const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), params.archivePrefix)); + await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8"); + await fs.writeFile(path.join(stateDir, "state.txt"), "state\n", "utf8"); + + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + const outputPath = path.join(archiveDir, params.outputName ?? "backup.tar.gz"); + + vi.spyOn(backupShared, "resolveBackupPlanFromDisk").mockResolvedValue( + await resolveBackupPlanFromPaths({ + stateDir, + configPath: path.join(stateDir, "openclaw.json"), + oauthDir: path.join(stateDir, "credentials"), + includeWorkspace: false, + configInsideState: true, + oauthInsideState: true, + nowMs: 123, + }), + ); + + return { + archiveDir, + outputPath, + runtime, + }; + } + + it("does not leave a partial final archive behind when tar creation fails", async () => { + const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario({ + archivePrefix: "openclaw-backup-failure-", + }); try { - await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8"); - await fs.writeFile(path.join(stateDir, "state.txt"), "state\n", "utf8"); - tarCreateMock.mockRejectedValueOnce(new Error("disk full")); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - const outputPath = path.join(archiveDir, "backup.tar.gz"); - vi.spyOn(backupShared, "resolveBackupPlanFromDisk").mockResolvedValue( - await resolveBackupPlanFromPaths({ - stateDir, - configPath: path.join(stateDir, "openclaw.json"), - oauthDir: path.join(stateDir, "credentials"), - includeWorkspace: false, - configInsideState: true, - oauthInsideState: true, - nowMs: 123, - }), - ); - await expect( backupCreateCommand(runtime, { output: outputPath, @@ -87,14 +102,12 @@ describe("backupCreateCommand atomic archive write", () => { }); it("does not overwrite an archive created after readiness checks complete", async () => { - const stateDir = path.join(tempHome.home, ".openclaw"); - const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-race-")); + const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario({ + archivePrefix: "openclaw-backup-race-", + }); const realLink = fs.link.bind(fs); const linkSpy = vi.spyOn(fs, "link"); try { - await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8"); - await fs.writeFile(path.join(stateDir, "state.txt"), "state\n", "utf8"); - tarCreateMock.mockImplementationOnce(async ({ file }: { file: string }) => { await fs.writeFile(file, "archive-bytes", "utf8"); }); @@ -103,24 +116,6 @@ describe("backupCreateCommand atomic archive write", () => { return await realLink(existingPath, newPath); }); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - const outputPath = path.join(archiveDir, "backup.tar.gz"); - vi.spyOn(backupShared, "resolveBackupPlanFromDisk").mockResolvedValue( - await resolveBackupPlanFromPaths({ - stateDir, - configPath: path.join(stateDir, "openclaw.json"), - oauthDir: path.join(stateDir, "credentials"), - includeWorkspace: false, - configInsideState: true, - oauthInsideState: true, - nowMs: 123, - }), - ); - await expect( backupCreateCommand(runtime, { output: outputPath, @@ -135,13 +130,11 @@ describe("backupCreateCommand atomic archive write", () => { }); it("falls back to exclusive copy when hard-link publication is unsupported", async () => { - const stateDir = path.join(tempHome.home, ".openclaw"); - const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-copy-fallback-")); + const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario({ + archivePrefix: "openclaw-backup-copy-fallback-", + }); const linkSpy = vi.spyOn(fs, "link"); try { - await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8"); - await fs.writeFile(path.join(stateDir, "state.txt"), "state\n", "utf8"); - tarCreateMock.mockImplementationOnce(async ({ file }: { file: string }) => { await fs.writeFile(file, "archive-bytes", "utf8"); }); @@ -149,24 +142,6 @@ describe("backupCreateCommand atomic archive write", () => { Object.assign(new Error("hard links not supported"), { code: "EOPNOTSUPP" }), ); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - const outputPath = path.join(archiveDir, "backup.tar.gz"); - vi.spyOn(backupShared, "resolveBackupPlanFromDisk").mockResolvedValue( - await resolveBackupPlanFromPaths({ - stateDir, - configPath: path.join(stateDir, "openclaw.json"), - oauthDir: path.join(stateDir, "credentials"), - includeWorkspace: false, - configInsideState: true, - oauthInsideState: true, - nowMs: 123, - }), - ); - const result = await backupCreateCommand(runtime, { output: outputPath, });