diff --git a/shared/schedule-policy.ts b/shared/schedule-policy.ts index deac5ec3..c1397b4c 100644 --- a/shared/schedule-policy.ts +++ b/shared/schedule-policy.ts @@ -62,7 +62,9 @@ export const SCHEDULE_POLICY = [ { _tag: 'observed', taskName: 'detect-star-surges', cron: '30 4 * * *', maxSilenceSeconds: 36 * 60 * 60, maxRuntimeSeconds: 30 * 60 }, { _tag: 'observed', taskName: 'drain-skill-dirty', cron: '*/5 * * * *', maxSilenceSeconds: 20 * 60, maxRuntimeSeconds: 4 * 60 }, { _tag: 'observed', taskName: 'embedding-parity-audit', cron: '0 21 * * *', maxSilenceSeconds: 36 * 60 * 60, maxRuntimeSeconds: 30 * 60 }, - { _tag: 'observed', taskName: 'purge-personal-data', cron: '30 4 * * *', maxSilenceSeconds: 36 * 60 * 60, maxRuntimeSeconds: 10 * 60, activeFromSeconds: 1789603200 }, + // First cron fire, not the authoring date: a grace anchored to 2026-09-17 + // expired before the deployed trigger ever fired and read as missing_run. + { _tag: 'observed', taskName: 'purge-personal-data', cron: '30 4 * * *', maxSilenceSeconds: 36 * 60 * 60, maxRuntimeSeconds: 10 * 60, activeFromSeconds: 1790051400 }, { _tag: 'observed', taskName: 'recompute-skill-scores', cron: '0 3 * * *', maxSilenceSeconds: 36 * 60 * 60, maxRuntimeSeconds: 60 * 60 }, { _tag: 'observed', taskName: 'refresh-x-engagement', cron: '10 * * * *', maxSilenceSeconds: 3 * 60 * 60, maxRuntimeSeconds: 30 * 60 }, { _tag: 'observed', taskName: 'reconcile-rendered', cron: '20 */6 * * *', maxSilenceSeconds: 15 * 60 * 60, maxRuntimeSeconds: 30 * 60 }, diff --git a/test/unit/scheduled-run.test.ts b/test/unit/scheduled-run.test.ts index 519dd301..739ef5b2 100644 --- a/test/unit/scheduled-run.test.ts +++ b/test/unit/scheduled-run.test.ts @@ -134,6 +134,23 @@ describe('scheduled run lifecycle', () => { )).toMatchObject({ _tag: 'missing_run', alertable: true }) }) + // purge-personal-data was authored 2026-09-17 but its first cron fire is + // 2026-09-22T04:30Z. An authoring-date anchor expired its 36-hour grace four + // days before that fire, so the nightly report raised a false missing_run. + it('keeps purge-personal-data healthy until its first cron fire passes', () => { + const policy = observedSchedulePolicy('purge-personal-data') + expect(policy.cron).toBe('30 4 * * *') + const firstFire = 1_790_051_400 // 2026-09-22T04:30Z + + expect(evaluateScheduleHealth(policy, { latest: null, latestTerminal: null }, firstFire - 1)) + .toMatchObject({ _tag: 'healthy', alertable: false }) + expect(evaluateScheduleHealth( + policy, + { latest: null, latestTerminal: null }, + firstFire + policy.maxSilenceSeconds + 1, + )).toMatchObject({ _tag: 'missing_run', alertable: true }) + }) + it('observes the digest on its monthly schedule', () => { const policy = observedSchedulePolicy('send-digests')