Files
cobble/src/functions/backend/backups/data-backup-run.test.ts
T
tbenandClaude Opus 5.5 b9a49ee09c Fix data archives and restores failing on their own lock
startDataBackupRun already holds the backup run lock, but the archive
(createDataBackup) and restoreDataBackup took it again, so every run
failed a few seconds in with "Another backup or restore is already
running". The writer and the restore now take no lock of their own.

The run's notice (archiving, restoring, or why it failed) moves to its
own line under the actions row so the buttons don't shift, and archiving
reads in gray rather than the warning colour.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:24:55 +01:00

100 lines
3.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import AppError from "@/src/utils/app-error";
import { BootId, grabDataBackupRun, startDataBackupRun } from "./data-backup-run";
import { isBackupRunning } from "./with-backup-run-lock";
import restoreDataBackup from "./restore-data-backup";
function deferred() {
let resolve!: (name: string) => void;
let reject!: (error: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
describe("startDataBackupRun", () => {
test("answers at once, then records the finished archive", async () => {
const task = deferred();
const run = startDataBackupRun({
kind: "archive",
task: () => task.promise,
});
expect(run.status).toBe("running");
expect(run.kind).toBe("archive");
expect(run.boot_id).toBe(BootId);
expect(isBackupRunning()).toBe(true);
task.resolve("cobble-data-1.tar.gz");
await settle();
const done = grabDataBackupRun();
expect(done.status).toBe("done");
expect(done.name).toBe("cobble-data-1.tar.gz");
expect(isBackupRunning()).toBe(false);
});
test("refuses a second run while one is going, before it starts", async () => {
const task = deferred();
let second_started = false;
startDataBackupRun({ kind: "archive", task: () => task.promise });
expect(() =>
startDataBackupRun({
kind: "restore",
task: async () => {
second_started = true;
return "x";
},
}),
).toThrow(AppError);
expect(second_started).toBe(false);
task.resolve("cobble-data-2.tar.gz");
await settle();
});
test("records a failure and frees the lock for the next run", async () => {
const task = deferred();
startDataBackupRun({ kind: "restore", task: () => task.promise });
task.reject(new Error("tar: broken"));
await settle();
const failed = grabDataBackupRun();
expect(failed.status).toBe("failed");
expect(failed.kind).toBe("restore");
expect(failed.error).toBe("tar: broken");
expect(isBackupRunning()).toBe(false);
const next = startDataBackupRun({
kind: "archive",
task: async () => "cobble-data-3.tar.gz",
});
expect(next.status).toBe("running");
await settle();
expect(grabDataBackupRun().status).toBe("done");
});
test("a restore runs under the run's lock instead of taking it again", async () => {
startDataBackupRun({
kind: "restore",
task: async () =>
(await restoreDataBackup({ backup_name: "missing.tar.gz" }))
.restored_name,
});
await settle();
const failed = grabDataBackupRun();
expect(failed.status).toBe("failed");
expect(failed.error).toBe("Archive not found");
expect(isBackupRunning()).toBe(false);
});
});