mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
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:
1 parent
2cbf72ec96
commit
7fe2a2528d
6 files changed
+125
-15
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user