From dc5ebe3f92d405e284b096f5add797864bb0f74e Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 14:02:45 -0500 Subject: [PATCH] Retain denial completion observed during evidence persistence --- tests/runner-e2e/copilot-protection-flow.ts | 17 ++++++++-- .../copilot-protection-settlement.test.ts | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/runner-e2e/copilot-protection-flow.ts b/tests/runner-e2e/copilot-protection-flow.ts index fbfd83c7cd..a2d566b2da 100644 --- a/tests/runner-e2e/copilot-protection-flow.ts +++ b/tests/runner-e2e/copilot-protection-flow.ts @@ -67,9 +67,20 @@ export async function settleCopilotDeniedRun(input: { // this causal boundary, and a later replay must never manufacture it. await input.retainPreStop(preStop); if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop"); - const dispatchState = await input.load(); - assertBeforeStop(dispatchState); - if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop"); + let dispatchState: Awaited>; + while (true) { + dispatchState = await input.load(); + assertBeforeStop(dispatchState); + if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop"); + const observed = observeCopilotPreStop({ events: dispatchState.events, request: input.request, companyId: dispatchState.issue.companyId, cancellationRequestId }); + if (JSON.stringify(observed.terminal) === JSON.stringify(preStop.terminal)) break; + // Completion can arrive while the previous receipt is being persisted. + // Retain that actual pre-dispatch observation, then recheck for an earlier + // Stop after the write rather than discarding the newly observed terminal. + preStop = observed; + await input.retainPreStop(preStop); + if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop"); + } const stopDispatchMonotonicNs = process.hrtime.bigint().toString(); const stopped = await input.api.post(`/api/heartbeat-runs/${input.request.runId}/cancel`); const stopAcknowledgement = readCopilotStopAcknowledgement(stopped, diff --git a/tests/runner-e2e/copilot-protection-settlement.test.ts b/tests/runner-e2e/copilot-protection-settlement.test.ts index 6744b63a9e..71d63bfcf2 100644 --- a/tests/runner-e2e/copilot-protection-settlement.test.ts +++ b/tests/runner-e2e/copilot-protection-settlement.test.ts @@ -164,6 +164,37 @@ describe("Copilot denial provider settlement and separate audited run Stop", () await vi.advanceTimersByTimeAsync(200); expect((await result).branch).toBe("provider_cancelled_or_interrupted"); expect(afterSettlement).toHaveBeenCalledOnce(); } finally { vi.restoreAllMocks(); vi.useRealTimers(); } }); + it("retains completion first observed after the initial receipt write before dispatching Stop", async () => { + vi.useFakeTimers(); let mono = 100n; vi.spyOn(process.hrtime, "bigint").mockImplementation(() => ++mono); + try { + const f = fixture(); let completionVisible = false; + const receipts: Array> = []; + const post = stopPost(f, () => { expect(receipts).toHaveLength(2); expect(receipts[1]!.terminal?.eventType).toBe("turn.completed"); }); + const result = settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 5000, + load: async () => ({ ...f, events: completionVisible ? f.events : f.events.filter((_, i) => i !== 4), run: post.mock.calls.length ? f.run : liveRun(f), retired: true }), + afterDeniedEdit: async () => {}, retainPreStop: async receipt => { receipts.push(receipt); completionVisible = true; }, afterSettlement: async () => {} }); + await vi.advanceTimersByTimeAsync(2000); + const settled = await result; + expect(settled.branch).toBe("provider_completed_observed_before_stop"); + expect(settled.preStop).toEqual(receipts[1]); expect(receipts[0]!.terminal).toBeNull(); expect(post).toHaveBeenCalledOnce(); + } finally { vi.restoreAllMocks(); vi.useRealTimers(); } + }); + it.each(["earlier-stop", "retention-failed"])("refuses dispatch when refreshed terminal retention encounters %s", async kind => { + vi.useFakeTimers(); let mono = 100n; vi.spyOn(process.hrtime, "bigint").mockImplementation(() => ++mono); + try { + const f = fixture(); let completionVisible = false, earlierStop = false, writes = 0; + const post = stopPost(f); + const result = settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 5000, + load: async () => ({ ...f, events: completionVisible ? f.events : f.events.filter((_, i) => i !== 4), run: earlierStop ? f.run : liveRun(f), retired: true }), + afterDeniedEdit: async () => {}, retainPreStop: async () => { + writes++; completionVisible = true; + if (writes === 2) { if (kind === "retention-failed") throw new Error("refreshed receipt write failed"); earlierStop = true; } + }, afterSettlement: async () => {} }); + const rejected = expect(result).rejects.toThrow(kind === "earlier-stop" ? "earlier Stop" : "refreshed receipt write failed"); + await vi.advanceTimersByTimeAsync(2000); await rejected; + expect(writes).toBe(2); expect(post).not.toHaveBeenCalled(); + } finally { vi.restoreAllMocks(); vi.useRealTimers(); } + }); it("rejects normal completion first returned after Stop even with an earlier transaction timestamp", async () => { vi.useFakeTimers(); let mono = 100n; vi.spyOn(process.hrtime, "bigint").mockImplementation(() => ++mono); try {