mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 08:19:54 +02:00
Retain denial completion observed during evidence persistence
This commit is contained in:
1 parent
4bd9a581fc
commit
dc5ebe3f92
2 files changed
+45
-3
No files matched your search
@@ -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<ReturnType<typeof input.load>>;
|
||||
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<Row>(`/api/heartbeat-runs/${input.request.runId}/cancel`);
|
||||
const stopAcknowledgement = readCopilotStopAcknowledgement(stopped,
|
||||
|
||||
@@ -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<ReturnType<typeof observeCopilotPreStop>> = [];
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user