diff --git a/landing-zone-configurator/app/apps/api/src/applications/interrupted-plan.ts b/landing-zone-configurator/app/apps/api/src/applications/interrupted-plan.ts index c1af1fd5..b74517fd 100644 --- a/landing-zone-configurator/app/apps/api/src/applications/interrupted-plan.ts +++ b/landing-zone-configurator/app/apps/api/src/applications/interrupted-plan.ts @@ -153,6 +153,7 @@ export async function completeInterruptedPlan( // Recheck after State observation; neither a stale CF read nor an expired // historical proof can authorize this terminal transition. await verifyStopped(); + const observedAt = new Date().toISOString(); try { const last = await read(backend, backend.descriptor.key); if ( @@ -166,7 +167,7 @@ export async function completeInterruptedPlan( if (error instanceof RecoveryReplanError) throw error; fail("state_unavailable"); } - const observedAt = new Date().toISOString(); + if (Date.now() - Date.parse(observedAt) > 30_000) fail("changed"); return work(async (client) => { const latest = await context(client); const { existingReceipt: _before, ...before } = c; diff --git a/landing-zone-configurator/app/tests/application-interrupted-plan.test.ts b/landing-zone-configurator/app/tests/application-interrupted-plan.test.ts index 692b7bf9..513641a1 100644 --- a/landing-zone-configurator/app/tests/application-interrupted-plan.test.ts +++ b/landing-zone-configurator/app/tests/application-interrupted-plan.test.ts @@ -319,3 +319,31 @@ it("revalidates bindings in a new transaction after external observations", asyn }); expect(f.recorded()).toBe(false); }); +it.each([29_000, 31_000])( + "retains the actual Runner observation time across a %sms final State read", + async (delay) => { + vi.useFakeTimers({ toFake: ["Date"] }); + const observed = new Date("2026-01-01T00:00:00.000Z"); + vi.setSystemTime(observed); + try { + const f = fixture(); + let reads = 0; + f.read.mockImplementation(async (_backend, key) => { + if (!key.endsWith(".tflock") && ++reads === 3) + vi.setSystemTime(observed.getTime() + delay); + return null; + }); + if (delay > 30_000) { + await expect(f.run()).rejects.toMatchObject({ + code: "application_plan_recovery_changed", + }); + expect(f.recorded()).toBe(false); + } else { + await expect(f.run()).resolves.toMatchObject({ status: "failed" }); + expect(f.query.mock.calls.at(-1)?.[1][4]).toBe(observed.toISOString()); + } + } finally { + vi.useRealTimers(); + } + }, +);