fix(runner-e2e): bind active-stop evidence before dispatch

Accept either canonical permission/tool arrival order while binding the exact native-origin and tool-start rows before Stop and through cancellation. Version the pending receipt and suite; retain strict identity, no-effects, unanswered-request, and settlement checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 22:02:09 -05:00
1 parent 2cbf72ec96
commit 7fe2a2528d
6 files changed
+125 -15

No files matched your search

+13 -2
View File
@@ -397,7 +397,7 @@ request can stop; it does not accept a no-op Stop of an already terminal run.
`native-active-stop` / `pending-permission-stop` has four explicit-only cells:
Cursor and Copilot, each local and Daytona. Its `native_active_stop` flow retains
`paperclip.e2e.native-active-stop-pending.v1` from the public API while exactly one
`paperclip.e2e.native-active-stop-pending.v2` from the public API while exactly one
native permission remains pending, the exact controller run is running, and no
Stop or answer marker exists. The receipt independently binds native session,
normalized session, turn, source instance, request/tool IDs, source sequences and
@@ -407,6 +407,17 @@ boundaries. Remote provider clocks and database transaction timestamps never
establish that ordering. The atomic caller UUID fence rejects an earlier racing
Stop instead of borrowing its acknowledgement.
Suite version 3 accepts either canonical card/tool-start arrival order. The
exact native origin, canonical tool start and unanswered permission must all
exist in both pre-Stop API observations. The v2 pending receipt adds the native
origin and tool-start row hashes and source sequences. The fresh reread and
settlement must preserve those exact rows; a later tool start cannot backfill
missing pre-Stop evidence. Command/path, request, tool, turn, session and source
checks remain strict. The native evidence projector still requires the exact
tool origin before it can emit correlated permission evidence; this does not
claim the original ACP wire order. Old v1 receipts are not valid inputs to the
new grader. Earlier paid failures retain their original definition and grade.
`paperclip.e2e.native-active-stop-settlement.v1` accepts only
`pending_permission_cancelled`: explicit-cancellation request closure with
`replayAllowed:false`, then one exact `turn.cancelled`, plus the same scoped
@@ -418,7 +429,7 @@ absence through continuous observation, and all observed owned descendants
retired. Local files require four fresh observations through cleanup. Daytona
instead retains two live snapshots (baseline and pending) plus one automatic
owned-process-retirement seal, with a continuous zero-mutation watcher and the
same complete root/descendant journal. Suite version 2 retains this as
same complete root/descendant journal. Since suite version 2, this is retained as
`paperclip.e2e.native-active-stop-remote-retirement.v1`; it explicitly records
`filesystemAfterRetirementObserved:false`. The per-turn observer seals itself
when the owned tree retires, before the sandbox is released. Reading that receipt
+10 -1
View File
@@ -1448,6 +1448,15 @@ exact native write, observes its unanswered permission card, then sends the
public Stop request with a freshly retained caller UUID. It never denies or
approves that callback before Stop.
Suite version 3 accepts a permission card before or after its correlated tool
start. Both the exact native origin and canonical start must already exist with
the unanswered permission when the fixture retains and rechecks the pre-Stop
API snapshot. The v2 pending receipt binds all four rows by hash and source
sequence through settlement. Missing, changed or replayed evidence cannot be
filled in after Stop. This is a new grader definition; old v1 pending receipts
and failed paid attempts are not regraded. No original provider wire order is
inferred from API insertion or display order.
Passage requires a canonical cancelled request and cancelled provider turn,
exact caller-intent acknowledgement, an unfinished task, rejection of a later
stale answer, no follow-up run, continuous target no-effect observation and
@@ -1457,7 +1466,7 @@ issue-interaction rows are not substituted for their authority. Pending and fina
screenshots, `native-active-stop-pending.json`, `native-active-stop-settlement.json`
and cleanup evidence retain the boundaries. Daytona also requires the exact
owned lease and sealed remote observer proof before sandbox deletion. Suite
version 2 records remote filesystem coverage only through verified retirement
version 2 introduced remote filesystem coverage only through verified retirement
of the owned runner/provider tree, with no target or workspace mutations; it
does not call a later read of the seal a fresh observation. Local cases retain
four filesystem phases through cleanup. Remote UI/stale-answer checks and final
+1 -1
View File
@@ -1052,7 +1052,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
description: "Stop while one exact Cursor or Copilot native permission remains unanswered; require cancelled provider settlement, caller-owned acknowledgement, stale-answer refusal and independent retirement/no effects.",
groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => ["cursor", "copilot"].includes(profile.qualificationCandidate ?? "")),
environments: runnerEnvironments, tasks: nativeActiveStopTasks, expectedMatrixSize: 4,
definitionMetadata: { version: 2, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v1", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" },
definitionMetadata: { version: 3, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v1", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" },
},
{
id: "copilot-protection", label: "Copilot native protection", manualOnly: true,
@@ -12,10 +12,11 @@ export interface ActiveStopCaller { type: "board"; userId: "local-board"; source
export type ActiveStopProvider = "cursor" | "copilot";
export interface ActiveStopScope { provider: ActiveStopProvider; companyId: string; issueId: string; runId: string; target: string; commandSha256?: string }
export interface ActiveStopPending {
schema: "paperclip.e2e.native-active-stop-pending.v1";
schema: "paperclip.e2e.native-active-stop-pending.v2";
caller: ActiveStopCaller; scope: ActiveStopScope; requestId: string; toolCallId: string; nativeSessionId: string; turnId: string;
normalizedSessionId: string; sourceInstanceId: string; requestSourceSeq: number; permissionSourceSeq: number;
requestRowSha256: string; permissionRowSha256: string; cancellationRequestId: string; observedMonotonicNs: string;
toolOriginSourceSeq: number; toolStartedSourceSeq: number; toolOriginRowSha256: string; toolStartedRowSha256: string;
}
const rec = (v: unknown): Row => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Row : {};
const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(v) && !v.includes("[REDACTED]");
@@ -99,13 +100,17 @@ function origin(events: readonly unknown[], scope: ActiveStopScope, bootstrap?:
&& executions.filter(x => x.event.eventType === "tool.execution.completed").length <= 1
&& executions.every(x => rec(x.event.payload).schema === "paperclip.tool.execution.v1" && rec(x.event.payload).transport === "builtin"
&& ["tool.execution.started", "tool.execution.progressed", "tool.execution.completed"].includes(x.event.eventType)
&& rec(x.event.payload).status === (x.event.eventType === "tool.execution.completed" ? "failed" : "running"))
&& executions.find(x => x.event.eventType === "tool.execution.started")!.event.sourceSeq < created[0]!.event.sourceSeq, "duplicate canonical operation lifecycle");
&& rec(x.event.payload).status === (x.event.eventType === "tool.execution.completed" ? "failed" : "running")), "duplicate canonical operation lifecycle");
// ACP permits a permission request before its tool notification. Require
// both in the pre-Stop observation, without imposing a card/start order.
const toolOrigin = rows.find(x => x.row.seq === tools[0]!.seq);
const toolStarted = executions.find(x => x.event.eventType === "tool.execution.started");
fail(toolOrigin && toolStarted, "native operation origin missing");
const allowed = new Set(notices.filter(n => n.stage === "tool").map(n => bootstrapReadExecutionId(n.toolCallId)));
fail(rows.filter(x => x.event.eventType.startsWith("tool.execution.")).every(x => allowed.has(rec(x.event.payload).executionId)), "unattributed operation");
fail(rows.filter(x => x.event.eventType === "tool.execution.completed" && rec(x.event.payload).executionId === bootstrapReadExecutionId(notice.toolCallId))
.every(x => rec(x.event.payload).status !== "completed"), "target operation completed");
return { rows, notice, permission, created: created[0]!, request, stream };
return { rows, notice, permission, created: created[0]!, toolOrigin, toolStarted, request, stream };
}
export function observeActiveStopPending(input: { events: readonly unknown[]; run: Row; issue: Row; scope: ActiveStopScope; caller: ActiveStopCaller; cancellationRequestId: string; bootstrap?: BootstrapReadProof }): ActiveStopPending {
const { run, issue, scope } = input, proof = origin(input.events, scope, input.bootstrap);
@@ -115,19 +120,23 @@ export function observeActiveStopPending(input: { events: readonly unknown[]; ru
fail(!(scope.provider === "cursor" ? readCursorToolEvidence(input.events, scope.runId) : readCopilotToolEvidence(input.events, scope.runId)).some(n => n.stage === "permission_delivered"), "permission already answered");
fail(!proof.rows.some(x => terminals.has(x.event.eventType) || closures.has(x.event.eventType)), "request or provider already settled");
fail(!proof.rows.some(x => x.event.eventType === "tool.execution.completed" && rec(x.event.payload).executionId === bootstrapReadExecutionId(proof.notice.toolCallId)), "operation already ended");
return { schema: "paperclip.e2e.native-active-stop-pending.v1", scope, caller: input.caller, requestId: proof.notice.requestId!, toolCallId: proof.notice.toolCallId,
return { schema: "paperclip.e2e.native-active-stop-pending.v2", scope, caller: input.caller, requestId: proof.notice.requestId!, toolCallId: proof.notice.toolCallId,
nativeSessionId: proof.notice.sessionId, turnId: proof.notice.turnId, normalizedSessionId: proof.permission.event.normalizedSessionId,
sourceInstanceId: proof.permission.event.sourceInstanceId, requestSourceSeq: proof.created.event.sourceSeq, permissionSourceSeq: proof.permission.event.sourceSeq,
requestRowSha256: hash(proof.created.row), permissionRowSha256: hash(proof.permission.row), cancellationRequestId: input.cancellationRequestId,
toolOriginSourceSeq: proof.toolOrigin.event.sourceSeq, toolStartedSourceSeq: proof.toolStarted.event.sourceSeq,
toolOriginRowSha256: hash(proof.toolOrigin.row), toolStartedRowSha256: hash(proof.toolStarted.row),
observedMonotonicNs: process.hrtime.bigint().toString() };
}
export function readActiveStopSettlement(input: { events: readonly unknown[]; run: Row; issue: Row; pending: ActiveStopPending; dispatchMonotonicNs: string; bootstrap?: BootstrapReadProof }) {
const before = input.pending, { run, issue } = input;
fail(before?.schema === "paperclip.e2e.native-active-stop-pending.v1" && isActiveStopCaller(before.caller) && uuid(before.cancellationRequestId)
fail(before?.schema === "paperclip.e2e.native-active-stop-pending.v2" && isActiveStopCaller(before.caller) && uuid(before.cancellationRequestId)
&& /^[1-9][0-9]{0,29}$/u.test(before.observedMonotonicNs) && /^[1-9][0-9]{0,29}$/u.test(input.dispatchMonotonicNs)
&& BigInt(before.observedMonotonicNs) < BigInt(input.dispatchMonotonicNs), "pre-dispatch pending observation missing");
const proof = origin(input.events, before.scope, input.bootstrap);
fail(hash(proof.created.row) === before.requestRowSha256 && hash(proof.permission.row) === before.permissionRowSha256
&& hash(proof.toolOrigin.row) === before.toolOriginRowSha256 && hash(proof.toolStarted.row) === before.toolStartedRowSha256
&& proof.toolOrigin.event.sourceSeq === before.toolOriginSourceSeq && proof.toolStarted.event.sourceSeq === before.toolStartedSourceSeq
&& proof.notice.sessionId === before.nativeSessionId && proof.notice.turnId === before.turnId && proof.notice.toolCallId === before.toolCallId
&& proof.notice.requestId === before.requestId && proof.permission.event.normalizedSessionId === before.normalizedSessionId
&& proof.permission.event.sourceInstanceId === before.sourceInstanceId && proof.created.event.sourceSeq === before.requestSourceSeq
@@ -138,7 +147,8 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru
fail(proof.stream(c.event) && proof.stream(t.event) && c.event.eventType === "runtime_request.cancelled" && cp.requestId === before.requestId && cp.turnId === before.turnId
&& cp.requestKind === "permission_approval" && cp.requestType === "permission" && cp.reason === "explicit_cancellation"
&& cp.provider === "acpx" && cp.itemId === proof.request.itemId && cp.replayAllowed === false && t.event.eventType === "turn.cancelled" && tp.status === "cancelled" && tp.provider === "acpx" && tp.providerTurnId === before.turnId && tp.error === null
&& c.event.sourceSeq > Math.max(before.requestSourceSeq, before.permissionSourceSeq) && t.event.sourceSeq > c.event.sourceSeq,
&& c.event.sourceSeq > Math.max(before.requestSourceSeq, before.permissionSourceSeq, before.toolOriginSourceSeq, before.toolStartedSourceSeq)
&& t.event.sourceSeq > c.event.sourceSeq,
"pending callback did not close through cancellation");
fail(proof.rows.filter(x => x.event.eventType.startsWith("tool.execution.")
|| (x.event.eventType === "provider.notice.recorded" && ["cursor_tool_evidence_v1", "copilot_tool_evidence_v1"].includes(rec(x.event.payload).category)))
+3 -1
View File
@@ -27,7 +27,9 @@ export async function stopAtPendingPermission(input: {
const pending = observeActiveStopPending({ ...state, scope: input.scope, caller: input.caller, cancellationRequestId, bootstrap: input.bootstrap });
await input.retain(pending);
const fresh = observeActiveStopPending({ ...await input.load(), scope: input.scope, caller: input.caller, cancellationRequestId, bootstrap: input.bootstrap });
if (fresh.requestRowSha256 !== pending.requestRowSha256 || fresh.permissionRowSha256 !== pending.permissionRowSha256 || Date.now() >= input.deadlineAt) throw new Error("Native active Stop pending boundary changed or expired");
if (fresh.requestRowSha256 !== pending.requestRowSha256 || fresh.permissionRowSha256 !== pending.permissionRowSha256
|| fresh.toolOriginRowSha256 !== pending.toolOriginRowSha256 || fresh.toolStartedRowSha256 !== pending.toolStartedRowSha256
|| Date.now() >= input.deadlineAt) throw new Error("Native active Stop pending boundary changed or expired");
const dispatchMonotonicNs = process.hrtime.bigint().toString();
const stopped = await input.stop(input.scope.runId, cancellationRequestId);
if (stopped.id !== input.scope.runId || stopped.resultJson?.nativeCancellation?.intentId !== `native-cancellation:${cancellationRequestId}`) throw new Error("Native active Stop response has a foreign intent");
+81 -3
View File
@@ -43,6 +43,20 @@ function fixture(provider: ActiveStopProvider = "copilot") {
}
const frame = (row: Row) => row.payload.prpEvent;
const payload = (row: Row) => frame(row).payload;
const arrivalOrders = ["tool-first", "permission-first"] as const;
function withArrivalOrder(provider: ActiveStopProvider, order: typeof arrivalOrders[number]) {
const f = fixture(provider);
if (order === "permission-first") {
// The card can be published while the provider callback is held. Native
// permission evidence still requires the later exact tool origin to flush.
f.events.unshift(f.events.pop()!);
f.events.forEach((row, index) => {
row.seq = frame(row).sourceSeq = index + 1;
frame(row).sourceEventId = `source:run:${index + 1}`;
});
}
return f;
}
function settled() { const f = fixture(); const pending = f.pending(); f.settle(); return { f, pending, read: () => readActiveStopSettlement({ ...f.state(), pending, dispatchMonotonicNs: (BigInt(pending.observedMonotonicNs) + 1n).toString() }) }; }
function withSessionPrefix(provider: ActiveStopProvider = "cursor") {
const f = fixture(provider);
@@ -65,6 +79,47 @@ function withSessionPrefix(provider: ActiveStopProvider = "cursor") {
}
describe("definitely active native permission Stop", () => {
describe.each(["cursor", "copilot"] as const)("%s pre-Stop arrival orders", provider => {
it.each(arrivalOrders)("accepts %s only after both exact origins exist and retains them through settlement", order => {
const f = withArrivalOrder(provider, order), pending = f.pending();
expect(pending.schema).toBe("paperclip.e2e.native-active-stop-pending.v2");
expect(pending.toolOriginRowSha256).toMatch(/^sha256:[a-f0-9]{64}$/u);
expect(pending.toolStartedRowSha256).toMatch(/^sha256:[a-f0-9]{64}$/u);
expect(pending.toolStartedSourceSeq < pending.requestSourceSeq).toBe(order === "tool-first");
f.settle();
expect(readActiveStopSettlement({ ...f.state(), pending, dispatchMonotonicNs: (BigInt(pending.observedMonotonicNs) + 1n).toString() }))
.toMatchObject({ branch: "pending_permission_cancelled", pending });
});
it.each([
["missing native origin", (f: ReturnType<typeof fixture>) => { f.events.splice(1, 1); }],
["missing canonical start", (f: ReturnType<typeof fixture>) => { f.events.splice(2, 1); }],
["wrong native tool", (f: ReturnType<typeof fixture>) => { payload(f.events[1]!).details.find((d: Row) => d.name === "toolCallId").value = "other"; }],
["wrong canonical tool", (f: ReturnType<typeof fixture>) => { payload(f.events[2]!).executionId = "other"; }],
["wrong command or path", (f: ReturnType<typeof fixture>) => { payload(f.events[1]!).details.find((d: Row) => d.name === (provider === "cursor" ? "commandSha256" : "target")).value = provider === "cursor" ? `sha256:${"b".repeat(64)}` : "other.txt"; }],
["wrong source", (f: ReturnType<typeof fixture>) => { frame(f.events[2]!).sourceInstanceId = "other"; frame(f.events[2]!).sourceEventId = "other:run:3"; }],
["wrong turn", (f: ReturnType<typeof fixture>) => { frame(f.events[2]!).turnId = "other"; }],
["wrong request", (f: ReturnType<typeof fixture>) => { payload(f.events[0]!).request.requestId = "other"; }],
["replayed canonical start", (f: ReturnType<typeof fixture>) => { f.events.push(f.row(5, "tool.execution.started", payload(f.events[2]!))); }],
["replayed native origin", (f: ReturnType<typeof fixture>) => { f.events.push(f.row(5, "provider.notice.recorded", payload(f.events[1]!))); }],
["answered permission", (f: ReturnType<typeof fixture>) => { f.events.push(f.notice(5, "permission_delivered", { requestId: "request", outcome: "cancel" })); }],
["terminal operation", (f: ReturnType<typeof fixture>) => { f.events.push(f.row(5, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", transport: "builtin", executionId: "tool", status: "failed" })); }],
] as const)("rejects permission-first %s before Stop", (_label, mutate) => {
const f = withArrivalOrder(provider, "permission-first"); mutate(f); expect(f.pending).toThrow();
});
it("cannot substitute a canonical start first observed after Stop", () => {
const f = withArrivalOrder(provider, "permission-first"), pending = f.pending();
const started = f.events.splice(2, 1)[0]!;
// A late row has a new durable identity even when every operation field
// is identical. Final evidence must not backfill the retained receipt.
started.seq = frame(started).sourceSeq = 5; frame(started).sourceEventId = "source:run:5";
f.events.push(started); f.settle();
for (const row of f.events.slice(-2)) {
row.seq += 1; frame(row).sourceSeq += 1; frame(row).sourceEventId = `source:run:${row.seq}`;
}
expect(() => readActiveStopSettlement({ ...f.state(), pending, dispatchMonotonicNs: (BigInt(pending.observedMonotonicNs) + 1n).toString() }))
.toThrow("observed pending request changed");
});
});
it.each(["cursor", "copilot"] as const)("accepts the mixed v1/v2 session prefix before strict %s pending proof", provider => {
const f = withSessionPrefix(provider);
expect(f.pending()).toMatchObject({ requestId: "request", toolCallId: "tool", permissionSourceSeq: 6, requestSourceSeq: 7 });
@@ -141,6 +196,13 @@ describe("definitely active native permission Stop", () => {
["replay permitted", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).replayAllowed = true; }],
["tampered pending row", ({ f }: ReturnType<typeof settled>) => { payload(f.events[3]!).request.itemId = "changed"; }],
["missing pending receipt", (s: ReturnType<typeof settled>) => { s.pending.requestRowSha256 = ""; }],
["legacy v1 pending receipt", (s: ReturnType<typeof settled>) => { (s.pending as Row).schema = "paperclip.e2e.native-active-stop-pending.v1"; }],
["missing native origin binding", (s: ReturnType<typeof settled>) => { delete (s.pending as Row).toolOriginRowSha256; }],
["missing tool start binding", (s: ReturnType<typeof settled>) => { delete (s.pending as Row).toolStartedRowSha256; }],
["changed native origin", ({ f }: ReturnType<typeof settled>) => { frame(f.events[0]!).emittedAt = "2026-01-01T00:00:01Z"; }],
["changed tool start", ({ f }: ReturnType<typeof settled>) => { frame(f.events[1]!).emittedAt = "2026-01-01T00:00:01Z"; }],
["wrong origin sequence", (s: ReturnType<typeof settled>) => { s.pending.toolOriginSourceSeq += 1; }],
["wrong start sequence", (s: ReturnType<typeof settled>) => { s.pending.toolStartedSourceSeq += 1; }],
["missing caller", ({ f }: ReturnType<typeof settled>) => { delete f.run.resultJson.startupCancellation.requestedBy; }],
["null caller", ({ f }: ReturnType<typeof settled>) => { f.run.resultJson.startupCancellation.requestedBy.userId = null; }],
["foreign caller", ({ f }: ReturnType<typeof settled>) => { f.run.resultJson.startupCancellation.requestedBy.userId = "another-board-user"; }],
@@ -178,8 +240,8 @@ describe("definitely active native permission Stop", () => {
});
describe("active Stop flow wiring", () => {
it("awaits retention and rechecks pending before issuing the single caller UUID request", async () => {
const f = fixture(); const order: string[] = []; let retainedId = "";
it.each(arrivalOrders)("awaits retention and rechecks %s evidence before issuing the single caller UUID request", async arrivalOrder => {
const f = withArrivalOrder("cursor", arrivalOrder); const order: string[] = []; let retainedId = "";
const stop = vi.fn(async (runId: string, id: string) => { expect(runId).toBe("run"); expect(id).toBe(retainedId); order.push("stop"); return f.settle(id); });
const result = await stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: Date.now() + 1000,
load: async () => { order.push("load"); return f.state(); },
@@ -193,6 +255,21 @@ describe("active Stop flow wiring", () => {
retain: async () => { if (mode === "retention failed") throw Error("disk failure"); if (mode === "competing Stop") f.run.resultJson.startupCancellation = { cancellationRequestId: "foreign" }; else f.events.push(f.row(5, "runtime_request.resolved", {})); },
})).rejects.toThrow(); expect(stop).not.toHaveBeenCalled();
});
it.each(["native origin", "canonical start"])("never sends Stop when the retained %s changes before the fresh reread", async kind => {
const f = withArrivalOrder("cursor", "permission-first"), stop = vi.fn();
await expect(stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: Date.now() + 1000, load: async () => f.state(), stop,
retain: async () => { frame(f.events[kind === "native origin" ? 1 : 2]!).emittedAt = "2026-01-01T00:00:01Z"; },
})).rejects.toThrow("pending boundary changed or expired");
expect(stop).not.toHaveBeenCalled();
});
it("never sends Stop to make a missing tool start arrive", async () => {
const f = withArrivalOrder("cursor", "permission-first");
const start = f.events.splice(2, 1)[0]!, retain = vi.fn();
const stop = vi.fn(async () => { f.events.push(start); return f.settle(); });
await expect(stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: Date.now() + 1000, load: async () => f.state(), retain, stop }))
.rejects.toThrow("duplicate canonical operation lifecycle");
expect(retain).not.toHaveBeenCalled(); expect(stop).not.toHaveBeenCalled();
});
it("fails promptly on a definitive provider failure before calling a throwing evidence reader", async () => {
const f = fixture(); const started = Date.now();
await expect(stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: started + 10000, load: async () => f.state(), retain: async () => {}, stop: async (_run, id) => {
@@ -209,7 +286,8 @@ describe("explicit active Stop discovery", () => {
expect(cells).toHaveLength(4); expect(suite.manualOnly).toBe(true);
expect(cells.map(e => `${e.profile.qualificationCandidate}/${e.environment.id}`).sort()).toEqual(["copilot/daytona", "copilot/local", "cursor/daytona", "cursor/local"]);
expect(cells.every(e => e.task.expectedRunCount === 1 && e.task.flow === "native_active_stop" && e.task.expectedTerminalState?.run === "cancelled")).toBe(true);
expect(suite.definitionMetadata).toMatchObject({ version: 2, normalCompletionAccepted: false, providerDeath: "not-covered" });
expect(suite.definitionMetadata).toMatchObject({ version: 3, normalCompletionAccepted: false, providerDeath: "not-covered" });
expect(suiteDefinitionHash(suite)).not.toBe(suiteDefinitionHash({ ...suite, definitionMetadata: { ...suite.definitionMetadata, version: 2 } }));
expect(suiteDefinitionHash(suite)).not.toBe(suiteDefinitionHash({ ...suite, definitionMetadata: { ...suite.definitionMetadata, normalCompletionAccepted: true } }));
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(e => e.suite.id === suite.id)).toBe(false);
});