mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-18 15:39:47 +02:00
fix(runner-e2e): match Product cancellation evidence
Require the normalized pending-request closure and cancelled terminal with the existing caller-owned Stop and native evidence guards. Exercise the real Product projection and version the changed settlement oracle. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b858178ad2
commit
b4b261367b
5 files changed
+67
-14
No files matched your search
@@ -421,10 +421,20 @@ both input orders. Neither policy claims the original ACP wire order of a live
|
||||
attempt. 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
|
||||
caller-owned native Stop acknowledgement. Missing, duplicate, foreign, failed,
|
||||
Suite version 4 and `paperclip.e2e.native-active-stop-settlement.v2` accept only
|
||||
`pending_permission_cancelled`: the Product harness's exact
|
||||
`runtime_request.cancelled` closure (`reason: turn_terminal`, matching request,
|
||||
item and turn, no answer), then one exact `turn.cancelled` (`status: cancelled`,
|
||||
`error: null`), plus the same scoped caller-owned native Stop acknowledgement.
|
||||
Both events must belong to the retained source/session/turn and follow all four
|
||||
pre-Stop evidence rows. The harness consumes raw backend request closures and
|
||||
projects its own pending-request outcome before the terminal. Raw backend-only
|
||||
`provider`, `requestType`, `replayAllowed` and `providerTurnId` fields are not
|
||||
required from that Product projection. Replay refusal is independently checked
|
||||
through the stale public answer below. A generic terminal without the retained
|
||||
request and exact acknowledged caller UUID is insufficient. The v2 settlement
|
||||
receipt records this changed oracle; historical v1 receipts and failed attempts
|
||||
retain their original grades. Missing, duplicate, foreign, failed,
|
||||
interrupted or normal terminal evidence fails. Only after this proof does the
|
||||
fixture attempt a stale public answer, requiring HTTP409 and an unanswerable
|
||||
browser card. It retains one cancelled run, issue `in_progress`, exact target
|
||||
|
||||
@@ -1460,6 +1460,15 @@ Native notice ordering follows each real projector: Cursor waits for its exact
|
||||
command origin before emitting permission evidence; Copilot can emit the
|
||||
permission notice first. Both still require the exact tool origin before Stop.
|
||||
|
||||
Suite version 4 uses the normalized Product cancellation contract and writes
|
||||
`paperclip.e2e.native-active-stop-settlement.v2`. It requires the exact request,
|
||||
item and turn closure with `reason: turn_terminal`, followed by the same stream's
|
||||
`turn.cancelled` with `status: cancelled` and `error: null`. The Product harness
|
||||
emits these fields; raw backend-only cancellation metadata is not its contract.
|
||||
The retained pending v2 receipt, caller UUID and durable scoped acknowledgement
|
||||
remain mandatory. This fixes an oracle mismatch observed after a real Stop was
|
||||
acknowledged; the failed attempt remains failed and needs a fresh live run.
|
||||
|
||||
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
|
||||
|
||||
@@ -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: 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" },
|
||||
definitionMetadata: { version: 4, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v2", 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,
|
||||
|
||||
@@ -147,9 +147,14 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru
|
||||
const closed = proof.rows.filter(x => closures.has(x.event.eventType)), terminal = proof.rows.filter(x => terminals.has(x.event.eventType));
|
||||
fail(closed.length === 1 && terminal.length === 1, "one closed request and one terminal required");
|
||||
const c = closed[0]!, t = terminal[0]!, cp = rec(c.event.payload), tp = rec(t.event.payload);
|
||||
// Product persists the harness projection, not the raw runnerd backend
|
||||
// payload. The terminal mapper closes pending permissions with turn_terminal
|
||||
// and emits {status,error}. Bind that exact boundary to the retained native
|
||||
// request and caller-owned Stop below; a generic terminal is insufficient.
|
||||
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
|
||||
&& cp.requestKind === "permission_approval" && cp.reason === "turn_terminal"
|
||||
&& cp.itemId === proof.request.itemId && cp.action === undefined && cp.response === undefined && cp.replayAllowed !== true
|
||||
&& t.event.eventType === "turn.cancelled" && tp.status === "cancelled" && tp.error === null
|
||||
&& 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");
|
||||
@@ -166,7 +171,7 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru
|
||||
&& Array.isArray(stop.effects) && stop.effects.length === 1 && stop.effects[0] === "release_run_resources"
|
||||
&& id(stop.intentAuditId) && id(stop.acknowledgementAuditId) && stop.intentAuditId !== stop.acknowledgementAuditId,
|
||||
"same-scope caller Stop acknowledgement missing");
|
||||
return { schema: "paperclip.e2e.native-active-stop-settlement.v1", pending: before, dispatchMonotonicNs: input.dispatchMonotonicNs,
|
||||
return { schema: "paperclip.e2e.native-active-stop-settlement.v2", pending: before, dispatchMonotonicNs: input.dispatchMonotonicNs,
|
||||
branch: "pending_permission_cancelled" as const, closedRequestSourceSeq: c.event.sourceSeq, terminalSourceSeq: t.event.sourceSeq,
|
||||
closedRequestRowSha256: hash(c.row), terminalRowSha256: hash(t.row), intentId: stop.intentId as string,
|
||||
intentAuditId: stop.intentAuditId as string, acknowledgementAuditId: stop.acknowledgementAuditId as string,
|
||||
|
||||
@@ -8,6 +8,9 @@ import { createCursorToolEvidence } from "../../packages/paperclip-runner/src/dr
|
||||
import { createCopilotToolEvidence } from "../../packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.js";
|
||||
import type { CanonicalProviderEvent } from "../../packages/paperclip-runner/src/provider-events.js";
|
||||
|
||||
import { CodexSessionState } from "../../packages/paperclip-runner/src/drivers/codex/codex-session-state.js";
|
||||
import { mapTerminalTurn } from "../../packages/paperclip-runner/src/drivers/codex/codex-session-terminal.js";
|
||||
|
||||
const caller = readActiveStopCaller({ deploymentMode: "local_trusted" }, { session: { userId: "local-board", id: "paperclip:local_implicit:local-board" } });
|
||||
const cancellationRequestId = "11111111-2222-4333-8444-555555555555";
|
||||
type Row = Record<string, any>;
|
||||
@@ -26,7 +29,7 @@ function fixture(provider: ActiveStopProvider = "copilot") {
|
||||
notice(1, "tool", { status: "pending", ...operation }),
|
||||
row(2, "tool.execution.started", { schema: "paperclip.tool.execution.v1", executionId: "tool", transport: "builtin", status: "running", ...operation }),
|
||||
notice(3, "permission_requested", { requestId: "request", declineOffered: true, ...operation }),
|
||||
row(4, "runtime_request.created", { request: { schema: "paperclip.runtime_request.v2", requestKind: "permission_approval", type: "permission", status: "pending", requestId: "request", turnId: "turn", itemId: "tool",
|
||||
row(4, "runtime_request.created", { request: { schema: "paperclip.runtime_request.v2", requestKind: "permission_approval", type: "permission", status: "pending", requestId: "request", turnId: "turn", itemId: "item-run", method: "item/commandExecution/requestApproval",
|
||||
details: { toolCallId: "tool" }, origin: { adapter: "acpx-runtime-sidecar", provider, method: "session/request_permission" } } }),
|
||||
];
|
||||
const run: Row = { id: "run", companyId: "company", nativeIssueId: "issue", runtimeMode: "native", status: "running", resultJson: {} };
|
||||
@@ -34,8 +37,17 @@ function fixture(provider: ActiveStopProvider = "copilot") {
|
||||
const state = () => ({ events, run, issue });
|
||||
const pending = () => observeActiveStopPending({ ...state(), scope, caller, cancellationRequestId });
|
||||
const settle = (requestId = cancellationRequestId) => {
|
||||
events.push(row(5, "runtime_request.cancelled", { provider: "acpx", requestId: "request", turnId: "turn", requestKind: "permission_approval", requestType: "permission", itemId: "tool", reason: "explicit_cancellation", replayAllowed: false }),
|
||||
row(6, "turn.cancelled", { provider: "acpx", providerTurnId: "turn", status: "cancelled", error: null }));
|
||||
// Exercise the actual Product terminal and pending-request producers. Only
|
||||
// the state storage/emitter and callback are test doubles; no provider runs.
|
||||
const request = events.find(event => event.eventType === "runtime_request.created")!.payload.prpEvent.payload.request;
|
||||
const state = {
|
||||
terminalTurns: new Map(), workspaceChangesByTurn: new Map(), result: null,
|
||||
conversationMode: "direct", activeTurnId: "turn", turnStarted: true,
|
||||
pendingRuntimeRequestMap: new Map([[request.requestId, { request, settle: vi.fn() }]]),
|
||||
cancelPendingRequests: CodexSessionState.prototype.cancelPendingRequests,
|
||||
emit: (eventType: string, value: Row) => events.push(row(events.length + 1, eventType, value)),
|
||||
} as unknown as CodexSessionState;
|
||||
mapTerminalTurn(state, { id: "turn", status: "cancelled", error: null }, "turn");
|
||||
run.status = "cancelled"; run.resultJson = { startupCancellation: { cancellationRequestId: requestId, requestedBy: { type: "board", userId: "local-board" } }, nativeCancellation: {
|
||||
schema: "paperclip.native-cancellation.v1", intentId: `native-cancellation:${requestId}`, companyId: "company", runId: "run", issueId: "issue", scope: "run",
|
||||
dispatched: true, dispatchState: "acknowledged", reasonCode: "cancellation_run_only", effects: ["release_run_resources"], intentAuditId: "audit-intent", acknowledgementAuditId: "audit-ack",
|
||||
@@ -203,10 +215,20 @@ describe("definitely active native permission Stop", () => {
|
||||
it.each(["cursor", "copilot"] as const)("binds %s's unanswered callback to cancelled provider settlement and caller-owned Stop", provider => {
|
||||
const f = fixture(provider), pending = f.pending(); f.settle();
|
||||
expect(readActiveStopSettlement({ ...f.state(), pending, dispatchMonotonicNs: (BigInt(pending.observedMonotonicNs) + 1n).toString() })).toMatchObject({
|
||||
schema: "paperclip.e2e.native-active-stop-settlement.v1", branch: "pending_permission_cancelled", normalCompletionAccepted: false, taskStillInProgress: true, replayAllowed: false,
|
||||
schema: "paperclip.e2e.native-active-stop-settlement.v2", branch: "pending_permission_cancelled", normalCompletionAccepted: false, taskStillInProgress: true, replayAllowed: false,
|
||||
pending: { normalizedSessionId: "normalized-session", nativeSessionId: "native-session", cancellationRequestId },
|
||||
});
|
||||
});
|
||||
it("matches the retained Product closure shape with distinct native and item identities", () => {
|
||||
const { f, pending, read } = settled();
|
||||
expect(pending.toolCallId).toBe("tool");
|
||||
// Content-free shape captured from the failed Product attempt. These exact
|
||||
// payloads come from mapTerminalTurn -> cancelPendingRequests above.
|
||||
expect(payload(f.events[4]!)).toEqual({ requestId: "request", requestKind: "permission_approval",
|
||||
turnId: "turn", itemId: "item-run", reason: "turn_terminal" });
|
||||
expect(payload(f.events[5]!)).toEqual({ status: "cancelled", error: null });
|
||||
expect(read().schema).toBe("paperclip.e2e.native-active-stop-settlement.v2");
|
||||
});
|
||||
it("admits only a fully attested remote bootstrap read completed before the pending operation", () => {
|
||||
const f = fixture(), actionFile = `.paperclip-eval-action-${"a".repeat(36)}.txt`;
|
||||
const readTargetSha256 = `sha256:${createHash("sha256").update(actionFile).digest("hex")}`;
|
||||
@@ -249,6 +271,13 @@ describe("definitely active native permission Stop", () => {
|
||||
["foreign request", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).requestId = "other"; }],
|
||||
["expired rather than cancelled", ({ f }: ReturnType<typeof settled>) => { f.events[4]!.eventType = frame(f.events[4]!).eventType = "runtime_request.expired"; }],
|
||||
["provider death", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).reason = "provider_process_lost"; }],
|
||||
["raw backend closure is not the Product contract", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).reason = "explicit_cancellation"; }],
|
||||
["answered closure", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).action = "accept"; }],
|
||||
["resolved request", ({ f }: ReturnType<typeof settled>) => { f.events[4]!.eventType = frame(f.events[4]!).eventType = "runtime_request.resolved"; }],
|
||||
["duplicate closure", ({ f }: ReturnType<typeof settled>) => { f.events.push(f.row(7, "runtime_request.cancelled", payload(f.events[4]!))); }],
|
||||
["foreign closure turn", ({ f }: ReturnType<typeof settled>) => { payload(f.events[4]!).turnId = "other"; }],
|
||||
["missing acknowledgement", ({ f }: ReturnType<typeof settled>) => { delete f.run.resultJson.nativeCancellation.acknowledgementAuditId; }],
|
||||
["unacknowledged intent", ({ f }: ReturnType<typeof settled>) => { f.run.resultJson.nativeCancellation.dispatchState = "pending"; }],
|
||||
["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 = ""; }],
|
||||
@@ -345,8 +374,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: 3, normalCompletionAccepted: false, providerDeath: "not-covered" });
|
||||
expect(suiteDefinitionHash(suite)).not.toBe(suiteDefinitionHash({ ...suite, definitionMetadata: { ...suite.definitionMetadata, version: 2 } }));
|
||||
expect(suite.definitionMetadata).toMatchObject({ version: 4, evidence: "paperclip.e2e.native-active-stop-settlement.v2", normalCompletionAccepted: false, providerDeath: "not-covered" });
|
||||
expect(suiteDefinitionHash(suite)).not.toBe(suiteDefinitionHash({ ...suite, definitionMetadata: { ...suite.definitionMetadata, version: 3 } }));
|
||||
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