From 4fa1f74edaaed1e48a9a7addcdedcd5f91d5fe52 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 27 Jul 2026 10:20:32 -0400 Subject: [PATCH] test: cover the pause-projection guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I previously shipped this guard as defence-in-depth and said it could not be tested. That was wrong: the INTERLEAVING needs two concurrent writers, but the STATE it produces does not. A row still active while its own occurrence's card is already settled is constructible directly, and that is precisely the input the guard exists for. Both directions are covered — the settled card is not walked back to a pause, and an unsettled one still projects normally — and the first fails without the guard. --- .../src/methods/schedule.methods.spec.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/data-schemas/src/methods/schedule.methods.spec.ts b/packages/data-schemas/src/methods/schedule.methods.spec.ts index fb3e01030e..17ad17fe48 100644 --- a/packages/data-schemas/src/methods/schedule.methods.spec.ts +++ b/packages/data-schemas/src/methods/schedule.methods.spec.ts @@ -1102,6 +1102,63 @@ describe('card projection fences (regressions in the ordered projection)', () => * projectLastRun keys off — so the NEXT projection saw an absent marker and let an * older occurrence's result overwrite the newer skip. */ + /** + * The pause wins its row transition and THEN projects, so a resume that terminalizes + * in between leaves exactly this state: row still active, card already settled for the + * SAME occurrence. Occurrence ordering alone permits the pause (equal `scheduledFor`), + * which would pin the card to "Needs approval" for a run that had already finished — + * and nothing corrects it until the next occurrence runs. + * + * The interleaving needs two concurrent writers, but the state it produces does not: + * constructing it directly tests the guard on exactly the input it exists for. + */ + it('refuses to walk an occurrence card back from a settled status to a pause', async () => { + const schedule = await methods.createSchedule(scheduleData()); + const scheduledFor = new Date('2026-07-26T10:00:00.000Z'); + await methods.insertScheduleRun(runData(schedule, { scheduledFor })); + // The resumed generation already settled this occurrence's card. + await mongoose.models.Schedule.updateOne( + { id: schedule.id }, + { + $set: { + lastRun: { + status: 'success', + firedAt: new Date(), + scheduledFor, + conversationId: 'convo-1', + }, + }, + }, + ); + + // The in-flight pause now lands for that same occurrence. + await methods.recordRunOutcome({ + scheduleId: schedule.id, + scheduledFor, + status: 'requires_action', + conversationId: 'convo-1', + autoDisableAfterFailures: 3, + }); + + expect((await getSchedule(schedule.id)).lastRun?.status).toBe('success'); + }); + + it('still projects a pause when the occurrence card is not yet settled', async () => { + const schedule = await methods.createSchedule(scheduleData()); + const scheduledFor = new Date('2026-07-26T10:00:00.000Z'); + await methods.insertScheduleRun(runData(schedule, { scheduledFor })); + + await methods.recordRunOutcome({ + scheduleId: schedule.id, + scheduledFor, + status: 'requires_action', + conversationId: 'convo-1', + autoDisableAfterFailures: 3, + }); + + expect((await getSchedule(schedule.id)).lastRun?.status).toBe('requires_action'); + }); + it('keeps the occurrence marker when recording a skip', async () => { const schedule = await methods.createSchedule(scheduleData()); const older = new Date('2026-07-26T10:00:00.000Z');