test(commands): share atomic backup setup

This commit is contained in:
Vincent Koc
2026-04-12 09:42:45 +01:00
parent b78713a363
commit 99e3d4a069

View File

@@ -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,
});