From 2645492fde6e5aacc716236b86e8553bb2a4dcf7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 15 May 2026 07:12:50 +0100 Subject: [PATCH] test: cover sync clobber snapshot collisions --- src/config/io.clobber-snapshot.test.ts | 55 ++++++++++++++++++++++++++ src/config/io.clobber-snapshot.ts | 48 ++++++++++++++-------- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/config/io.clobber-snapshot.test.ts b/src/config/io.clobber-snapshot.test.ts index a685c74f3b7..f1424208d6a 100644 --- a/src/config/io.clobber-snapshot.test.ts +++ b/src/config/io.clobber-snapshot.test.ts @@ -75,6 +75,23 @@ describe("config clobber snapshots", () => { } } + function touchClobberFilesByContentOrderSync(configPath: string): void { + const dir = path.dirname(configPath); + const prefix = `${path.basename(configPath)}.clobbered.`; + for (const entry of fs.readdirSync(dir)) { + if (!entry.startsWith(prefix)) { + continue; + } + const targetPath = path.join(dir, entry); + const match = /^polluted-(\d+)\n$/.exec(fs.readFileSync(targetPath, "utf-8")); + if (!match) { + continue; + } + const touchedAt = new Date(`2026-05-03T00:00:${match[1].padStart(2, "0")}.000Z`); + fs.utimesSync(targetPath, touchedAt, touchedAt); + } + } + it("keeps concurrent async snapshots under the per-path cap by rotating oldest files", async () => { await withCase(async (configPath) => { const warn = vi.fn(); @@ -227,4 +244,42 @@ describe("config clobber snapshots", () => { expect(capWarnings).toHaveLength(1); }); }); + + it("keeps rotating same-timestamp sync snapshots after the base artifact is reused", async () => { + await withCase(async (configPath) => { + const warn = vi.fn(); + const observedAt = "2026-05-03T00:00:00.000Z"; + + for (let index = 0; index < CONFIG_CLOBBER_SNAPSHOT_LIMIT; index++) { + persistBoundedClobberedConfigSnapshotSync({ + deps: { fs, logger: { warn } }, + configPath, + raw: `polluted-${index}\n`, + observedAt, + }); + } + touchClobberFilesByContentOrderSync(configPath); + + for ( + let index = CONFIG_CLOBBER_SNAPSHOT_LIMIT; + index < CONFIG_CLOBBER_SNAPSHOT_LIMIT + 3; + index++ + ) { + persistBoundedClobberedConfigSnapshotSync({ + deps: { fs, logger: { warn } }, + configPath, + raw: `polluted-${index}\n`, + observedAt, + }); + } + + const clobberFiles = await listClobberFiles(configPath); + expect(clobberFiles).toHaveLength(CONFIG_CLOBBER_SNAPSHOT_LIMIT); + const contents = await readClobberFileContents(configPath); + expect(contents).not.toContain("polluted-0\n"); + expect(contents).not.toContain("polluted-1\n"); + expect(contents).not.toContain("polluted-2\n"); + expect(contents).toContain(`polluted-${CONFIG_CLOBBER_SNAPSHOT_LIMIT + 2}\n`); + }); + }); }); diff --git a/src/config/io.clobber-snapshot.ts b/src/config/io.clobber-snapshot.ts index 567710b1e16..8078207e52d 100644 --- a/src/config/io.clobber-snapshot.ts +++ b/src/config/io.clobber-snapshot.ts @@ -137,6 +137,20 @@ function compareClobberedSiblings( ); } +function createClobberedSiblingSnapshot(params: { + dir: string; + entry: string; + prefix: string; + mtimeMs: number; +}): ClobberedSiblingSnapshot { + return { + name: params.entry, + path: path.join(params.dir, params.entry), + timestampKey: params.entry.slice(params.prefix.length).replace(/-\d{2}$/, ""), + mtimeMs: params.mtimeMs, + }; +} + async function listClobberedSiblings( deps: ConfigClobberSnapshotDeps, dir: string, @@ -149,14 +163,15 @@ async function listClobberedSiblings( if (!entry.startsWith(prefix)) { continue; } - const snapshotPath = path.join(dir, entry); - const stat = await deps.fs.promises.stat(snapshotPath).catch(() => null); - snapshots.push({ - name: entry, - path: snapshotPath, - timestampKey: entry.slice(prefix.length).replace(/-\d{2}$/, ""), - mtimeMs: stat?.mtimeMs ?? 0, - }); + const stat = await deps.fs.promises.stat(path.join(dir, entry)).catch(() => null); + snapshots.push( + createClobberedSiblingSnapshot({ + dir, + entry, + prefix, + mtimeMs: stat?.mtimeMs ?? 0, + }), + ); } return snapshots.toSorted(compareClobberedSiblings); } catch { @@ -175,14 +190,15 @@ function listClobberedSiblingsSync( if (!entry.startsWith(prefix)) { continue; } - const snapshotPath = path.join(dir, entry); - const stat = deps.fs.statSync(snapshotPath, { throwIfNoEntry: false }); - snapshots.push({ - name: entry, - path: snapshotPath, - timestampKey: entry.slice(prefix.length).replace(/-\d{2}$/, ""), - mtimeMs: stat?.mtimeMs ?? 0, - }); + const stat = deps.fs.statSync(path.join(dir, entry), { throwIfNoEntry: false }); + snapshots.push( + createClobberedSiblingSnapshot({ + dir, + entry, + prefix, + mtimeMs: stat?.mtimeMs ?? 0, + }), + ); } return snapshots.toSorted(compareClobberedSiblings); } catch {