From f285bbbab83b56501c95f8ac813cecafc4596aca Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 9 Aug 2026 03:48:55 +0000 Subject: [PATCH] test(runner): finish phase 6 conformance Exercise all atomic-liveness failpoints, make native cancellation retry-safe, and align the allowlist and verification evidence with the final database-backed matrix. Co-Authored-By: Paperclip --- .../design/phase-6-thin-paperclip-adapter.md | 6 ++++- .../phase-06-thin-paperclip-adapter.md | 2 +- .../2026-08-09-phase-06-verification.md | 7 +++--- .../2026-08-09-phase-06-implementation.md | 4 ++-- .../__tests__/native-run-finalizer.test.ts | 1 + .../native-session-executor.test.ts | 19 +++++++++++++++ .../native-runtime/native-session-executor.ts | 9 +++++-- .../paperclip-control-plane-port.test.ts | 24 ++++++++++++++++--- 8 files changed, 60 insertions(+), 12 deletions(-) diff --git a/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md index 8a73b397d1d..b457e2d0462 100644 --- a/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md @@ -703,14 +703,18 @@ from the provisional list above. They are added without expanding authority: ```text packages/paperclip-runner/src/native-session-runtime.ts packages/paperclip-runner/src/native-session-runtime.test.ts +packages/paperclip-runner/spec/fixtures/status-authority-phase5.json server/src/services/native-runtime/native-session-executor.ts server/src/services/native-runtime/native-session-executor.test.ts +server/src/services/native-runtime/evidence-classifier.test.ts server/src/services/native-runtime/paperclip-control-plane-port.test.ts server/src/services/native-runtime/status-arbiter.test.ts packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md ``` -The package runtime files own persisted provider-session recovery. The server +The package runtime files own persisted provider-session recovery. The corpus +edit moves two existing coverage labels between fixtures without changing any +fixture input or expected outcome, eliminating an unjoinable fixture. The server executor files own only the run-scoped cancellation handle and coordinator lease around that package runtime. The colocated port and arbiter tests exercise the same approved database and pure-policy seams exposed through the named diff --git a/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md index 904f04f0045..760a647de63 100644 --- a/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md @@ -57,7 +57,7 @@ pnpm --filter @paperclipai/server exec vitest run \ ``` This runs the unchanged package conformance suite against the real Paperclip -port. Twenty-five targeted tests check company binding, duplicate/gap replay, +port. Twenty-eight targeted tests check company binding, duplicate/gap replay, checkpoint recovery, immutable result ingestion, durable-evidence authority, workspace-gated finalization, atomic liveness rollback, status-version CAS, migration repair, legacy byte equivalence, and audit output. All database tests diff --git a/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md b/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md index 7140657aa0c..80580eba117 100644 --- a/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md +++ b/packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md @@ -39,7 +39,7 @@ existing accepted result event, and appended only the missing terminal fact. # Real Paperclip adapter and public-session task -The complete acceptance gate below passed 25 tests in ten files against +The complete acceptance gate below passed 28 tests in ten files against embedded PostgreSQL with zero skipped tests. Database-backed tests start their own database instead of skipping when a developer `DATABASE_URL` is absent. The same package conformance suite passed unchanged against @@ -68,10 +68,11 @@ The focused corpus also proved: - a failed workspace barrier preserves the accepted semantic result, leaves the issue status unchanged, and records a leased `retryable_failure` plus recovery owner/action; -- four injected failures prove interactions, wakes, blocker bindings, status, +- six injected failures prove governance binding, interactions, wakes, blocker + bindings, recovery, status, decisions, and effects roll back together before retry ownership is recorded. -The acceptance-matrix entry points also passed as one 10-file, 25-test gate: +The acceptance-matrix entry points also passed as one 10-file, 28-test gate: ```sh pnpm --filter @paperclipai/server exec vitest run \ diff --git a/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md b/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md index f2dfd225cdd..f0a3fd6f149 100644 --- a/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md +++ b/packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md @@ -47,8 +47,8 @@ existing server authority and keep legacy execution as the default. - [Phase 6 reference](../../docs/phase-06-thin-paperclip-adapter.md) - [Runnable tutorial](../../docs/tutorials/phase-06-thin-paperclip-adapter.md) - Package conformance and recovery: four files, six tests passed. -- Phase 6 acceptance-matrix entry points: ten files, twenty-five tests passed - with zero skips, including the database-backed selected-task canary, four +- Phase 6 acceptance-matrix entry points: ten files, twenty-eight tests passed + with zero skips, including the database-backed selected-task canary, six atomic-liveness failpoints, migration, sequencing, bounded recovery, and legacy compatibility. - Focused database rehearsal: five files, eleven tests passed with zero skips; diff --git a/server/src/__tests__/native-run-finalizer.test.ts b/server/src/__tests__/native-run-finalizer.test.ts index c1a2933f37c..d63320d1335 100644 --- a/server/src/__tests__/native-run-finalizer.test.ts +++ b/server/src/__tests__/native-run-finalizer.test.ts @@ -1,3 +1,4 @@ // Acceptance-matrix entry point for evidence, finalization, CAS, and atomic // status/liveness effects exercised by the real database-backed port suite. import "../services/native-runtime/paperclip-control-plane-port.test.js"; +import "../services/native-runtime/evidence-classifier.test.js"; diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 04020f0a3fa..7d1f1a8d719 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -96,4 +96,23 @@ describe("native session cancellation", () => { await running; await expect(cancelNativeSession(execution.binding.runId, "late cancel")).resolves.toBe(false); }); + + it("allows cancellation to be retried when the session dispatch fails", async () => { + state.cancel.mockRejectedValueOnce(new Error("transport unavailable")); + const running = executePaperclipNativeSession({ + db: leaseDb(), + execution, + runnerInstanceId: "runner", + }); + await vi.waitFor(() => expect(state.release).toBeTypeOf("function")); + + await expect(cancelNativeSession(execution.binding.runId, "budget hard stop")) + .rejects.toThrow("transport unavailable"); + await expect(cancelNativeSession(execution.binding.runId, "retry budget stop")) + .resolves.toBe(true); + expect(state.cancel).toHaveBeenNthCalledWith(2, { reason: "retry budget stop" }); + + state.release?.(); + await running; + }); }); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 5771d353571..426ed3cb7d0 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -23,8 +23,13 @@ export async function cancelNativeSession(runId: string, reason: string): Promis if (!active) return false; if (active.cancelRequested) return true; active.cancelRequested = true; - if (active.session.cancel) await active.session.cancel({ reason }); - else if (active.session.interrupt) await active.session.interrupt({ reason }); + try { + if (active.session.cancel) await active.session.cancel({ reason }); + else if (active.session.interrupt) await active.session.interrupt({ reason }); + } catch (error) { + active.cancelRequested = false; + throw error; + } return true; } diff --git a/server/src/services/native-runtime/paperclip-control-plane-port.test.ts b/server/src/services/native-runtime/paperclip-control-plane-port.test.ts index 479c194cbc0..71b2bd6ae1c 100644 --- a/server/src/services/native-runtime/paperclip-control-plane-port.test.ts +++ b/server/src/services/native-runtime/paperclip-control-plane-port.test.ts @@ -43,6 +43,7 @@ describe("PaperclipControlPlanePort conformance", () => { const taskIssueId = "00000000-0000-4000-8000-000000000008"; const taskContractId = "00000000-0000-4000-8000-000000000009"; const taskRunId = "00000000-0000-4000-8000-000000000010"; + const taskSessionId = "00000000-0000-4000-8000-000000000018"; const taskWorkProductId = "00000000-0000-4000-8000-000000000017"; const workspaceFailureIssueId = "00000000-0000-4000-8000-000000000011"; const workspaceFailureContractId = "00000000-0000-4000-8000-000000000012"; @@ -135,6 +136,7 @@ describe("PaperclipControlPlanePort conformance", () => { runtimeMode: "native", runtimeModeResolverVersion: "phase6-v1", runtimeModeReason: "eligible_opt_in", + nativeSessionId: taskSessionId, completionContractId: taskContractId, completionContractSha256: "phase6-task-contract", contextSnapshot: { issueId: taskIssueId }, @@ -297,7 +299,7 @@ describe("PaperclipControlPlanePort conformance", () => { it("completes one selected Paperclip task through the public package session contract", async () => { const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity; - const sessionId = "session-phase6-paperclip-task"; + const sessionId = taskSessionId; const evidenceRef = `work_product:${taskWorkProductId}`; const taskResult = structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT); taskResult.completionClaim.criteria[0]!.evidenceRefs = [evidenceRef]; @@ -558,12 +560,14 @@ describe("PaperclipControlPlanePort conformance", () => { await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, workspaceFailureRunId))).resolves.toHaveLength(1); }); - it("LIVE-01..04 rolls back status, decision, and liveness rows at every materialization failpoint", async () => { + it("LIVE-01..06 rolls back status, decision, and liveness rows at every materialization failpoint", async () => { const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity; const cases: Array<{ suffix: number; - failpoint: "interaction_materialization" | "continuation_materialization" | "blocker_materialization" | "status_projection"; + failpoint: "governance_materialization" | "interaction_materialization" | "continuation_materialization" | "blocker_materialization" | "recovery_materialization" | "status_projection"; result: PrpStructuredRunResult; + terminalState?: "succeeded" | "failed" | "cancelled"; + executionState?: Record; }> = [ { suffix: 20, @@ -593,6 +597,18 @@ describe("PaperclipControlPlanePort conformance", () => { failpoint: "status_projection", result: { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), reportedWorkDisposition: "needs_review" }, }, + { + suffix: 25, + failpoint: "recovery_materialization", + terminalState: "cancelled", + result: structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), + }, + { + suffix: 26, + failpoint: "governance_materialization", + executionState: { status: "pending", stage: "approval" }, + result: structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), + }, ]; for (const entry of cases) { const value = String(entry.suffix).padStart(12, "0"); @@ -606,6 +622,7 @@ describe("PaperclipControlPlanePort conformance", () => { status: "in_progress", assigneeAgentId: identity.agentId, workMode: "standard", + executionState: entry.executionState, }); await db.insert(completionContracts).values({ id: contractId, @@ -634,6 +651,7 @@ describe("PaperclipControlPlanePort conformance", () => { }); const terminal: PrpTerminalState = { ...CONTROL_PLANE_CONFORMANCE_TERMINAL, + runTerminalState: entry.terminalState ?? "succeeded", reportedWorkDisposition: entry.result.reportedWorkDisposition, }; const port = new PaperclipControlPlanePort(db, {