mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
Reviewed partition of the current-master Copilot integration. Complete v30 live qualification is retained; current v31 recovery compatibility, CI and review are tracked explicitly. Co-Authored-By: Paperclip <noreply@paperclip.ing>
254 lines
21 KiB
TypeScript
254 lines
21 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { applyMainlineStopMetadata, stopAcknowledgementForTest } from "./copilot-stop-test-fixtures.js";
|
|
import { readCopilotToolEvidence } from "./copilot-evidence.js";
|
|
import { observeCopilotPreStop, copilotDenialSampleCursor, readCopilotDenialSettlement } from "./copilot-protection-evidence.js";
|
|
import { settleCopilotDeniedRun } from "./copilot-protection-flow.js";
|
|
|
|
// Sanitized source order from the failed Daytona attempt. Its .900 createdAt
|
|
// is transaction time, not commit evidence. Pre-Stop receipts below are synthetic
|
|
// test observations; the historical paid attempt did not capture one.
|
|
const date = (ms: number) => new Date(Date.UTC(2026, 8, 30, 15, 13, 7, ms)).toISOString();
|
|
function fixture(eventType = "turn.completed") {
|
|
const frame = (seq: number, type: string, payload: unknown, persisted = 305, emitted = 129): any => ({
|
|
seq: seq + 40, companyId: "company", runId: "run", eventType: type, createdAt: date(persisted),
|
|
payload: { prpEvent: { schema: "paperclip.prp.event.v1", sourceKind: "runner", runId: "run", turnId: "turn",
|
|
normalizedSessionId: "normalized-session", sourceInstanceId: "runner-instance", sourceSeq: seq,
|
|
sourceEventId: `runner-instance:run:${seq}`, eventType: type, emittedAt: date(emitted), payload } },
|
|
});
|
|
const notice = (seq: number, stage: string, details: Record<string, string>) => frame(seq, "provider.notice.recorded", {
|
|
schema: "paperclip.provider.notice.v1", category: "copilot_tool_evidence_v1", scope: "turn",
|
|
provenance: { sessionId: "native-session", turnId: "turn", eventType: stage, method: stage === "tool" ? "session/update" : "session/request_permission" },
|
|
details: Object.entries({ stage, toolCallId: "denied-edit", operation: "edit", ...details }).map(([name, value]) => ({ name, value })),
|
|
});
|
|
const events = [notice(111, "permission_requested", { requestId: "request", target: "copilot-denied-nonce.txt", declineOffered: "true" }),
|
|
notice(113, "permission_delivered", { requestId: "request", outcome: "reject_once" }), notice(114, "tool", { status: "failed" }),
|
|
frame(115, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", executionId: "denied-edit", operation: "edit", status: "failed" }, 317),
|
|
frame(121, eventType, { status: eventType.slice(5), error: null }, 900, 887),
|
|
frame(112, "runtime_request.resolved", { requestId: "request", turnId: "turn", requestKind: "permission_approval", action: "decline" })];
|
|
const run: any = { id: "run", companyId: "company", nativeIssueId: "issue", status: "cancelled", finishedAt: date(910),
|
|
resultJson: { startupCancellation: { requestedAt: date(892), cancellationRequestId: "11111111-1111-4111-8111-111111111111" }, nativeCancellation: {
|
|
schema: "paperclip.native-cancellation.v1", companyId: "company", runId: "run", issueId: "issue", scope: "run", dispatched: true,
|
|
dispatchState: "acknowledged", reasonCode: "cancellation_run_only", effects: ["release_run_resources"], intentId: "native-cancellation:11111111-1111-4111-8111-111111111111",
|
|
intentAuditId: "intent-audit", acknowledgementAuditId: "ack-audit", recordedAt: date(901), acknowledgedAt: date(909),
|
|
} } };
|
|
applyMainlineStopMetadata(run);
|
|
const request = readCopilotToolEvidence(events, "run")[0]!;
|
|
const preStop = observeCopilotPreStop({ events, request, companyId: "company", cancellationRequestId: "11111111-1111-4111-8111-111111111111" });
|
|
const stopDispatchMonotonicNs = process.hrtime.bigint().toString();
|
|
const stopAcknowledgement = stopAcknowledgementForTest(run, preStop.cancellationRequestId, stopDispatchMonotonicNs);
|
|
return { events, request, run, issue: { id: "issue", companyId: "company", status: "in_progress" }, preStop, stopDispatchMonotonicNs, stopAcknowledgement };
|
|
|
|
}
|
|
const terminal = (f: ReturnType<typeof fixture>) => f.events[4]!.payload.prpEvent;
|
|
const liveRun = (f: ReturnType<typeof fixture>) => ({ ...f.run, status: "running", resultJson: {} });
|
|
function stopPost(f: ReturnType<typeof fixture>, callback = () => {}) {
|
|
return vi.fn(async (_path: string) => {
|
|
callback();
|
|
return f.run;
|
|
});
|
|
}
|
|
|
|
describe("Copilot denial provider settlement and separate audited run Stop", () => {
|
|
it("accepts a synthetic pre-Stop API observation without claiming provider cancellation", () => {
|
|
const f = fixture();
|
|
expect(readCopilotDenialSettlement(f)).toMatchObject({ branch: "provider_completed_observed_before_stop", providerCancellationTerminalObserved: false,
|
|
providerTerminal: { sourceSeq: 121, failedToolSourceSeq: 115, rowCreatedAtMs: Date.parse(date(900)) }, runStop: { status: "cancelled" } });
|
|
// Native/provider clock skew is irrelevant to the operator causal boundary.
|
|
terminal(f).emittedAt = date(999);
|
|
f.preStop = observeCopilotPreStop({ ...f, companyId: "company", cancellationRequestId: "11111111-1111-4111-8111-111111111111" }); f.stopDispatchMonotonicNs = process.hrtime.bigint().toString();
|
|
f.stopAcknowledgement = stopAcknowledgementForTest(f.run, f.preStop.cancellationRequestId, f.stopDispatchMonotonicNs);
|
|
expect(readCopilotDenialSettlement(f).branch).toBe("provider_completed_observed_before_stop");
|
|
});
|
|
it.each(["turn.cancelled", "turn.interrupted"])("retains the distinct observed %s branch", type => {
|
|
expect(readCopilotDenialSettlement(fixture(type))).toMatchObject({ branch: "provider_cancelled_or_interrupted", providerCancellationTerminalObserved: true });
|
|
});
|
|
it("cannot use a legacy summary or cancelled run as provider evidence", () => {
|
|
const f = fixture(); f.events.splice(4, 1); f.events.push({ eventType: "turn.completed", payload: {} });
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["equal", "later"])("accepts observed pre-Stop completion with %s transaction time, without ordering it against ack", kind => {
|
|
const f = fixture(); f.events[4]!.createdAt = date(kind === "equal" ? 909 : 910);
|
|
f.preStop = observeCopilotPreStop({ ...f, companyId: "company", cancellationRequestId: "11111111-1111-4111-8111-111111111111" }); f.stopDispatchMonotonicNs = process.hrtime.bigint().toString();
|
|
f.stopAcknowledgement = stopAcknowledgementForTest(f.run, f.preStop.cancellationRequestId, f.stopDispatchMonotonicNs);
|
|
expect(readCopilotDenialSettlement(f).branch).toBe("provider_completed_observed_before_stop");
|
|
});
|
|
it("rejects late normal completion even when its transaction began before Stop ack", () => {
|
|
const f = fixture(); f.preStop.terminal = null; f.events[4]!.createdAt = date(800);
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["missing", "foreign", "hash", "sequence", "equal-clock", "reverse-clock", "non-string-clock"])("rejects %s pre-dispatch observation", kind => {
|
|
const f = fixture();
|
|
if (kind === "missing") (f as any).preStop = null;
|
|
if (kind === "foreign") f.preStop.runId = "foreign";
|
|
if (kind === "hash") f.preStop.terminal!.rowSha256 = `sha256:${"a".repeat(64)}`;
|
|
if (kind === "sequence") f.preStop.terminal!.sourceSeq++;
|
|
if (kind === "equal-clock") f.stopDispatchMonotonicNs = f.preStop.apiReadCompletedMonotonicNs;
|
|
if (kind === "reverse-clock") f.stopDispatchMonotonicNs = "1";
|
|
if (kind === "non-string-clock") (f.preStop as any).apiReadCompletedMonotonicNs = ["1"];
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["runId", "turnId", "normalizedSessionId", "sourceInstanceId", "eventType", "sourceEventId"])("rejects terminal %s mismatch", field => {
|
|
const f = fixture(); terminal(f)[field] = "foreign";
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["missing", "duplicate", "failed", "before-tool", "wrong-status", "error"])("rejects %s terminal", kind => {
|
|
const f = fixture();
|
|
if (kind === "missing") f.events.splice(4, 1);
|
|
if (kind === "duplicate") f.events.push(structuredClone(f.events[4]));
|
|
if (kind === "failed") { f.events[4]!.eventType = terminal(f).eventType = "turn.failed"; terminal(f).payload.status = "failed"; }
|
|
if (kind === "before-tool") { terminal(f).sourceSeq = 114; terminal(f).sourceEventId = "runner-instance:run:114"; }
|
|
if (kind === "wrong-status") terminal(f).payload.status = "cancelled";
|
|
if (kind === "error") terminal(f).payload.error = { code: "failed" };
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["scope", "runId", "companyId", "issueId", "intentId", "intentAuditId", "acknowledgementAuditId", "acknowledgedAt", "recordedAt", "schema", "dispatchState", "dispatched", "reasonCode", "effects"])("rejects malformed or unbound Stop %s", field => {
|
|
const f = fixture(); f.run.resultJson.nativeCancellation[field] = null;
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["missing-stop", "pending", "wrong-scope", "same-audit", "wrong-run-status", "issue-done", "ack-before-intent"])("rejects %s controller state", kind => {
|
|
const f = fixture(), c = f.run.resultJson.nativeCancellation;
|
|
if (kind === "missing-stop") delete f.run.resultJson.nativeCancellation;
|
|
if (kind === "pending") c.dispatchState = "pending";
|
|
if (kind === "wrong-scope") c.scope = "turn";
|
|
if (kind === "same-audit") c.acknowledgementAuditId = c.intentAuditId;
|
|
if (kind === "wrong-run-status") f.run.status = "succeeded";
|
|
if (kind === "issue-done") f.issue.status = "done";
|
|
if (kind === "ack-before-intent") c.recordedAt = date(911);
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow(/settlement/);
|
|
});
|
|
it.each(["native-session", "normalized-session", "failed-tool", "delivery", "duplicate-tool", "tool-order"])("rejects %s corruption", kind => {
|
|
const f = fixture();
|
|
if (kind === "native-session") f.events[2]!.payload.prpEvent.payload.provenance.sessionId = "foreign";
|
|
if (kind === "normalized-session") f.events[2]!.payload.prpEvent.normalizedSessionId = "foreign";
|
|
if (kind === "failed-tool") f.events[3]!.payload.prpEvent.payload.status = "completed";
|
|
if (kind === "delivery") f.events[1]!.payload.prpEvent.payload.details.find((d: any) => d.name === "outcome").value = "allow_once";
|
|
if (kind === "duplicate-tool") f.events.push(structuredClone(f.events[3]));
|
|
if (kind === "tool-order") f.events[3]!.payload.prpEvent.sourceSeq = 122;
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow();
|
|
});
|
|
it.each(["missing", "duplicate", "foreign-turn", "accept", "expired", "after-delivery"])("rejects %s durable resolution", kind => {
|
|
const f = fixture(), r = f.events[5]!, p = r.payload.prpEvent;
|
|
if (kind === "missing") f.events.pop();
|
|
if (kind === "duplicate") f.events.push(structuredClone(r));
|
|
if (kind === "foreign-turn") p.payload.turnId = "foreign";
|
|
if (kind === "accept") p.payload.action = "accept";
|
|
if (kind === "expired") r.eventType = p.eventType = "runtime_request.expired";
|
|
if (kind === "after-delivery") { p.sourceSeq = 114; p.sourceEventId = "runner-instance:run:114"; }
|
|
expect(() => readCopilotDenialSettlement(f)).toThrow();
|
|
});
|
|
it("captures the exact durable stream at the sampling boundary without clock conversion", () => {
|
|
const f = fixture();
|
|
expect(copilotDenialSampleCursor([f.events[0]], f.request)).toEqual({ runId: "run", turnId: "turn", normalizedSessionId: "normalized-session", sourceInstanceId: "runner-instance", sourceSeq: 111 });
|
|
expect(copilotDenialSampleCursor(f.events, f.request).sourceSeq).toBe(121);
|
|
f.events[1]!.payload.prpEvent.normalizedSessionId = "foreign";
|
|
expect(() => copilotDenialSampleCursor(f.events, f.request)).toThrow(/cursor/);
|
|
});
|
|
it("awaits exact pre-Stop receipt retention and waits for post-Stop cancelled terminal before sampling", async () => {
|
|
vi.useFakeTimers(); let mono = 100n; vi.spyOn(process.hrtime, "bigint").mockImplementation(() => ++mono);
|
|
try {
|
|
const f = fixture("turn.cancelled"); let loads = 0, dispatched = false, postLoads = 0;
|
|
let release!: () => void; const retention = new Promise<void>(resolve => { release = resolve; });
|
|
const post = stopPost(f, () => { dispatched = true; });
|
|
const retainPreStop = vi.fn(async (receipt) => { expect(receipt.terminal).toBeNull(); await retention; });
|
|
const afterDeniedEdit = vi.fn(async () => {}), afterSettlement = vi.fn(async () => {});
|
|
const result = settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 5000,
|
|
load: async () => {
|
|
loads++; if (dispatched) postLoads++;
|
|
const events = f.events.filter((_, i) => !(loads === 1 && i === 3) && !((!dispatched || postLoads === 1) && i === 4));
|
|
return { ...f, events, run: dispatched ? f.run : liveRun(f), retired: dispatched };
|
|
}, afterDeniedEdit, retainPreStop, afterSettlement });
|
|
await vi.advanceTimersByTimeAsync(0); expect(post).not.toHaveBeenCalled(); expect(afterDeniedEdit).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(200); expect(afterDeniedEdit).toHaveBeenCalledOnce(); expect(post).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(2000); expect(retainPreStop).toHaveBeenCalledOnce(); expect(post).not.toHaveBeenCalled();
|
|
release(); await vi.advanceTimersByTimeAsync(0); expect(post).toHaveBeenCalledOnce(); expect(afterSettlement).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(200); expect((await result).branch).toBe("provider_cancelled_or_interrupted"); expect(afterSettlement).toHaveBeenCalledOnce();
|
|
} 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 {
|
|
const f = fixture(); f.events[4]!.createdAt = date(1); let dispatched = false;
|
|
const sample = vi.fn(async () => {}), post = stopPost(f, () => { dispatched = true; });
|
|
const retain = vi.fn(async (receipt) => { expect(receipt.terminal).toBeNull(); });
|
|
const result = settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 3000,
|
|
load: async () => ({ ...f, run: dispatched ? f.run : liveRun(f), events: dispatched ? f.events : f.events.filter((_, i) => i !== 4), retired: dispatched }),
|
|
afterDeniedEdit: async () => {}, retainPreStop: retain, afterSettlement: sample });
|
|
const rejected = expect(result).rejects.toThrow(/Timed out waiting for correlated provider settlement/);
|
|
await vi.advanceTimersByTimeAsync(3200); await rejected;
|
|
expect(retain).toHaveBeenCalledOnce(); expect(post).toHaveBeenCalledOnce(); expect(sample).not.toHaveBeenCalled();
|
|
} finally { vi.restoreAllMocks(); vi.useRealTimers(); }
|
|
});
|
|
it.each(["missing-edit", "foreign-edit", "missing-terminal", "foreign-terminal", "live-process"])("fails closed on %s without premature side effects or terminal samples", async kind => {
|
|
vi.useFakeTimers(); let mono = 100n; vi.spyOn(process.hrtime, "bigint").mockImplementation(() => ++mono);
|
|
try {
|
|
const f = fixture(), post = stopPost(f), sample = vi.fn(async () => {});
|
|
if (kind === "missing-edit") f.events.splice(3, 1);
|
|
if (kind === "foreign-edit") f.events[3]!.payload.prpEvent.normalizedSessionId = "foreign";
|
|
if (kind === "missing-terminal") f.events.splice(4, 1);
|
|
if (kind === "foreign-terminal") terminal(f).sourceInstanceId = "foreign";
|
|
const result = settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 3000,
|
|
load: async () => ({ ...f, run: post.mock.calls.length ? f.run : liveRun(f), retired: kind !== "live-process" }), afterDeniedEdit: async () => {}, retainPreStop: async () => {}, afterSettlement: sample });
|
|
const rejected = expect(result).rejects.toThrow();
|
|
await vi.advanceTimersByTimeAsync(3200); await rejected;
|
|
expect(post).toHaveBeenCalledTimes(kind.endsWith("edit") || kind === "foreign-terminal" ? 0 : 1);
|
|
expect(sample).not.toHaveBeenCalled();
|
|
} finally { vi.restoreAllMocks(); vi.useRealTimers(); }
|
|
});
|
|
it.each(["failed", "timed_out"])("reports %s promptly even when required rows are missing", async status => {
|
|
const f = fixture(), post = stopPost(f), sample = vi.fn(async () => {});
|
|
const load = vi.fn(async () => ({ ...f, events: [], run: { ...f.run, status }, retired: false }));
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 10000,
|
|
load, afterDeniedEdit: sample, retainPreStop: sample, afterSettlement: sample })).rejects.toThrow(`Stopped waiting for persisted correlated failed native edit: Copilot provider run failed`);
|
|
expect(load).toHaveBeenCalledOnce(); expect(post).not.toHaveBeenCalled(); expect(sample).not.toHaveBeenCalled();
|
|
});
|
|
it.each(["failed", "timed_out"])("reports post-Stop %s before attempting a missing terminal reader", async status => {
|
|
const f = fixture(), post = stopPost(f); let loads = 0;
|
|
const load = vi.fn(async () => ({ ...f, events: ++loads < 4 ? f.events : [], run: loads < 4 ? liveRun(f) : { ...f.run, status }, retired: true }));
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 10000,
|
|
load, afterDeniedEdit: async () => {}, retainPreStop: async () => {}, afterSettlement: async () => {} })).rejects.toThrow("Stopped waiting for correlated provider settlement and retired run: Copilot provider run failed");
|
|
expect(load).toHaveBeenCalledTimes(4); expect(post).toHaveBeenCalledOnce();
|
|
});
|
|
it("does not dispatch Stop if the pre-dispatch receipt cannot be retained", async () => {
|
|
const f = fixture(), post = stopPost(f);
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 1000,
|
|
load: async () => ({ ...f, run: post.mock.calls.length ? f.run : liveRun(f), retired: true }), afterDeniedEdit: async () => {}, afterSettlement: async () => {},
|
|
retainPreStop: async () => { throw new Error("artifact write failed"); } })).rejects.toThrow("artifact write failed");
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
it("retains completed-before-Stop settlement and propagates retirement sampling failure", async () => {
|
|
const f = fixture(), post = stopPost(f);
|
|
const input = { api: { post } as any, request: f.request, deadlineAt: Date.now() + 1000,
|
|
load: async () => ({ ...f, run: post.mock.calls.length ? f.run : liveRun(f), retired: true }), afterDeniedEdit: async () => {}, retainPreStop: async () => {}, afterSettlement: async () => {} };
|
|
expect((await settleCopilotDeniedRun(input)).branch).toBe("provider_completed_observed_before_stop");
|
|
post.mockClear();
|
|
await expect(settleCopilotDeniedRun({ ...input, afterSettlement: async () => { throw new Error("remote descendants live"); } })).rejects.toThrow("remote descendants live");
|
|
});
|
|
it.each(["cancelled", "pending-intent", "startup-only"])("rejects visible earlier Stop %s before dispatch", async kind => {
|
|
const f = fixture(), post = stopPost(f);
|
|
const run = kind === "cancelled" ? f.run : { ...liveRun(f), resultJson: kind === "startup-only"
|
|
? { startupCancellation: { requestedAt: date(1) } } : { nativeCancellation: { intentId: "earlier", dispatchState: "pending" } } };
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 1000,
|
|
load: async () => ({ ...f, run, retired: false }), afterDeniedEdit: async () => {}, retainPreStop: async () => {}, afterSettlement: async () => {},
|
|
})).rejects.toThrow("earlier Stop");
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
it("rejects Stop racing during artifact retention", async () => {
|
|
const f = fixture(), post = stopPost(f); let earlier = false;
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 1000,
|
|
load: async () => ({ ...f, run: earlier ? f.run : liveRun(f), retired: false }), afterDeniedEdit: async () => {},
|
|
retainPreStop: async () => { earlier = true; }, afterSettlement: async () => {},
|
|
})).rejects.toThrow("earlier Stop");
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
it.each(["conflict", "foreign-response"])("rejects a conflicting or foreign-caller Stop API response: %s", async kind => {
|
|
const f = fixture(); const post = vi.fn(async (_path: string) => { if (kind === "conflict") throw new Error("409 cancellation intent conflict"); f.run.resultJson.cancelledByUserId = "foreign-caller"; return f.run; });
|
|
const sampled = vi.fn(async () => {});
|
|
await expect(settleCopilotDeniedRun({ api: { post } as any, request: f.request, deadlineAt: Date.now() + 1000,
|
|
load: async () => ({ ...f, run: liveRun(f), retired: false }), afterDeniedEdit: async () => {}, retainPreStop: async () => {}, afterSettlement: sampled,
|
|
})).rejects.toThrow(kind === "conflict" ? "409" : "Stop acknowledgement");
|
|
expect(post).toHaveBeenCalledOnce(); expect(sampled).not.toHaveBeenCalled();
|
|
expect(post.mock.calls[0]).toEqual(["/api/heartbeat-runs/run/cancel"]);
|
|
});
|
|
|
|
});
|