Test: lib.backup() takes no new original once onProgress has thrown or its lock is lost
check / check (push) Waiting to run

The onProgress test now runs eight photos with a pool of five and holds
only the first five downloads, so a backup that went on after the throw
would download and store the other three. The end-to-end test of a
removed backup.lock expects the backup to stop after its three originals,
before it fetches ML data.

Model: opus-5-5
This commit is contained in:
2026-10-14 06:37:26 +00:00
parent d95c49e548
commit 767545f4ab
2 changed files with 36 additions and 23 deletions
+32 -19
View File
@@ -397,19 +397,33 @@ describe("lib.backup()", () => {
}); });
// A throw from `onProgress` fails the backup: `lib.backup()` rejects with // A throw from `onProgress` fails the backup: `lib.backup()` rejects with
// it. It rejects only once it has finished with every original it was // it. From then on the backup starts no further original. It finishes the
// working on, so nothing is written into the backup after it has ended // ones it is working on, and rejects only once they are in place, so
// and removed its lock. Here, with a content pool of eight, all eight // nothing is written into the backup after it has ended and removed its
// downloads wait at once. Photo 103's is let go first and answered 404, // lock. Here the first five downloads wait. Photo 103's is let go first
// and the line reporting that failure throws while the other seven wait. // and answered 404, and the line reporting that failure throws while the
it("when onProgress throws, rejects only once the originals it was working on are in place", async () => { // other four wait. Only those five downloads are held: a backup that went
// on would store the other three photos.
it("when onProgress throws, starts no further original, and rejects once the originals it was working on are in place", async () => {
const dir = mkdtempSync(join(root, "backup-")); const dir = mkdtempSync(join(root, "backup-"));
const holding = holdingServer(103); const downloads: number[] = [];
const lib = await openLibrary( const held: { id: number; release: () => void }[] = [];
dir, const fetch: typeof globalThis.fetch = async (input, init) => {
holding.fetch, const id = downloadOf(input);
new RequestPools({ contentConcurrency: 8 }), if (id !== undefined) {
); downloads.push(id);
if (downloads.length <= 5) {
await new Promise<void>((release) =>
held.push({ id, release }),
);
}
if (id === 103) {
return new Response("not found", { status: 404 });
}
}
return server(input, init);
};
const lib = await openLibrary(dir, fetch);
const lines: string[] = []; const lines: string[] = [];
try { try {
const backup = lib.backup({ const backup = lib.backup({
@@ -432,12 +446,12 @@ describe("lib.backup()", () => {
return err; return err;
}); });
await vi.waitFor(() => expect(holding.held).toHaveLength(8), { await vi.waitFor(() => expect(held).toHaveLength(5), {
interval: 5, interval: 5,
timeout: 5000, timeout: 5000,
}); });
const at103 = holding.held.findIndex((h) => h.id === 103); const at103 = held.findIndex((h) => h.id === 103);
holding.held.splice(at103, 1)[0]!.release(); held.splice(at103, 1)[0]!.release();
await vi.waitFor( await vi.waitFor(
() => () =>
expect( expect(
@@ -445,14 +459,13 @@ describe("lib.backup()", () => {
).toBe(true), ).toBe(true),
{ interval: 5, timeout: 5000 }, { interval: 5, timeout: 5000 },
); );
for (const download of holding.held) download.release(); for (const download of held) download.release();
expect(await rejection).toEqual( expect(await rejection).toEqual(
new Error("the progress output is closed"), new Error("the progress output is closed"),
); );
expect(storedWhenRejected).toEqual( expect(downloads.sort()).toEqual([101, 102, 103, 104, 105]);
PHOTO_IDS.filter((id) => id !== 103), expect(storedWhenRejected).toEqual([101, 102, 104, 105]);
);
expect(lockedWhenRejected).toBe(false); expect(lockedWhenRejected).toBe(false);
} finally { } finally {
await lib.close(); await lib.close();
+4 -4
View File
@@ -705,9 +705,10 @@ describe("a backup whose lock is lost", () => {
}; };
// The backup downloads up to five originals at once, so all three are // The backup downloads up to five originals at once, so all three are
// under way when the lock is lost, and beach.jpg is stored. `quak backup` // under way when the lock is lost, and beach.jpg is stored. The backup
// prints why as one line, with no stack trace, and exits 1. // then stops before it fetches ML data. `quak backup` prints why as one
it("stops quak backup before its next file when backup.lock is removed, and exits 1", async () => { // line, with no stack trace, and exits 1.
it("stops quak backup before it fetches ML data when backup.lock is removed, and exits 1", async () => {
const result = backup(dir, {}, fetch); const result = backup(dir, {}, fetch);
await heldRequested; await heldRequested;
rmSync(lockPath, { recursive: true }); rmSync(lockPath, { recursive: true });
@@ -722,7 +723,6 @@ describe("a backup whose lock is lost", () => {
"Fetching original beach.jpg (100)...\n" + "Fetching original beach.jpg (100)...\n" +
"Fetching original IMG_0002.HEIC (300)...\n" + "Fetching original IMG_0002.HEIC (300)...\n" +
"Fetching original toast.mp4 (200)...\n" + "Fetching original toast.mp4 (200)...\n" +
"Fetching ML data...\n" +
`quak: lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'\n`, `quak: lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'\n`,
); );
expect(downloads.sort()).toEqual([100, 200, 300]); expect(downloads.sort()).toEqual([100, 200, 300]);