From 603588cc4fff3a07b2c998921042b64fb74cd584 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 26 Apr 2026 02:13:47 +0900 Subject: [PATCH] =?UTF-8?q?chore(backup):=20polish=20=E2=80=94=20boundary?= =?UTF-8?q?=20test,=20roundtrip=20lock-in,=20precompute=20today?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code reviewer minor nitpicks: - Add test for inkling-2026-02-30.sqlite (locks roundtrip-validation contract) - Add test for weekly window inclusive at oldest boundary - Precompute today=startOfDayUtc(now) once outside the loop, pass to helpers No behavior change; tests added cover existing semantics. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/services/backupRotation.ts | 20 +++++++++----------- tests/unit/backupRotation.test.ts | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/services/backupRotation.ts b/src/main/services/backupRotation.ts index 49d485d..e6631b0 100644 --- a/src/main/services/backupRotation.ts +++ b/src/main/services/backupRotation.ts @@ -24,17 +24,15 @@ function startOfDayUtc(d: Date): Date { return x; } -function isWithinDailyWindow(fileDate: Date, now: Date): boolean { - const today = startOfDayUtc(now); +function isWithinDailyWindow(fileDate: Date, today: Date): boolean { const oldest = new Date(today.getTime() - (DAILY_WINDOW_DAYS - 1) * ONE_DAY_MS); return fileDate >= oldest && fileDate <= today; } -function isWithinWeeklyWindow(fileDate: Date, now: Date): boolean { +function isWithinWeeklyWindow(fileDate: Date, today: Date): boolean { // UTC-based Monday detection. UTCDay: 0=Sun, 1=Mon..6=Sat if (fileDate.getUTCDay() !== 1) return false; - const today = startOfDayUtc(now); - // Weekly window: anchor on the most recent Monday on/before `now`, then reach back + // Weekly window: anchor on the most recent Monday on/before `today`, then reach back // WEEKLY_WINDOW_PRIOR_MONDAYS * 7 days from that anchor. Effective semantic: // up to 5 distinct Mondays kept (the anchor Monday + 4 prior). The plan's commit // header says "4 weekly Mondays" but the plan's test case at @@ -47,9 +45,8 @@ function isWithinWeeklyWindow(fileDate: Date, now: Date): boolean { return fileDate >= oldest && fileDate <= today; } -function isWithinMonthlyWindow(fileDate: Date, now: Date): boolean { +function isWithinMonthlyWindow(fileDate: Date, today: Date): boolean { if (fileDate.getUTCDate() !== 1) return false; - const today = startOfDayUtc(now); // months ago: difference in calendar months const monthsAgo = (today.getUTCFullYear() - fileDate.getUTCFullYear()) * 12 + @@ -60,18 +57,19 @@ function isWithinMonthlyWindow(fileDate: Date, now: Date): boolean { export function applyGfsRetention(filenames: string[], now: Date): RetentionResult { const keep: string[] = []; const remove: string[] = []; + const today = startOfDayUtc(now); for (const name of filenames) { const iso = parseBackupFilename(name); if (iso === null) continue; // unrecognized — ignore (no-op) const fileDate = new Date(iso + 'T00:00:00Z'); - if (fileDate > startOfDayUtc(now)) { + if (fileDate > today) { keep.push(name); // future-dated — clock skew safety continue; } const survives = - isWithinDailyWindow(fileDate, now) || - isWithinWeeklyWindow(fileDate, now) || - isWithinMonthlyWindow(fileDate, now); + isWithinDailyWindow(fileDate, today) || + isWithinWeeklyWindow(fileDate, today) || + isWithinMonthlyWindow(fileDate, today); if (survives) keep.push(name); else remove.push(name); } diff --git a/tests/unit/backupRotation.test.ts b/tests/unit/backupRotation.test.ts index d2fd98f..d981b6d 100644 --- a/tests/unit/backupRotation.test.ts +++ b/tests/unit/backupRotation.test.ts @@ -11,6 +11,12 @@ describe('parseBackupFilename', () => { expect(parseBackupFilename('inkling-2026-13-99.sqlite')).toBeNull(); expect(parseBackupFilename('.last-snapshot')).toBeNull(); }); + + it('returns null for date that JS would silently coerce (roundtrip lock-in)', () => { + // Without the toISOString roundtrip check, JS coerces 2026-02-30 to 2026-03-02. + // This test locks in the roundtrip-validation contract. + expect(parseBackupFilename('inkling-2026-02-30.sqlite')).toBeNull(); + }); }); describe('applyGfsRetention', () => { @@ -52,6 +58,14 @@ describe('applyGfsRetention', () => { expect(r.remove).toContain('inkling-2026-03-09.sqlite'); }); + it('weekly window is inclusive at oldest boundary', () => { + // 2026-03-23 is exactly 4*7 days before anchor Monday 2026-04-20. + // Locks in the boundary semantic explicitly. + const r = applyGfsRetention(names('2026-03-23', '2026-03-16'), NOW); + expect(r.keep).toContain('inkling-2026-03-23.sqlite'); + expect(r.remove).toContain('inkling-2026-03-16.sqlite'); + }); + it('keeps month-firsts within last 6 months', () => { // Last 6 month-firsts from 2026-04-26: 2026-04-01, 2026-03-01, 2026-02-01, // 2026-01-01, 2025-12-01, 2025-11-01