From 1e0cfcb4fc4ed78d10f46cd60990935af4ac9973 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:44:02 -0500 Subject: [PATCH] test: retire redundant suites and consolidate owner coverage (#15978) Retire duplicate ACPX and Codex proof after migrating unique assertions to their behavioral owners. Consolidate repeated fixtures and exact case matrices while preserving lifecycle, protocol, company, credential, approval, and ordering contracts. Remove the unused runtime-controls compatibility path and document test ownership and preservation evidence. Co-Authored-By: Paperclip --- doc/plans/2026-10-12-test-audit.md | 404 ++ feature-map/questions-and-approvals.md | 2 +- .../adapter-utils/src/acpx-engine/AGENTS.md | 22 + .../composed-run-characterization.test.ts | 876 ---- .../src/acpx-engine/execute.test.ts | 319 +- .../src/acpx-engine/run-fault-matrix.test.ts | 102 +- .../settlement-characterization.test.ts | 1164 ----- .../startup-characterization.test.ts | 917 ---- .../acpx-engine/turn-characterization.test.ts | 686 --- .../src/execution-target-sandbox.test.ts | 1431 ++---- .../src/execution-target.test.ts | 131 +- .../src/sandbox-managed-runtime.test.ts | 23 + .../paperclip-runner/docs/durable-recovery.md | 2 +- .../docs/live-console-protocol-server.md | 2 +- .../paperclip-runner/docs/tutorials/codex.md | 2 +- .../tutorials/live-console-protocol-server.md | 4 +- .../src/drivers/codex/AGENTS.md | 21 + ...-app-server-driver.context-control.test.ts | 235 +- .../codex-app-server-driver.recovery.test.ts | 25 + ...app-server-driver.semantic-results.test.ts | 156 + .../codex/codex-app-server-driver.test.ts | 4153 ----------------- .../daytona/src/plugin.test.ts | 34 - .../shared/src/slack-app-manifest.test.ts | 6 + .../chat-channels.integration.test.ts | 3006 +++--------- .../heartbeat-process-recovery.test.ts | 389 +- ...ue-interactions-read-only-contract.test.ts | 79 - .../issue-thread-interaction-routes.test.ts | 9 +- server/src/__tests__/issues-service.test.ts | 175 - .../src/__tests__/tool-access-service.test.ts | 167 +- server/src/__tests__/tool-gateway.test.ts | 13 + .../src/__tests__/workspace-runtime.test.ts | 103 - .../native-session-executor.test.ts | 2473 ++++------ .../native-session-resume.test.ts | 315 +- .../native-runtime/runner-api.test.ts | 26 +- tests/runner-e2e/stock-harness-checks.mjs | 1 - ui/src/api/agents.test.ts | 34 +- ui/src/api/chatEndpoints.test.ts | 70 - .../WorkspaceRuntimeControls.test.tsx | 133 - .../components/WorkspaceRuntimeControls.tsx | 57 +- .../task-chat/TaskChatProtocolCard.test.tsx | 89 +- ui/src/pages/CompanyEnvironments.test.tsx | 108 +- ui/src/pages/Secrets.render.test.tsx | 496 +- .../pages/apps/chat/chat-ui-contract.test.ts | 30 - 43 files changed, 3460 insertions(+), 15030 deletions(-) create mode 100644 doc/plans/2026-10-12-test-audit.md create mode 100644 packages/adapter-utils/src/acpx-engine/AGENTS.md delete mode 100644 packages/adapter-utils/src/acpx-engine/composed-run-characterization.test.ts delete mode 100644 packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts delete mode 100644 packages/adapter-utils/src/acpx-engine/startup-characterization.test.ts delete mode 100644 packages/adapter-utils/src/acpx-engine/turn-characterization.test.ts create mode 100644 packages/paperclip-runner/src/drivers/codex/AGENTS.md delete mode 100644 packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts delete mode 100644 server/src/__tests__/issue-interactions-read-only-contract.test.ts diff --git a/doc/plans/2026-10-12-test-audit.md b/doc/plans/2026-10-12-test-audit.md new file mode 100644 index 00000000000..90031bb3aba --- /dev/null +++ b/doc/plans/2026-10-12-test-audit.md @@ -0,0 +1,404 @@ +# Test audit: remove redundant proof and repeated fixtures + +Date: 2026-10-12 +Baseline: `fb21a3f30a` +Reference: [OpenClaw test audit](https://github.com/openclaw/openclaw/blob/main/.agents/skills/test-audit/SKILL.md) + +The combined cleanup removes **11,976 net test/support lines, 41 unused +production lines, and 1,073 registered cases**. Four obsolete ACPX +characterization files and the old Codex driver monolith are retired. Unique +contracts remain in their current owners. The latest round passes all 1,421 +affected/sibling cases, with caught mutations and restored passing controls. +Workspace typecheck and build pass. The previous round's completed owner and +chat proof is recorded separately below. This is a reviewed subset of the +repository, not an exhaustive review or a complete default-suite run. + +## PR delivery verification + +The cleanup is published in [PR #15978](https://github.com/paperclipai/paperclip/pull/15978). +The three cleanup commits rebased onto `914b70572c` without changing their +patches. At code revision `454e3a91ff`, fresh `pnpm -r typecheck` and +`pnpm build` pass. + +The local `pnpm test:run` started at that code revision. During its first +general-server invocation, one unchanged tool-access case timed out at +15,035ms: `rejects unavailable Workspace gmail.read (other profile advertised: +true)`. The invocation was stopped after the reported failure and exited 130. +It did not complete the 882 general-server files or reach the remaining +workspace and 158 serialized suites. **There is no full local default-suite +pass.** The timing discrepancy remains unresolved; earlier passes alone do +not establish its cause. + +Commit `37eb57418b` only corrects a coverage-guide link to the retained HTTP +route suite. It arrived during the local default invocation, so that run is +not an immutable run of the later head, although its executable sources are +unchanged. The complete tool-access owner then runs at immutable +`37eb57418b` and passes all **419 cases**, including the timed-out matrix row, +with its existing assertions, fixtures and timeout. UI token gates also pass. +No test was skipped, removed or given a longer budget in response to the failure. + +At `37eb57418b`, all remote CI and security checks pass, Greptile completes a +fresh **5/5** review, and there are zero unresolved review threads. The review +fix and this delivery record change Markdown only. Check the PR for the final +head and its required CI/review results; the local failure remains disclosed +in the PR verification section. + +## Second expansion + +Baseline: `8538e024d7`. Refreshed `origin/master` remains at `fb21a3f30a`. +Three auditors nominated owner batches; original-source and proposal SHA-256 +guards were checked before cutover. This round changes tests, one test-gate +file selector, documentation, and scoped test ownership guidance. It changes +zero application source lines. + +| Owner batch | Net test/support lines removed | Net registered cases removed | +| --- | ---: | ---: | +| Codex monolith retirement and test-gate selector | 3,780 | 64 | +| Sandbox/SSH execution target fixtures | 976 | 0 | +| Tool request/audit and heartbeat recovery consolidation | 367 | 4 | +| Issue workspace inheritance duplicates | 175 | 2 | +| **New round** | **5,298** | **70** | +| Earlier rounds | 6,678 | 1,003 | +| **Combined** | **11,976** | **1,073** | + +These figures exclude Markdown. The Codex row contains 3,779 test lines and +one removed prerequisite-selector line. The earlier 41 application lines +were the unused WorkspaceRuntimeControls compatibility path; no additional +application code was removed in this round. No measured speedup is claimed. + +### New retired proof and keepers + +- Codex: `06f8773097` (#12371) split the driver tests but retained the + 4,153-line monolith. Its 70 declarations have 56 exact keepers, four + evolved stronger keepers, and ten unique migrations. All parameter rows + survive. Move tool refresh to recovery; continuation/effort/controls/goals + to context-control; direct-chat tools and unknown outcomes to + semantic-results. Preserve exact host-HOME equality as well as descendant + denial, the -32004 goal-policy denial and exact availability event, and + stronger diff/path and rejected/missing-result contracts. The monolith's + 76 registered cases become 12 migrated rows; the nine-file baseline has + 214 cases and the eight retained owners have 150. +- Test-gate routing: remove only SH-1's stale monolith filename. Its + existing lifecycle keeper still proves additive instructions. Required + patterns, source-digest receipts, other prerequisites and grading stay. + Update four docs to reachable suite filters/owners. The runner package + has its own Vitest config and is not in the root project list. +- Adapter fixtures: consolidate fresh roots/workspaces, repeated sandbox + targets, bridge defaults and SSH targets. Expand the actual new helpers + with their actual arguments/defaults and compare the entire executable + AST to the committed originals. Every case, all 523 expectation statements, + custom runner, body limit, timeout, callback, await and cleanup remains. + Helpers allocate fresh objects/runners; all 41 converted bridge calls + resolve target.remoteCwd to the original local remoteCwd expression. +- Tool requests: combine two audit-only cases with their exact same + behavioral request/actor/policy owners. Move both complete activity and + dedicated-audit assertion blocks. Preserve the missing-ID HTTP 404 in + the existing live lifecycle. Repair the misleading non-test-origin + negative, whose random missing ID never reached the origin guard, using + the real persisted agent-owned request in the connected-MCP gateway case. + Its company/connection/agent/run/issue preconditions reach the intended guard. +- Heartbeat: combine the weaker no-state accepted-plan recovery case with + the dropped-accept-time-wake keeper. Carry every full-context/retry + assertion. Consolidate only identical DB seed rows in five retained + siblings; retain historical-run, answered-question, board authority and + post-resolution timing scenarios. +- Issues: remove the second exact inheritance and explicit-override + declarations. Both groups initialize the same real DB/service/schema and + per-case experimental inputs. Keep every unique generic-project, + cross-project, source-inheritance, update and runtime-config case. + +### Second expansion preservation and validation + +Independent reviews cover all four boundary groups. They found no remaining +preservation blockers. Codex review independently compares all 70 mappings: +66 callback/table/budget ASTs are identical under the one workspace alias, +including the ten migrations; four evolved keepers preserve/strengthen the +original contracts. Eight shared helpers match; the fake transport's existing +optional providerIdentity is absent in these cases. The existing disposable, +canonical workspace preserves cwd identity and containment tests. + +Adapter helper expansion, issue full-file comparison after the two exact +deletions, server audit-block comparison and recovery helper expansion retain +all surrounding executable statements. No generated expectation, fake owner +receipt, broad skip or longer timeout replaces behavioral proof. + +- Baseline: **1,459 cases pass** across all nominated complete files and the + stock-harness gate test. The initial sandboxed attempt hit localhost + `listen EPERM` and consequent skips; unrestricted fixture runs pass every case. +- Candidate: **1,211 server/adapter cases, 182 Codex/transport/console cases, + and 28 stock-harness cases pass**, zero failed/skipped cases. The 182 include + all 150 retained Codex/diff owners plus 32 transport/console siblings. +- Mutation: bypass only getTestCallStatus's origin clause. The real persisted + request resolves and the new rejection assertion fails. Lookup/company/ + connection preconditions remain valid. Restore source byte for byte; control + passes. +- Mutation: misclassify Codex goal policy denial as unsupported. The retained + real availability-event assertion fails. Restore source byte for byte; + control passes. +- Workspace `pnpm -r typecheck`, `pnpm build`, runner E2E-support typecheck, + and `git diff --check` pass. No UI source changed in this round. +- The full default repository suite remains unverified; this is complete + affected-owner/sibling proof. A full default run is still required before + claiming a full local pass. These commits were local at the end of that + round; later PR verification is recorded above. + +### Retained second-round nominees + +Keep native-session-runtime, runnerd transport, ACPX translation and browser +model suites: they exercise distinct lifecycle, authentication, protocol or +separate implementation owners. Keep sandbox queue versus real HTTP2 layers, +credential/stream/order guards, and unrelated tool/company/security cases. +No additional full-layer retirement was justified in those boundaries. + +### Codex declaration ledger + +Each exact case name identifies its protected contract; callback/table preservation was checked against the committed baseline. D deletes redundant old proof; C moves or consolidates it. + +| Old line / exact case | Mark | Keeper | Evidence | +| --- | --- | --- | --- | +| 330: refreshes authorized tools on resume without replacing the provider thread | C move | `codex-app-server-driver.recovery.test.ts:1305` | identical body/table under cwd alias | +| 433: parses a complete Codex turn diff snapshot with bounded file statistics | D | `codex-turn-diff.test.ts:17` | identical body/table under cwd alias | +| 466: bounds aggregate Codex diffs and rejects unsafe workspace paths | C stronger keeper | `codex-turn-diff.test.ts:50` | explicit superset/migration above | +| 496: persists process ownership before sending provider requests | D | `codex-app-server-driver.lifecycle.test.ts:265` | identical body/table under cwd alias | +| 533: sends direct chat as plain text and permits a follow-up turn | D | `codex-app-server-driver.lifecycle.test.ts:417` | identical body/table under cwd alias | +| 579: forwards the persisted native model to the runner transport | D | `codex-app-server-driver.lifecycle.test.ts:463` | identical body/table under cwd alias | +| 600: adds Paperclip developer instructions without replacing the Codex base and enables selected skills | D | `codex-app-server-driver.lifecycle.test.ts:486` | identical body/table under cwd alias | +| 630: passes the common typed-event contract and reports one provider turn terminal | D | `codex-app-server-driver.lifecycle.test.ts:574` | identical body/table under cwd alias | +| 845: admits a strictly bound semantic result from the durable runner | D | `codex-app-server-driver.events.test.ts:47` | identical body/table under cwd alias | +| 879: correlates rehydrated notifications with the final canonical PRP event ids | D | `codex-app-server-driver.events.test.ts:81` | identical body/table under cwd alias | +| 935: maps canonical workspace snapshots with monotonic revisions and one terminal diff | D | `codex-app-server-driver.events.test.ts:137` | identical body/table under cwd alias | +| 1052: finalizes an authoritative empty workspace snapshot when a turn is interrupted | D | `codex-app-server-driver.events.test.ts:254` | identical body/table under cwd alias | +| 1095: accepts a thread usage snapshot replayed before a resumed turn starts | D | `codex-app-server-driver.events.test.ts:319` | identical body/table under cwd alias | +| 1129: normalizes provider message and reasoning phases onto every streamed item event | D | `codex-app-server-driver.events.test.ts:353` | identical body/table under cwd alias | +| 1264: captures an exact skillless model/environment snapshot with credentials absent | D | `codex-app-server-driver.context-control.test.ts:82` | identical body/table under cwd alias | +| 1356: pins approval policy %s for both thread start and resume | D | `codex-app-server-driver.context-control.test.ts:174` | identical body/table under cwd alias | +| 1383: requires trusted continuation metadata before omitting task context (%s) | C move | `codex-app-server-driver.context-control.test.ts:692` | identical body/table under cwd alias | +| 1402: allows eval fixtures to opt out of Codex collaboration instructions | D | `codex-app-server-driver.context-control.test.ts:201` | identical body/table under cwd alias | +| 1423: negotiates genuine plan mode with collaboration instructions and read-only workspace access | D | `codex-app-server-driver.context-control.test.ts:222` | identical body/table under cwd alias | +| 1466: sends the selected reasoning effort with the Codex turn | C move | `codex-app-server-driver.context-control.test.ts:711` | identical body/table under cwd alias | +| 1478: fails closed when the installed app-server does not confirm plan mode | D | `codex-app-server-driver.context-control.test.ts:265` | identical body/table under cwd alias | +| 1492: refuses to turn host credential roots into model-writable workspaces | C stronger keeper | `codex-app-server-driver.context-control.test.ts:279` | explicit superset/migration above | +| 1528: uses negotiated ACP controls through the Codex transport facade | C move | `codex-app-server-driver.context-control.test.ts:723` | identical body/table under cwd alias | +| 1549: keeps native follow-up disabled for the Codex app-server driver | C move | `codex-app-server-driver.context-control.test.ts:744` | identical body/table under cwd alias | +| 1560: steers and interrupts an active turn without replacing the session | D | `codex-app-server-driver.context-control.test.ts:315` | identical body/table under cwd alias | +| 1590: loads the deterministic Live console wire fixture | D | `codex-app-server-driver.context-control.test.ts:345` | identical body/table under cwd alias | +| 1612: holds browser-resolved upstream requests and returns the exact fixture responses | D | `codex-app-server-driver.context-control.test.ts:367` | identical body/table under cwd alias | +| 1673: acknowledges same-turn steering and rejects stale or child steering | D | `codex-app-server-driver.context-control.test.ts:428` | identical body/table under cwd alias | +| 1792: queues an interrupt before turn identity and reports a terminal race precisely | D | `codex-app-server-driver.context-control.test.ts:547` | identical body/table under cwd alias | +| 1827: maps all goal operations and advertises an exact unsupported state | C stronger keeper | `codex-app-server-driver.context-control.test.ts:582` | explicit superset/migration above | +| 1907: keeps goal support advertised when the probe fails transiently | D | `codex-app-server-driver.context-control.test.ts:662` | identical body/table under cwd alias | +| 1937: preserves an inactive goal status while editing its objective | C move | `codex-app-server-driver.context-control.test.ts:755` | identical body/table under cwd alias | +| 1964: rolls back only a definitely rejected idle goal autostart | C move | `codex-app-server-driver.context-control.test.ts:782` | identical body/table under cwd alias | +| 2014: preserves a provider-neutral goal action subset through the Codex transport facade | C move | `codex-app-server-driver.context-control.test.ts:832` | identical body/table under cwd alias | +| 2059: validates runtime request resolutions against the kind of request they answer | D | `codex-app-server-driver.runtime-requests.test.ts:47` | identical body/table under cwd alias | +| 2176: replays the DOT-185 requestUserInput shape as one canonical three-question request | D | `codex-app-server-driver.runtime-requests.test.ts:164` | identical body/table under cwd alias | +| 2282: rejects an explicit malformed Codex form without falling back to v1 | D | `codex-app-server-driver.runtime-requests.test.ts:317` | identical body/table under cwd alias | +| 2324: emits one canonical outcome payload for every terminal runtime request fact | D | `codex-app-server-driver.runtime-requests.test.ts:359` | identical body/table under cwd alias | +| 2410: expires a canonical input with its full question set when the provider transport is lost | D | `codex-app-server-driver.runtime-requests.test.ts:445` | identical body/table under cwd alias | +| 2469: hands a live canonical input off exactly once when the durable window expires | D | `codex-app-server-driver.runtime-requests.test.ts:504` | identical body/table under cwd alias | +| 2551: exposes and dispatches only explicit chat tools across fresh and resumed direct chat | C move | `codex-app-server-driver.semantic-results.test.ts:866` | identical body/table under cwd alias | +| 2683: lets an answer claimed before expiry win the terminal-event race | D | `codex-app-server-driver.runtime-requests.test.ts:644` | identical body/table under cwd alias | +| 2758: redacts browser-visible request details and diagnostics from the fixture markers | D | `codex-app-server-driver.runtime-requests.test.ts:719` | identical body/table under cwd alias | +| 2796: makes duplicate semantic completion idempotent and rejects changed payloads | D | `codex-app-server-driver.semantic-results.test.ts:151` | identical body/table under cwd alias | +| 2873: accepts a normalized runnerd echo of a tool-committed semantic result | D | `codex-app-server-driver.semantic-results.test.ts:226` | identical body/table under cwd alias | +| 2928: advertises and dispatches run-authorized control-plane tools | D | `codex-app-server-driver.semantic-results.test.ts:288` | identical body/table under cwd alias | +| 2988: preserves the dynamic tool outcome boundary (%s) | C move | `codex-app-server-driver.semantic-results.test.ts:998` | identical body/table under cwd alias | +| 3011: fails closed when an agent message changes a tool-committed result | D | `codex-app-server-driver.semantic-results.test.ts:348` | identical body/table under cwd alias | +| 3058: rejects a tool result that changes an agent-message commitment | D | `codex-app-server-driver.semantic-results.test.ts:450` | identical body/table under cwd alias | +| 3109: treats a canonically identical cross-channel result as a no-op | D | `codex-app-server-driver.semantic-results.test.ts:501` | identical body/table under cwd alias | +| 3153: fails closed when a live terminal embeds a changed semantic result | D | `codex-app-server-driver.semantic-results.test.ts:545` | identical body/table under cwd alias | +| 3205: rejects a semantic result for $label without committing it | D | `codex-app-server-driver.semantic-results.test.ts:597` | identical body/table under cwd alias | +| 3244: rejects pre-turn, cross-thread, and post-terminal notifications | D | `codex-app-server-driver.semantic-results.test.ts:636` | identical body/table under cwd alias | +| 3328: rejects duplicate and conflicting terminal facts | D | `codex-app-server-driver.semantic-results.test.ts:720` | identical body/table under cwd alias | +| 3362: rejects oversized semantic results without retaining provider payloads | D | `codex-app-server-driver.semantic-results.test.ts:754` | identical body/table under cwd alias | +| 3404: normalizes finish and block tools into one canonical result contract | D | `codex-app-server-driver.semantic-results.test.ts:796` | identical body/table under cwd alias | +| 3475: resumes and reconciles the exact provider thread after transport loss | D | `codex-app-server-driver.recovery.test.ts:297` | identical body/table under cwd alias | +| 3507: ignores an old terminal turn when the persisted active turn is still running | D | `codex-app-server-driver.recovery.test.ts:365` | identical body/table under cwd alias | +| 3548: preserves proposal idempotency and rejects a changed result after transport loss | D | `codex-app-server-driver.recovery.test.ts:406` | identical body/table under cwd alias | +| 3632: rejects changed terminal result content discovered during recovery | D | `codex-app-server-driver.recovery.test.ts:490` | identical body/table under cwd alias | +| 3703: does not re-emit an already observed terminal after recovery | D | `codex-app-server-driver.recovery.test.ts:561` | identical body/table under cwd alias | +| 3758: fails recovery explicitly when the persisted active turn is missing or replaced | D | `codex-app-server-driver.recovery.test.ts:1186` | identical body/table under cwd alias | +| 3799: reconciles a turn completed during transport loss without replacing either session identity | D | `codex-app-server-driver.recovery.test.ts:1227` | identical body/table under cwd alias | +| 3877: degrades unsupported operations with explicit redacted diagnostics | D | `codex-app-server-driver.conformance.test.ts:47` | identical body/table under cwd alias | +| 3912: rejects proposals that do not match the exact task envelope | D | `codex-app-server-driver.conformance.test.ts:82` | identical body/table under cwd alias | +| 3947: keeps a completed runtime successful for an accepted %s advisory proposal | D | `codex-app-server-driver.conformance.test.ts:117` | identical body/table under cwd alias | +| 4001: records rejected and missing proposals as recovery evidence instead of review status | C stronger keeper | `codex-app-server-driver.conformance.test.ts:171` | explicit superset/migration above | +| 4033: preserves four independent stable identities under controller ownership | D | `codex-app-server-driver.conformance.test.ts:210` | identical body/table under cwd alias | +| 4054: validates persisted identity, uniqueness, terminals, and line bounds before replay | D | `codex-app-server-driver.conformance.test.ts:231` | identical body/table under cwd alias | +| 4103: degrades declared unsupported capabilities without Codex-specific core branches | D | `codex-app-server-driver.conformance.test.ts:280` | identical body/table under cwd alias | + +## Aggressive follow-up + +The follow-up baseline is `3951d69f9b`, which contains the small first pass. +`origin/master` was refreshed and remained at `fb21a3f30a`. Three read-only +auditors examined adapter lifecycle, native execution, and UI ownership. +Proposals were prepared outside the checkout while baseline tests ran, then +applied with original-source SHA-256 guards. + +| Owner batch | Net test/support lines removed | Production lines removed | Net registered cases removed | +| --- | ---: | ---: | ---: | +| ACPX characterization layer | 3,283 | 0 | 44 | +| Chat database read fixtures | 1,612 | 0 | 0 | +| Native executor, resume, and runner API | 802 | 0 | 943 | +| UI fixture consolidation and obsolete compatibility | 698 | 41 | 7 | +| Exact workspace/Daytona duplicate cases | 137 | 0 | 2 | +| Initial pass | 146 | 0 | 7 | +| **Total** | **6,678** | **41** | **1,003** | + +These figures exclude documentation. Savings are measured from the Git diff, +not formatting estimates. The case counts include migrated/table-expanded +keepers, not just deleted declarations. No wall-time speedup is attributed to +the change: baseline and candidate runs had different cache and machine load. + +### Retired layers and their keepers + +- ACPX: remove startup, turn, settlement, and composed-run characterization + suites (3,643 gross lines). They copied the engine harness for #11461's + refactor; #11576 completed the coordinator extraction. The current + `execute.test.ts`, fault matrix, sequence, cancellation, session-store, and + workspace teardown suites own their remaining proof. Unique Stop-phase + regressions from #14990 remain, with actual callback/gate observations. + Preserve bridge credential isolation, current prompt/turn inputs, fingerprint + dimensions, retry order, close/cancel policy, bridge-stop before sync-back, + lease release, and native restore/last-finalize assertions in those keepers. +- Chat: replace 607 repeated real database query chains with 22 typed, + file-local query owners. Every table, column, parameter, caller await, + ordering, limit, polling loop, action, and assertion remains. Independent + expansion of the helpers produces a normalized AST identical to the entire + original program. All 1,237 integration cases execute before and after. +- Native: consolidate repeated scoped-runtime fixtures while retaining every + executor declaration and parameter row, including 1,010 complete assertion + chains. Replace six copied historical catalog constructors with independently + checked immutable digests in the existing resume table. Restore the valid + context before testing changed agent identity so that the negative cannot + pass through an earlier, unrelated instruction mismatch. +- Runner API: remove the second identical REST-route matrix. Baseline + collection executes 943 rows twice; the keeper executes each same row once. + The full file changes from 1,994 to 1,051 passing cases. No catalog entries + or public route/actor/response contract are removed. +- UI: remove five exact duplicate cases and two tests of an internal legacy + workspace API with no production callers or published source export. Both + shipped workspace pages use the retained `sections` interface. Consolidate + Secrets rendering, fresh Daytona capabilities, and four literal runtime + status rows without changing their actions, waits, or outcomes. Two AWS + error negatives now actually receive the sensitive provider details they + assert are absent. +- Workspace/Daytona: remove a second byte-equivalent worktree dependency case + and an overlapping-execute case whose body duplicated the retained lease + tombstone test. All other provisioning and teardown guards remain. + +### Preservation review and final proof + +Independent reviewers checked each of the four large owner proposals against +its original tests and production callers. Review caught and repaired lost +existing-file overwrite proof and a prompt guard that could accept +`String(undefined)`. Both fingerprints in the dimension matrix and the exact +session-key keeper now also require nonempty strings. No remaining preservation +blocker was found. + +Three additional production mutations were caught at assertions: dropping the +runtime prompt, skipping existing-file writes during native restore, and +spreading unsafe secret-provider details into the UI. Every production file was +restored byte for byte. The two AWS negative cases fail on the unsafe-details +mutation. All four focused controls pass after restoration. The earlier HTTP +mutation proof remains below. + +- Complete affected owners and siblings: **22 files, 2,817 cases pass**. +- Complete chat integration: **1,237 cases pass**, both baseline and candidate. +- ACPX baseline: 335 cases pass; native/UI baseline: 939 pass; runner API + baseline: 1,994 pass. Additional workspace/Daytona/UI baseline records 508 + passes and 10 pending cases with a failed workspace file but no failed case. + That baseline inconsistency is not claimed repaired; the complete retained + workspace file subsequently passes all 175 cases with built prerequisites. +- `pnpm -r typecheck`, `pnpm build`, token gates, and diff whitespace checks pass. +- The full default repository suite remains unverified. The completed tests + above are full files at the affected owners; they are not a claim that all + repository tests ran. A full default run remains required for a PR-ready + handoff. + +## Initial pass evidence + +### Scope and findings + +This is a repository inventory and a small evidence-backed cleanup, not a review +of every test. The baseline has 2,816 tracked `*.test.*` / `*.spec.*` files: +1,038 server, 725 UI, 697 packages, 216 test harnesses, 66 CLI, 50 scripts, +21 GitHub tooling, and 3 skill tooling files. These include opt-in suites; +the count does not describe the default Vitest run alone. + +A text scan found 382 files that combine file reads with containment/matching +assertions: 156 packages, 133 server, 42 harnesses, 18 UI, 13 CLI, 13 scripts, +6 GitHub tooling, and 1 skill tooling file. The scan also matches fixture reads +and executable checks. **382 is a review queue, not a deletion estimate.** + +The useful question is which independent regression a test catches that the +retained owner proof misses. File reads, snapshots, mocks, and slow tests are +not sufficient reasons to remove a test. + +### Applied decisions + +| Candidate | Decision and retained proof | +| --- | --- | +| `server/src/__tests__/issue-interactions-read-only-contract.test.ts`: route source check | Remove the file after adding terminal-expiry and activity-write assertions to the existing real HTTP read-path test in `issue-thread-interaction-routes.test.ts`. Check both `in_review` and `done`, the JSON response, listing, and absence of recovery. | +| Same file: two whitespace rows, three forbidden-name rows, two missing-boundary rows | Remove all seven synthetic cases. They test a local regex/assertion helper against hand-written route strings, not production behavior. Removing the helper removes their purpose. | +| Retry cases in `ui/src/api/chatEndpoints.test.ts` and `agents.test.ts` | Consolidate under `agents.test.ts`, the production API owner. Preserve the exact company-scoped request, ordinary and durable receipts, distinct stopped-successor errors, deferred dispatch, legacy receipts, both skipped-wake forms, and single-dispatch checks. | +| Slack manifest replay in `ui/src/pages/apps/chat/chat-ui-contract.test.ts` | Remove duplicate scope/event assertions already pinned by the shared owner snapshots. Move the unique agent-view, bot presence, and app-home assertions to the existing shared manifest test. Keep the UI's manifest integration checks. | +| Same UI file: sanitizer implementation substrings | Remove the checks for `replaceAll` and `encodeURIComponent`. Keep the UI integration assertions and `chat-setup-error.test.ts`, which exercises raw, escaped, encoded, and malformed UTF-16 secrets. | +| Same UI file: v8 wireframe generator/import strings | Remove the design-plan implementation checks. Keep the shipped GitHub setup assertions, wizard behavior suite, and PEM file/late-read tests. Design generator version names are not a product contract. | + +The source-only route test originated in #10414; its synthetic parser cases +arrived in #13038, alongside the real HTTP test. The UI retry coverage came from +#13038 and #13765. The current chat source-check owner was last changed in +#15416. None of the removed support code has a production caller. + +Change size before this report: **146 net test lines removed**, one test file +retired, seven fewer executed cases across the affected route suites, and zero +production lines changed. No runtime speedup is claimed: cold transforms, +prerequisite builds, and a baseline timeout prevent a fair wall-time comparison. + +### Preservation evidence + +- UI baseline: 48 cases pass across the two API files, chat UI source contract, + chooser routing, and setup-error suite. The shared manifest has four cases. +- After cleanup: those six UI/shared suites pass, 52 cases total. +- Route baseline: the eight source-check cases pass; 84 of 85 HTTP cases pass. + The unchanged standard-work plan-confirmation case times out at 15 seconds + during the full file run. Isolating it with the original read-path case passes + both. This does not establish a flake repair; its test and timeout remain intact. +- Three temporary mutations add historical expiry, terminal expiry, or an + activity write to the production GET handler. Each makes the retained HTTP + test fail at its side-effect assertion. Production source is restored byte + for byte after every mutation, including failure cleanup. +- UI token gates and `git diff --check` pass. + +- Final complete HTTP route file: all 86 cases pass. Together with the 52 + UI/shared cases, focused validation passes 138 cases. +- `pnpm -r typecheck` and `pnpm build` pass. +- `pnpm test:run` was started, then interrupted after about five minutes while + still running its first general-server file, the 75,178-line chat integration + suite. It exited 130; there is no full-suite pass or failure conclusion. + The default runner has 158 separately serialized suites after its general + lanes. A complete run remains a prerequisite for claiming a PR-ready handoff. + +### Next review boundaries + +1. Remaining `chat-ui-contract.test.ts` assertions: exact retry call syntax, + tabs, lifecycle handlers, and provider fields. Replace them only after + rendered tests exercise the selected run, navigation, error, or credential + outcome. Provider guidance and permission bytes need independent proof. +2. Component-only helper exports: `shouldRenderComposerHandoffPreview`, + `replaceResolvedInteraction`, and model-transition helpers. Their direct + tests currently protect distinct behavior. The existing rendered suites do + not cover every assertion, so deletion is not yet justified. +3. Server source guards: compare each with its route/service owner before + pruning. Authentication ordering and company boundaries deserve particular + care; a source guard can protect routes missing a behavioral negative case. + +Retain protocol-schema cross-checks, package/build metadata, migration replay, +dependency singleton checks, provider permission snapshots, and theme contrast +tests. They compare independent contracts or outcomes. The telemetry README +guard is also retained; changing that path requires its own strict review. + +For another batch, pin a fresh baseline, assign one production boundary, +record every deletion's keeper first, and prove the keeper catches the relevant +mutation. Do not optimize for test counts or remove baseline failures for green. diff --git a/feature-map/questions-and-approvals.md b/feature-map/questions-and-approvals.md index 0446cd11c30..32b611d3211 100644 --- a/feature-map/questions-and-approvals.md +++ b/feature-map/questions-and-approvals.md @@ -148,7 +148,7 @@ Real provider and model checks follow [Task reviews](../doc/connections/TASK-REV ### `skill-test` -Automated: [read-only interaction contract](../server/src/__tests__/issue-interactions-read-only-contract.test.ts) +Automated: [read-only interaction contract](../server/src/__tests__/issue-thread-interaction-routes.test.ts) checks that fetching interactions does not mutate them. Shared card suites cover rendering. Neither proves the complete Skill Studio host or its permission matrix. diff --git a/packages/adapter-utils/src/acpx-engine/AGENTS.md b/packages/adapter-utils/src/acpx-engine/AGENTS.md new file mode 100644 index 00000000000..fc6ac1a780d --- /dev/null +++ b/packages/adapter-utils/src/acpx-engine/AGENTS.md @@ -0,0 +1,22 @@ +# ACPX lifecycle test ownership + +The engine's primary behavioral boundary is `createAcpxEngineExecutor` in +`execute.test.ts`. `run-fault-matrix.test.ts` owns resource acquisition/failure +outcomes. Sequence, cancellation, session-store, and workspace restore tests +retain their separate ordering and persistence contracts. + +- Extend the existing harness or literal case table before creating another + full executor/bridge/sandbox mock graph. Temporary characterization suites + must have a cutover plan; migrate their unique assertions when the refactor + finishes rather than keeping both layers indefinitely. +- A provider adapter suite needs a distinct provider configuration, credential, + prompt, parser, or transport contract. Do not replay shared lifecycle proof + merely under different adapter names. +- Verify actual close, cancel, bridge-stop, restore, and lease-release callbacks + when guarding physical cleanup. Ledger dispositions alone do not establish + that the cleanup ran or that ordering is correct. +- Keep stalled-phase/Stop tests at the live engine boundary. Pure sequence + tests cannot replace callback wiring while real awaits are held open. +- Before retiring a layer, map every unique assertion to a keeper, obtain an + independent preservation review, and use focused mutations for repaired gaps. + Never remove a baseline failure to make the suite green. diff --git a/packages/adapter-utils/src/acpx-engine/composed-run-characterization.test.ts b/packages/adapter-utils/src/acpx-engine/composed-run-characterization.test.ts deleted file mode 100644 index 67e4e4df061..00000000000 --- a/packages/adapter-utils/src/acpx-engine/composed-run-characterization.test.ts +++ /dev/null @@ -1,876 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AcpRuntimeOptions } from "acpx/runtime"; -import type { AdapterExecutionContext, AdapterRuntimeMcpAccess } from "@paperclipai/adapter-utils"; -import { - prepareAdapterExecutionTargetRuntime, - startAdapterExecutionTargetPaperclipBridge, - startAdapterExecutionTargetProcessSessionBridge, -} from "@paperclipai/adapter-utils/execution-target"; - -// This file is a characterization test. It pins the engine boundary's CURRENT -// behavior; it never changes production code. Each test states the observed -// contract of `executeAcpxEngine` as data, so a later refactor that alters an -// exit path's result form, its finalization set, or its warm-lane decision -// fails here. -// -// Adapter-boundary automatic CLI fallback note (File 1b): the codex, claude, and -// gemini adapters wrap this engine and fall back to their CLI lane when an -// AUTOMATIC ACP selection fails, but RETHROW when ACP was EXPLICITLY selected. -// Those three adapters live in higher packages that depend on this one, so an -// adapter-utils test cannot import them (a reverse dependency does not exist in -// package.json). That branch already has REAL coverage in each adapter package: -// - packages/adapters/codex-local/src/server/execute.acp-fallback.test.ts -// - packages/adapters/claude-local/src/server/execute.acp-fallback.test.ts -// - packages/adapters/gemini-local/src/server/execute.acp-fallback.test.ts -// and each adapter's `normalizeEngine` / `resolveXxxExecutionEngineForRun` -// classification (explicit acp/cli vs automatic acp) is pinned in each -// package's acp.test.ts. This file pins the engine-level exit paths those -// adapters wrap. - -// Wrap the staging seam + both sandbox bridges in call-recording spies that -// still delegate to the real implementations (a runner-backed sandbox test -// exercises them end-to-end against a local runner). This lets the staging -// tests assert the exact `runtimeRootDir`/`workspaceLocalDir`/`assets` the -// engine threads without changing any real behavior for the other tests. -vi.mock("@paperclipai/adapter-utils/execution-target", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - prepareAdapterExecutionTargetRuntime: vi.fn(actual.prepareAdapterExecutionTargetRuntime), - startAdapterExecutionTargetPaperclipBridge: vi.fn(actual.startAdapterExecutionTargetPaperclipBridge), - startAdapterExecutionTargetProcessSessionBridge: vi.fn(actual.startAdapterExecutionTargetProcessSessionBridge), - }; -}); -import { - createAcpxEngineExecutor, - type AcpxEngineExecutorOptions, -} from "./execute.js"; -import { runChildProcess } from "../server-utils.js"; - -const tempRoots: string[] = []; - -async function makeTempRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-")); - tempRoots.push(root); - return root; -} - -afterEach(async () => { - // A remote run stages a process-session bridge whose detached event writer can - // still be flushing a trailing event file into `.../process-sessions//events` - // when the run's own best-effort `client.remove(sessionDir)` (which production - // catch-wraps) has already returned. Under CI load that trailing write can land - // between this recursive delete's directory snapshot and its `rmdir`, surfacing as - // `ENOTEMPTY`. `maxRetries`/`retryDelay` make the cleanup ride out that window the - // same way production tolerates it, instead of failing the just-passed test. - await Promise.all( - tempRoots.splice(0).map((root) => - fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }), - ), - ); -}); - -function createLocalSandboxRunner( - onExecute?: (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - }) => void, -) { - let counter = 0; - return { - execute: async (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - stdin?: string; - timeoutMs?: number; - onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - onSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }) => { - counter += 1; - onExecute?.(input); - const command = input.command === "bash" ? "/bin/bash" : input.command; - return await runChildProcess(`acpx-sandbox-run-${counter}`, command, input.args ?? [], { - cwd: input.cwd ?? process.cwd(), - env: input.env ?? {}, - stdin: input.stdin, - timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)), - graceSec: 5, - onLog: input.onLog ?? (async () => {}), - onSpawn: input.onSpawn - ? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt }) - : undefined, - }); - }, - }; -} - -function buildRuntime( - onSetConfigOption?: (input: { key: string; value: string }) => void, - onEnsureSession?: (input: Record) => void, -) { - return { - ensureSession: async (input: Record) => { - onEnsureSession?.(input); - return ({ - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }); - }, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }), - setConfigOption: async (input: { key: string; value: string }) => { - onSetConfigOption?.(input); - }, - close: async () => {}, - }; -} - -async function runExecutor( - config: Record, - options: { - context?: Record; - executionTransport?: Record; - authToken?: string; - executionTarget?: Record; - runtimeMcp?: AdapterRuntimeMcpAccess; - prepareRemoteManagedHome?: AcpxEngineExecutorOptions["prepareRemoteManagedHome"]; - startupTraceContext?: AdapterExecutionContext["startupTraceContext"]; - } = {}, -) { - const runtimeOptions: Record[] = []; - const configOptions: Array<{ key: string; value: string }> = []; - const sessionInputs: Record[] = []; - const meta: Record[] = []; - const logs: Array<{ stream: string; text: string }> = []; - const events: Array<{ eventType: string; payload?: Record }> = []; - const execute = createAcpxEngineExecutor({ - ...(options.prepareRemoteManagedHome - ? { prepareRemoteManagedHome: options.prepareRemoteManagedHome } - : {}), - createRuntime: (options) => { - runtimeOptions.push(options as unknown as Record); - return buildRuntime( - ({ key, value }) => configOptions.push({ key, value }), - (input) => sessionInputs.push(input), - ) as never; - }, - }); - - const result = await execute({ - runId: "run-1", - agent: { - id: "agent-1", - companyId: "company-1", - }, - runtime: {}, - config, - context: options.context ?? {}, - executionTransport: options.executionTransport, - authToken: options.authToken, - executionTarget: options.executionTarget, - runtimeMcp: options.runtimeMcp, - startupTraceContext: options.startupTraceContext, - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - onMeta: async (payload: unknown) => { - meta.push(payload as Record); - }, - onEvent: async (event: { eventType: string; payload?: Record }) => { - events.push(event); - }, - } as never); - - expect(result.exitCode).toBe(0); - return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result }; -} - -// A remote sandbox setup that stages the host worktree through the real local -// runner, so a run reaches the post-build window with live bridges and a held -// staging lease. -async function setupRemoteSandbox() { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const localCwd = path.join(root, "worktree"); - const remoteCwd = path.join(root, "remote-workspace"); - await fs.mkdir(localCwd, { recursive: true }); - await fs.mkdir(remoteCwd, { recursive: true }); - await fs.writeFile(path.join(localCwd, "hello.txt"), "hi", "utf8"); - const executionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "fake-plugin", - remoteCwd, - runner: createLocalSandboxRunner(), - }; - return { root, stateDir, localCwd, remoteCwd, executionTarget }; -} - -const okHandle = { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", -}; - -function completedTurn() { - return { - events: (async function* () {})(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }; -} - -function throwingTurn() { - return { - events: (async function* () { - throw new Error("turn upstream boom"); - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }; -} - -// A context whose only prompt-read field throws when read, so the prompt build -// fails after the session handshake succeeds and before the turn starts. -function throwingHandoffContext(): Record { - const context: Record = {}; - Object.defineProperty(context, "paperclipSessionHandoffMarkdown", { - enumerable: false, - get() { - throw new Error("prompt build boom"); - }, - }); - return context; -} - -// Stub both sandbox bridges with stop spies collected per start, so a test can -// assert the bridges stopped without running the real bridge transport. -function stubBridges() { - const paperclipStops: Array> = []; - const processStops: Array> = []; - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => { - const stop = vi.fn(async () => {}); - paperclipStops.push(stop); - return { env: {}, stop } as never; - }); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementation(async () => { - const stop = vi.fn(async () => {}); - processStops.push(stop); - return { agentCommand: null, stop } as never; - }); - const anyStopped = (stops: Array>) => - stops.some((stop) => stop.mock.calls.length > 0); - const stoppedCount = (stops: Array>) => - stops.filter((stop) => stop.mock.calls.length > 0).length; - return { paperclipStops, processStops, anyStopped, stoppedCount }; -} - -function remoteArgs( - stateDir: string, - localCwd: string, - executionTarget: unknown, - overrides: Record = {}, -) { - return { - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - context: {}, - authToken: "real-run-jwt", - executionTarget, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - ...overrides, - }; -} - -describe("composed ACPX run: engine-boundary result form per exit path", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("returns (never throws) a create_runtime error result when the post-build runtime construction fails", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => { - throw new Error("createRuntime boom"); - }, - }); - - // The engine RETURNS a settled error result on this path; it does not throw. - const result = await execute({ - runId: "boundary-create-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.signal).toBe(null); - expect(result.timedOut).toBe(false); - expect(result.resultJson?.phase).toBe("create_runtime"); - expect(result.summary).toContain("createRuntime boom"); - }); - - it("returns an ensure_session error result when the handshake fails", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => { - throw new Error("ensureSession boom"); - }, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - }); - - const result = await execute({ - runId: "boundary-ensure-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("ensure_session"); - }); - - it("returns a runtime-error result with no clearSession and no errorMeta when ensureSession yields no handle", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - // A runtime that returns no session handle drives the missing-handle path. - ensureSession: async () => undefined, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - }); - - const result = await execute({ - runId: "boundary-missing-handle", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("ensure_session"); - expect(result.errorCode).toBe("acpx_runtime_error"); - // The missing-handle result carries neither clearSession nor errorMeta, unlike - // the other pre-turn error paths. - expect(result.clearSession).toBeUndefined(); - expect(result.errorMeta).toBeUndefined(); - }); - - it("returns a configure_session error result that carries the requested session fields", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - // A custom agent with a requested model applies a session config option; - // a throwing setConfigOption drives the configure_session failure path. - setConfigOption: async () => { - throw new Error("setConfigOption boom"); - }, - close: async () => {}, - }) as never, - }); - - const result = await execute({ - runId: "boundary-configure-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { - agent: "custom", - agentCommand: "node ./fake-acp.js", - model: "custom-model-x", - stateDir: path.join(root, "state"), - }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("configure_session"); - // The configure_session result echoes the requested session identity fields. - expect(result.resultJson).toHaveProperty("agent"); - expect(result.resultJson?.requestedModel).toBe("custom-model-x"); - expect(result.resultJson).toHaveProperty("requestedThinkingEffort"); - expect(result.resultJson).toHaveProperty("fastMode"); - }); - - it("returns a prepare_turn error result when the prompt build fails before the turn starts", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - }); - - const result = await execute({ - runId: "boundary-prepare-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: throwingHandoffContext(), - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("prepare_turn"); - }); - - it("returns a turn error result with acpx_turn_failed when the running turn fails", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => throwingTurn(), - close: async () => {}, - }) as never, - }); - - const result = await execute({ - runId: "boundary-turn-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("turn"); - expect(result.errorCode).toBe("acpx_turn_failed"); - }); - - it("returns the terminal turn-success shape (the one non-error-shaped return)", async () => { - const root = await makeTempRoot(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => buildRuntime() as never, - }); - - const result = await execute({ - runId: "boundary-success", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - // A completed, non-timed-out turn returns exitCode 0, no signal, no errorCode, - // and the session identity fields, with resultJson.status = "completed". - expect(result.exitCode).toBe(0); - expect(result.signal).toBe(null); - expect(result.timedOut).toBe(false); - expect(result.errorCode).toBe(null); - expect(result.resultJson?.status).toBe("completed"); - expect(result.sessionId).toBe("backend-session"); - expect(result.sessionDisplayId).toBe("agent-session"); - expect(result.sessionParams).toBeTruthy(); - }); - - it("THROWS (does not return) when buildRuntime fails before it settles", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - // A staging failure throws inside buildRuntime before it settles, so the engine - // rethrows through the `if (!buildRuntimeSettled) throw err` guard. This is one - // of exactly two engine-boundary throw paths. - vi.mocked(prepareAdapterExecutionTargetRuntime).mockImplementationOnce(async () => { - throw new Error("staging boom"); - }); - const execute = createAcpxEngineExecutor({ - warmHandles: new Map(), - stagedRuntimes: new Map(), - stagingLocks: new Map(), - createRuntime: () => buildRuntime() as never, - }); - - await expect( - execute({ - runId: "boundary-build-throw", - ...remoteArgs(stateDir, localCwd, executionTarget), - } as never), - ).rejects.toThrow("staging boom"); - }); - - it("THROWS (does not return) on a partial bridge failure and stops the started sibling bridge once", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - // The paperclip bridge fails while the concurrently-started process-session - // bridge resolves a live handle. This is the second engine-boundary throw path; - // the abandon path must stop the started sibling so no bridge leaks. - const stop = vi.fn(async () => {}); - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementationOnce(async () => { - throw new Error("paperclip bridge boom"); - }); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementationOnce( - async () => ({ agentCommand: null, stop }) as never, - ); - const execute = createAcpxEngineExecutor({ - warmHandles: new Map(), - stagedRuntimes: new Map(), - stagingLocks: new Map(), - createRuntime: () => buildRuntime() as never, - }); - - await expect( - execute({ - runId: "boundary-bridge-throw", - ...remoteArgs(stateDir, localCwd, executionTarget), - } as never), - ).rejects.toThrow("paperclip bridge boom"); - expect(stop).toHaveBeenCalledTimes(1); - }); -}); - -describe("composed ACPX run: finalization set fires exactly once per exit path", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("stops both bridges once and releases the staging lease on every exit path", async () => { - const scenarios: Array<{ - name: string; - createRuntime: AcpxEngineExecutorOptions["createRuntime"]; - config?: Record; - context?: Record; - }> = [ - { - name: "create_runtime", - createRuntime: () => { - throw new Error("createRuntime boom"); - }, - }, - { - name: "ensure_session", - createRuntime: () => - ({ - ensureSession: async () => { - throw new Error("ensure boom"); - }, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - }, - { - name: "configure_session", - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - setConfigOption: async () => { - throw new Error("config boom"); - }, - close: async () => {}, - }) as never, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", model: "custom-model-x" }, - }, - { - name: "prepare_turn", - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - context: throwingHandoffContext(), - }, - { - name: "turn", - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => throwingTurn(), - close: async () => {}, - }) as never, - }, - { - name: "success", - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close: async () => {}, - }) as never, - }, - ]; - - for (const scenario of scenarios) { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const { paperclipStops, processStops, stoppedCount } = stubBridges(); - const stagingLocks = new Map>(); - const execute = createAcpxEngineExecutor({ - stagingLocks, - warmHandles: new Map(), - stagedRuntimes: new Map(), - createRuntime: scenario.createRuntime, - }); - - const overrides: Record = {}; - if (scenario.config) { - overrides.config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - cwd: localCwd, - ...scenario.config, - }; - } - if (scenario.context) overrides.context = scenario.context; - - await execute({ - runId: `finalize-${scenario.name}`, - ...remoteArgs(stateDir, localCwd, executionTarget, overrides), - } as never).catch(() => {}); - - // The finalization set fires exactly once: each bridge stops once and the - // per-session staging lease releases, so the lock map never strands the next - // same-session run. - expect(stoppedCount(paperclipStops), `paperclip bridge must stop once on ${scenario.name}`).toBe(1); - expect(stoppedCount(processStops), `process-session bridge must stop once on ${scenario.name}`).toBe(1); - expect(stagingLocks.size, `lease must release on ${scenario.name}`).toBe(0); - } - }); - - it("does not re-run the turn teardown when the result mapping throws after the close", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const { paperclipStops, processStops, stoppedCount } = stubBridges(); - let closeCount = 0; - const execute = createAcpxEngineExecutor({ - warmHandles: new Map(), - stagedRuntimes: new Map(), - stagingLocks: new Map(), - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - // A completed turn whose result mapping throws after the close is read. - result: Promise.resolve({ - status: "completed", - get stopReason(): string { - throw new Error("mapping boom"); - }, - }), - cancel: async () => {}, - }), - close: async () => { - closeCount += 1; - }, - }) as never, - }); - - await execute({ - runId: "finalize-map-throw", - ...remoteArgs(stateDir, localCwd, executionTarget), - } as never).catch(() => {}); - - // The completed turn closed the runtime once; the mapping throw did not re-run - // the teardown through the turn catch. - expect(closeCount).toBe(1); - expect(stoppedCount(paperclipStops)).toBe(1); - expect(stoppedCount(processStops)).toBe(1); - }); -}); - -describe("composed ACPX run: host-lane warm handle set", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("closes and relaunches a persistent local runtime, so the next compatible run re-creates it", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let createCount = 0; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: () => { - createCount += 1; - return buildRuntime() as never; - }, - }); - const config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - mode: "persistent", - warmHandleIdleMs: 60_000, - }; - - const first = await execute({ - runId: "warm-save-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - expect(first.exitCode).toBe(0); - // Amendment B: a persistent local completed turn closes and relaunches the - // runtime instead of warm-saving it (the run-minted API key is never revoked). - expect(warmHandles.size).toBe(0); - expect(createCount).toBe(1); - - const second = await execute({ - runId: "warm-save-2", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: first.sessionParams }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - expect(second.exitCode).toBe(0); - // No warm runtime survived, so the compatible second run re-creates it. - expect(createCount).toBe(2); - }); - - it("closes (does not warm-save) a completed non-persistent runtime, so the next run re-creates", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let createCount = 0; - let closeCount = 0; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: () => { - createCount += 1; - return { - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - setConfigOption: async () => {}, - close: async () => { - closeCount += 1; - }, - } as never; - }, - }); - // No persistent mode, so the completed turn closes the runtime instead of - // warm-saving it. - const config = { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }; - - const first = await execute({ - runId: "warm-none-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - expect(first.exitCode).toBe(0); - expect(warmHandles.size).toBe(0); - expect(closeCount).toBe(1); - - const second = await execute({ - runId: "warm-none-2", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: first.sessionParams }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - expect(second.exitCode).toBe(0); - // No warm handle survived, so the second run constructs a fresh runtime. - expect(createCount).toBe(2); - }); - - it("runs a fresh acp.handshake on the second run and re-creates the runtime (no warm reuse)", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let createCount = 0; - const warmHandles = new Map(); - const secondEvents: Array<{ eventType: string; payload?: Record }> = []; - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: () => { - createCount += 1; - return buildRuntime() as never; - }, - }); - const config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - mode: "persistent", - warmHandleIdleMs: 60_000, - }; - const first = await execute({ - runId: "warm-hit-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - - await execute({ - runId: "warm-hit-2", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: (first as { sessionParams?: unknown }).sessionParams }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async (event: { eventType: string; payload?: Record }) => { - secondEvents.push(event); - }, - } as never); - - // Amendment B: the first run closed and relaunched, so no warm handle survives. - // The second run runs a fresh acp.handshake (outcome ok, not skipped) and - // re-creates the runtime. - const handshakeEvents = secondEvents.filter( - (event) => event.eventType === "run.startup.step" && event.payload?.step === "acp.handshake", - ); - expect(handshakeEvents).toHaveLength(1); - expect(handshakeEvents[0]!.payload?.outcome).not.toBe("skipped"); - expect(createCount).toBe(2); - }); -}); diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index 704e0643cd3..78fd4ebfcf3 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -392,6 +392,54 @@ const ALLOWED_TURN_SPAN_ATTRIBUTE_KEYS = new Set([ ]); describe("shared ACPX engine runtime behavior", () => { + it("passes the current prompt, session, cancellation and diagnostic inputs to the runtime turn", async () => { + const root = await makeTempRoot(); + const { result, turnInputs, meta } = await runExecutor({ + agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state"), + cwd: root, timeoutSec: 120, + }); + expect(turnInputs).toHaveLength(1); + expect(turnInputs[0]).toMatchObject({ + handle: { backendSessionId: "backend-session", agentSessionId: "agent-session", runtimeSessionName: "runtime-session" }, + text: meta[0]?.prompt, mode: "prompt", requestId: "run-1", timeoutMs: 120_000, + onTerminalSessionFailure: expect.any(Function), + }); + expect(meta[0]?.prompt).toBeTypeOf("string"); + expect(turnInputs[0]?.text).toBeTypeOf("string"); + expect((meta[0]?.prompt as string).length).toBeGreaterThan(0); + expect((turnInputs[0]?.text as string).length).toBeGreaterThan(0); + expect(turnInputs[0]?.signal).toBeInstanceOf(AbortSignal); + expect((turnInputs[0]?.signal as AbortSignal).aborted).toBe(false); + expect(result.summary).toBe("end_turn"); + expect(result.sessionParams?.configFingerprint).toBeTypeOf("string"); + expect((result.sessionParams?.configFingerprint as string).length).toBeGreaterThan(0); + expect(result.sessionParams?.sessionKey).toBe(`paperclip:company-1:agent-1:default:${result.sessionParams?.configFingerprint}`); + }); + + it.each(["agentCommand", "cwd", "model", "thinkingEffort", "mode", "fastMode"] as const)( + "folds configured %s into the engine session fingerprint", + async (dimension) => { + const root = await makeTempRoot(); + const config = { + agent: dimension === "fastMode" ? "codex" : "custom", agentCommand: "node ./fake-acp.js", + stateDir: path.join(root, "state"), cwd: root, + env: { CODEX_HOME: path.join(root, "codex-home") }, + paperclipRuntimeSkills: [], paperclipSkillSync: { desiredSkills: [] }, + }; + const changed: Record = { + agentCommand: "node ./other-acp.js", cwd: path.join(root, "other-workspace"), model: "test-model", + thinkingEffort: "high", mode: "oneshot", fastMode: true, + }; + const first = await runExecutor(config); + const second = await runExecutor({ ...config, [dimension]: changed[dimension] }); + expect(first.result.sessionParams?.configFingerprint).toBeTypeOf("string"); + expect(second.result.sessionParams?.configFingerprint).toBeTypeOf("string"); + expect((first.result.sessionParams?.configFingerprint as string).length).toBeGreaterThan(0); + expect((second.result.sessionParams?.configFingerprint as string).length).toBeGreaterThan(0); + expect(second.result.sessionParams?.configFingerprint).not.toBe(first.result.sessionParams?.configFingerprint); + }, + ); + it("integrates the env-free store while delivering current credentials and options to the provider", async () => { const root = await makeTempRoot(); const stateDir = path.join(root, "state"); @@ -1002,10 +1050,11 @@ describe("shared ACPX engine runtime behavior", () => { events: (async function* () { yield { type: "text_delta", - text: "streamed hello", + text: " streamed ", stream: "output", tag: "agent_message_chunk", }; + yield { type: "text_delta", text: "hello ", stream: "output", tag: "agent_message_chunk" }; yield { type: "done", stopReason: "end_turn" }; })(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -1031,15 +1080,13 @@ describe("shared ACPX engine runtime behavior", () => { } as never); expect(result.exitCode).toBe(0); - expect(logs).toContainEqual({ - stream: "stdout", - text: `${JSON.stringify({ - type: "acpx.text_delta", - text: "streamed hello", - channel: "output", - tag: "agent_message_chunk", - })}\n`, - }); + expect(result.summary).toBe("streamed hello"); + for (const text of [" streamed ", "hello "]) { + expect(logs).toContainEqual({ + stream: "stdout", + text: `${JSON.stringify({ type: "acpx.text_delta", text, channel: "output", tag: "agent_message_chunk" })}\n`, + }); + } }); it("defaults run summaries to the final output segment without thought text", async () => { @@ -1619,6 +1666,11 @@ describe("shared ACPX engine runtime behavior", () => { } as never); expect(result.exitCode).not.toBe(0); + if (status === "failed") { + expect(result.errorCode).toBe("acpx_turn_failed"); + expect(result.summary).toBe("fixture failure"); + expect(result.resultJson?.stopReason).toBe("fixture failure"); + } expect(result.usageComplete).toBe(false); expect(onUsage).toHaveBeenLastCalledWith(expect.objectContaining({ complete: false, costUsd: 0.31 })); expect(result.usage).toEqual({ inputTokens: 40, outputTokens: 700, cachedInputTokens: 60 }); @@ -3266,6 +3318,7 @@ describe("gemini ACP flag selection", () => { "Run exceeded the adapter execution timeout (timeoutSec=1, configured via adapterConfig.timeoutSec). " + "Set adapterConfig.timeoutSec to raise it."; expect(result.timedOut).toBe(true); + expect(result.signal).toBe("SIGTERM"); expect(result.errorCode).toBe("acpx_timeout"); expect(result.resultJson?.adapterExecutionTimeout).toEqual({ timeoutSec: 1, source: "configured" }); expect(result.errorMessage).toBe(expectedMessage); @@ -3531,8 +3584,10 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () = // Decode the process-session LAUNCH payload (the base64 command blob) — the // in-sandbox process env is carried there, NOT in the exec's own `env`. let launchPayload: Record | null = null; + let bridgeExecEnv: Record | undefined; (executionTarget as { runner: unknown }).runner = createLocalSandboxRunner((input) => { if (input.env?.PAPERCLIP_SANDBOX_EXEC_CHANNEL === "bridge") { + bridgeExecEnv = input.env; const script = input.args?.[1] ?? ""; const match = script.match(/PAPERCLIP_PROCESS_SESSION_COMMAND_B64='([^']+)'/); if (match) { @@ -3567,6 +3622,8 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () = expect(String(payloadEnv.PAPERCLIP_API_URL ?? "")).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); expect(payloadEnv.PAPERCLIP_API_KEY).toBeTruthy(); expect(payloadEnv.PAPERCLIP_API_KEY).not.toBe("real-run-jwt"); + expect(bridgeExecEnv?.PAPERCLIP_SANDBOX_EXEC_CHANNEL).toBe("bridge"); + expect(bridgeExecEnv?.PAPERCLIP_API_KEY).toBeUndefined(); }); it("publishes referenced-project workspace hints repointed at their staged sandbox directories", async () => { @@ -3703,12 +3760,17 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () = expect(sessionInputs[0]?.cwd).toBe(remoteCwd); }); - it("test_remote_warm_handle_reused_after_cwd_change", async () => { + it("recreates the remote runtime while resuming after the cwd is staged", async () => { const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox(); const ensureInputs: Array> = []; + let created = 0; + const warmHandles = new Map(); const execute = createAcpxEngineExecutor({ - warmHandles: new Map(), - createRuntime: () => buildRuntime(undefined, (input) => ensureInputs.push(input)) as never, + warmHandles, + createRuntime: () => { + created += 1; + return buildRuntime(undefined, (input) => ensureInputs.push(input)) as never; + }, }); const base = { agent: { id: "agent-1", companyId: "company-1" }, @@ -3736,6 +3798,9 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () = expect(first.exitCode).toBe(0); expect(second.exitCode).toBe(0); + expect(created).toBe(2); + expect(warmHandles.size).toBe(0); + expect(ensureInputs).toHaveLength(2); // Both runs resolve session/new to the in-sandbox cwd... expect(ensureInputs[0]?.cwd).toBe(remoteCwd); expect(ensureInputs[1]?.cwd).toBe(remoteCwd); @@ -3868,6 +3933,7 @@ describe("ACPX engine remote managed-home seam (PR 2: per-adapter home seed)", ( // The seam's asset was threaded through the shared staging seam... const stageArgs = vi.mocked(prepareAdapterExecutionTargetRuntime).mock.calls[0]![0]; + expect(stageArgs.workspaceLocalDir).toBe(localCwd); expect(stageArgs.assets).toEqual([ { key: "home", localDir: managedHomeDir, followSymlinks: true }, ]); @@ -6259,10 +6325,11 @@ describe("ACPX engine per-step startup timing (run.startup.step events)", () => const root = await makeTempRoot(); const stateDir = path.join(root, "state"); const warmHandles = new Map(); + let created = 0; const secondEvents: Array<{ eventType: string; payload?: Record }> = []; const execute = createAcpxEngineExecutor({ warmHandles, - createRuntime: () => buildRuntime() as never, + createRuntime: () => { created += 1; return buildRuntime() as never; }, }); const config = { agent: "custom", @@ -6300,6 +6367,7 @@ describe("ACPX engine per-step startup timing (run.startup.step events)", () => const handshakeEvents = secondEvents.filter( (event) => event.eventType === "run.startup.step" && event.payload?.step === "acp.handshake", ); + expect(created).toBe(2); expect(handshakeEvents).toHaveLength(1); expect(handshakeEvents[0]!.payload?.outcome).not.toBe("skipped"); }); @@ -6837,6 +6905,229 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)" }; } + it.each(["close_session", "stop_transport", "instruction_collection", "workspace_restore", "phase_reporting", "close_error_reporting"])( + "keeps %s visible while its real settlement await is stalled", + async (blockedPhase) => { + const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); + stubBridges(); + let resume!: () => void; + let entered!: () => void; + const gate = new Promise((resolve) => { resume = resolve; }); + const reached = new Promise((resolve) => { entered = resolve; }); + const block = () => { entered(); return gate; }; + const scopes = new Map(); + if (blockedPhase === "stop_transport") { + vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => ({ env: {}, stop: block }) as never); + } + const execute = createAcpxEngineExecutor({ + createRuntime: () => ({ + ensureSession: async () => okHandle, + startTurn: () => blockedPhase === "close_error_reporting" ? throwingTurn() : completedTurn(), + close: blockedPhase === "close_session" ? block : async () => { + if (blockedPhase === "close_error_reporting") throw new Error("close fixture failed"); + }, + }) as never, + prepareRemoteManagedHome: async (input) => ({ + stagedRuntime: await input.stage([]), + teardown: async () => { + if (blockedPhase === "workspace_restore") await block(); + return { ok: true }; + }, + }), + }); + const execution = execute({ + runId: "pending-settlement-fixture", + ...remoteArgs(stateDir, localCwd, executionTarget), + onProviderStopped: blockedPhase === "instruction_collection" ? block : async () => {}, + onExecutionPhase: (phase: string) => { + const token = Symbol(); + scopes.set(token, phase); + return () => { scopes.delete(token); }; + }, + onEvent: async (event: { eventType: string; payload?: Record }) => { + if (blockedPhase === "phase_reporting" && event.eventType === "run.phase.timing" && event.payload?.phase === "end_session") await block(); + }, + onLog: async (_stream: string, text: string) => { + if (blockedPhase === "close_error_reporting" && text.includes("close fixture failed")) await block(); + }, + } as never); + try { + await reached; + expect([...scopes.values()].at(-1)).toBe(blockedPhase === "close_error_reporting" ? "phase_reporting" : blockedPhase); + } finally { + resume(); + expect((await execution).exitCode).toBe(blockedPhase === "close_error_reporting" ? 1 : 0); + } + expect(scopes.size).toBe(0); + }, + ); + + it("keeps the primary Stop cancellation visible until its await settles", async () => { + const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); + stubBridges(); + const controller = new AbortController(); + let started!: () => void; + let cancelled!: () => void; + let resume!: () => void; + const turnStarted = new Promise((resolve) => { started = resolve; }); + const cancellationStarted = new Promise((resolve) => { cancelled = resolve; }); + const gate = new Promise((resolve) => { resume = resolve; }); + const scopes = new Map(); + const execute = createAcpxEngineExecutor({ + createRuntime: () => ({ + ensureSession: async () => okHandle, + startTurn: () => { + started(); + return { + events: (async function* () { await gate; })(), + result: gate.then(() => ({ status: "cancelled", stopReason: "cancelled" })), + cancel: async () => { cancelled(); await gate; }, + }; + }, + close: async () => {}, + }) as never, + }); + const execution = execute({ + runId: "pending-cancel-fixture", + ...remoteArgs(stateDir, localCwd, executionTarget), + signal: controller.signal, + onExecutionPhase: (phase: string) => { + const token = Symbol(); + scopes.set(token, phase); + return () => { scopes.delete(token); }; + }, + } as never); + try { + await turnStarted; + controller.abort(); + await cancellationStarted; + expect([...scopes.values()].at(-1)).toBe("cancel_turn"); + } finally { + resume(); + expect((await execution).exitCode).toBe(1); + } + expect(scopes.size).toBe(0); + }); + + it("test_clean_completed_remote_teardown_runs_bridge_stop_then_sync_back_then_lease_release", async () => { + // cleanupRemoteBridges (execute.ts:2306-2326) fixes the sub-order for a clean + // exit: stop both bridges (allSettled) → run the managed-home sync-back + // (remoteManagedHomeTeardown) → release the per-session staging lease LAST in + // a finally. Record the order through spies threaded into the stubbed bridges + // and the seam teardown, and read the still-held lease during the sync-back. + const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); + const order: string[] = []; + vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => ({ + env: {}, + stop: vi.fn(async () => { + order.push("bridge-stop"); + }), + }) as never); + vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementation(async () => ({ + agentCommand: null, + stop: vi.fn(async () => { + order.push("bridge-stop"); + }), + }) as never); + + const stagingLocks = new Map>(); + let leaseSizeDuringSyncBack = -1; + const execute = createAcpxEngineExecutor({ + stagingLocks, + warmHandles: new Map(), + stagedRuntimes: new Map(), + createRuntime: () => + ({ + ensureSession: async () => okHandle, + startTurn: () => completedTurn(), + close: vi.fn(async () => {}), + }) as never, + prepareRemoteManagedHome: async (input) => { + const stagedRuntime = await input.stage([]); + return { + stagedRuntime, + teardown: async () => { + order.push("sync-back"); + // The lease is still held while the sync-back runs; it releases only + // afterward, in the cleanupRemoteBridges finally. + leaseSizeDuringSyncBack = stagingLocks.size; + return { ok: true }; + }, + }; + }, + }); + + const result = await execute({ + runId: "clean-order", + ...remoteArgs(stateDir, localCwd, executionTarget), + } as never); + + expect(result.exitCode).toBe(0); + // Both bridge stops precede the sync-back; the sync-back is the last recorded + // step before the lease release. + expect(order).toEqual(["bridge-stop", "bridge-stop", "sync-back"]); + // The lease was still held during the sync-back... + expect(leaseSizeDuringSyncBack).toBe(1); + // ...and released last, so the lock map never strands the next run. + expect(stagingLocks.size).toBe(0); + }); + + + it("test_terminal_close_reason_and_discard_persistent_state_per_outcome", async () => { + const cases: Array<{ + status: "completed" | "failed" | "cancelled"; + reason: string; + discard: boolean; + exitCode: number; + }> = [ + { status: "completed", reason: "paperclip completed turn cleanup", discard: false, exitCode: 0 }, + { status: "failed", reason: "paperclip turn failed", discard: false, exitCode: 1 }, + { status: "cancelled", reason: "paperclip turn cancelled", discard: true, exitCode: 1 }, + ]; + + for (const testCase of cases) { + const root = await makeTempRoot(); + const closeSpy = vi.fn(async () => {}); + const execute = createAcpxEngineExecutor({ + createRuntime: () => + ({ + ensureSession: async () => okHandle, + startTurn: () => ({ + events: (async function* () { + yield { type: "done", stopReason: "end_turn" }; + })(), + result: + testCase.status === "failed" + ? Promise.resolve({ status: "failed", error: new Error("turn boom") }) + : testCase.status === "cancelled" + ? Promise.resolve({ status: "cancelled", stopReason: "cancelled" }) + : Promise.resolve({ status: "completed", stopReason: "end_turn" }), + cancel: async () => {}, + }), + close: closeSpy, + }) as never, + }); + + const result = await execute({ + runId: `terminal-${testCase.status}`, + agent: { id: "agent-1", companyId: "company-1" }, + runtime: {}, + config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, + context: {}, + onLog: async () => {}, + onMeta: async () => {}, + } as never); + + expect(result.exitCode, `${testCase.status} exit`).toBe(testCase.exitCode); + expect(closeSpy, `${testCase.status} closes once`).toHaveBeenCalledTimes(1); + expect((closeSpy.mock.calls[0]! as unknown[])[0], `${testCase.status} close args`).toMatchObject({ + reason: testCase.reason, + discardPersistentState: testCase.discard, + }); + } + }); + + it("collects instructions after confirmed close on a thrown provider turn, before workspace restore", async () => { const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); const { paperclipStops, anyStopped } = stubBridges(); diff --git a/packages/adapter-utils/src/acpx-engine/run-fault-matrix.test.ts b/packages/adapter-utils/src/acpx-engine/run-fault-matrix.test.ts index e8e7c990a97..a1c37e61085 100644 --- a/packages/adapter-utils/src/acpx-engine/run-fault-matrix.test.ts +++ b/packages/adapter-utils/src/acpx-engine/run-fault-matrix.test.ts @@ -47,6 +47,14 @@ import { LedgerStateError, type ResourceId } from "./run-contracts.js"; import type { SettlementDispositionReport } from "./run-coordinator.js"; const tempRoots: string[] = []; +const bridgeStops: Array> = []; +const stagingLockMaps: Array>> = []; + +function makeStagingLocks() { + const locks = new Map>(); + stagingLockMaps.push(locks); + return locks; +} async function makeTempRoot() { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-fault-")); @@ -151,16 +159,17 @@ function throwingHandoffContext(): Record { // handles without running the real bridge transport. `stopRejects` makes the // bridge stop reject during settlement. function stubBridges(options: { stopRejects?: boolean } = {}) { - const stop = options.stopRejects - ? vi.fn(async () => { - throw new Error("bridge stop boom"); - }) + const makeStop = () => options.stopRejects + ? vi.fn(async () => { throw new Error("bridge stop boom"); }) : vi.fn(async () => {}); + const controlStop = makeStop(); + const agentStop = makeStop(); + bridgeStops.push(controlStop, agentStop); vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => { - return { env: {}, stop } as never; + return { env: {}, stop: controlStop } as never; }); vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementation(async () => { - return { agentCommand: null, stop } as never; + return { agentCommand: null, stop: agentStop } as never; }); } @@ -261,6 +270,10 @@ function assertDispositionReport( expect(ids.length, "no resource is reported twice").toBe(new Set(ids).size); // The report keys equal the acquired subset of the six resource ids. expect(new Set(ids)).toEqual(new Set(expected.acquired)); + // Reports alone can claim cleanup without invoking the actual cleanup callbacks. + if (ids.includes("control_bridge")) expect(bridgeStops[0]).toHaveBeenCalledTimes(1); + if (ids.includes("agent_bridge")) expect(bridgeStops[1]).toHaveBeenCalledTimes(1); + for (const locks of stagingLockMaps) expect(locks.size).toBe(0); const transferred = expected.transferred ?? null; for (const record of records) { expect(["finalized", "transferred"]).toContain(record.disposition); @@ -289,6 +302,8 @@ const SANDBOX_WITH_MANAGED_HOME: ResourceId[] = [ describe("composed ACPX run fault matrix", () => { beforeEach(() => { vi.clearAllMocks(); + bridgeStops.length = 0; + stagingLockMaps.length = 0; }); // Case 1 — a build failure throws before the ledger acquires any resource. @@ -302,7 +317,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), onSettlementDisposition: capture.onSettlementDisposition, }); @@ -329,7 +344,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), onSettlementDisposition: capture.onSettlementDisposition, }); @@ -350,7 +365,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => { throw new Error("createRuntime boom"); }, @@ -363,6 +378,9 @@ describe("composed ACPX run fault matrix", () => { } as never); expect(result.exitCode).toBe(1); + expect(result.signal).toBeNull(); + expect(result.timedOut).toBe(false); + expect(result.summary).toContain("createRuntime boom"); expect(result.resultJson?.phase).toBe("create_runtime"); assertDispositionReport(capture.last(), { acquired: SANDBOX_PRE_RUNTIME }); }); @@ -372,17 +390,18 @@ describe("composed ACPX run fault matrix", () => { const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); stubBridges(); const capture = captureDisposition(); + const close = vi.fn(async () => {}); const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => { throw new Error("ensureSession boom"); }, startTurn: () => completedTurn(), - close: async () => {}, + close, }) as never, onSettlementDisposition: capture.onSettlementDisposition, }); @@ -393,6 +412,7 @@ describe("composed ACPX run fault matrix", () => { } as never); expect(result.exitCode).toBe(1); + expect(close).toHaveBeenCalledTimes(1); expect(result.resultJson?.phase).toBe("ensure_session"); assertDispositionReport(capture.last(), { acquired: SANDBOX_WITH_RUNTIME }); }); @@ -406,7 +426,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => undefined, @@ -434,10 +454,11 @@ describe("composed ACPX run fault matrix", () => { const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); stubBridges(); const capture = captureDisposition(); + const close = vi.fn(async () => {}); const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => okHandle, @@ -445,7 +466,7 @@ describe("composed ACPX run fault matrix", () => { setConfigOption: async () => { throw new Error("setConfigOption boom"); }, - close: async () => {}, + close, }) as never, onSettlementDisposition: capture.onSettlementDisposition, }); @@ -464,7 +485,11 @@ describe("composed ACPX run fault matrix", () => { } as never); expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("configure_session"); + expect(close).toHaveBeenCalledTimes(1); + expect(result.resultJson).toMatchObject({ + phase: "configure_session", agent: "custom", requestedModel: "custom-model-x", + requestedThinkingEffort: null, fastMode: false, + }); assertDispositionReport(capture.last(), { acquired: SANDBOX_WITH_RUNTIME }); }); @@ -476,7 +501,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), onSettlementDisposition: capture.onSettlementDisposition, }); @@ -517,16 +542,20 @@ describe("composed ACPX run fault matrix", () => { // succeeds. The runtime is created once, so acp_runtime registers once. const capture = captureDisposition(); let created = 0; + let started = 0; + const ensureInputs: Record[] = []; + const logs: string[] = []; const secondExecute = createAcpxEngineExecutor({ onSettlementDisposition: capture.onSettlementDisposition, createRuntime: () => { created += 1; return { ensureSession: async (session: Record) => { + ensureInputs.push(session); if (session.resumeSessionId) throw new Error("resume session not found"); return okHandle; }, - startTurn: () => completedTurn(), + startTurn: () => { started += 1; return completedTurn(); }, setConfigOption: async () => {}, close: async () => {}, } as never; @@ -538,13 +567,19 @@ describe("composed ACPX run fault matrix", () => { runtime: { sessionParams: (first as { sessionParams?: unknown }).sessionParams }, config, context: {}, - onLog: async () => {}, + onLog: async (_stream: string, text: string) => { logs.push(text); }, onMeta: async () => {}, } as never); expect(second.exitCode).toBe(0); // The runtime was created exactly once across the resume and the fresh retry. expect(created).toBe(1); + expect(ensureInputs).toHaveLength(2); + expect(ensureInputs[0]?.resumeSessionId).toBe(first.sessionId); + expect(ensureInputs[1]?.resumeSessionId).toBeUndefined(); + expect(started).toBe(1); + expect(second.clearSession).toBe(true); + expect(logs.some((text) => text.includes("is unavailable; retrying with a fresh session"))).toBe(true); // The host lane acquires only the runtime, and it appears once. assertDispositionReport(capture.last(), { acquired: ["acp_runtime"] }); }); @@ -555,15 +590,16 @@ describe("composed ACPX run fault matrix", () => { const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); stubBridges(); const capture = captureDisposition(); + const order: string[] = []; const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => okHandle, - startTurn: () => throwingTurn(), - close: async () => {}, + startTurn: () => ({ ...throwingTurn(), cancel: async () => { order.push("cancel"); } }), + close: async () => { order.push("close"); }, }) as never, onSettlementDisposition: capture.onSettlementDisposition, }); @@ -574,6 +610,7 @@ describe("composed ACPX run fault matrix", () => { } as never); expect(result.exitCode).toBe(1); + expect(order).toEqual(["cancel", "close"]); expect(result.resultJson?.phase).toBe("turn"); expect(result.errorCode).toBe("acpx_turn_failed"); assertDispositionReport(capture.last(), { acquired: SANDBOX_WITH_RUNTIME }); @@ -591,7 +628,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => okHandle, @@ -629,7 +666,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => ({ ensureSession: async () => okHandle, @@ -658,7 +695,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), prepareRemoteManagedHome: managedHomeSeed(), onSettlementDisposition: capture.onSettlementDisposition, @@ -670,6 +707,13 @@ describe("composed ACPX run fault matrix", () => { } as never); expect(result.exitCode).toBe(0); + expect(result.signal).toBeNull(); + expect(result.timedOut).toBe(false); + expect(result.errorCode).toBeNull(); + expect(result.sessionId).toBe("backend-session"); + expect(result.sessionDisplayId).toBe("agent-session"); + expect(result.sessionParams).toBeTruthy(); + expect(result.summary).toBe("end_turn"); expect(result.resultJson?.status).toBe("completed"); assertDispositionReport(capture.last(), { acquired: SANDBOX_WITH_MANAGED_HOME, @@ -686,7 +730,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), onSettlementDisposition: capture.onSettlementDisposition, }); @@ -714,7 +758,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), prepareRemoteManagedHome: managedHomeSeed({ teardownRejects: true }), onSettlementDisposition: capture.onSettlementDisposition, @@ -742,7 +786,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), prepareRemoteManagedHome: managedHomeSeed(), }); @@ -770,7 +814,7 @@ describe("composed ACPX run fault matrix", () => { const execute = createAcpxEngineExecutor({ warmHandles: new Map(), stagedRuntimes: new Map(), - stagingLocks: new Map(), + stagingLocks: makeStagingLocks(), createRuntime: () => completedRuntime(), prepareRemoteManagedHome: managedHomeSeedWithClassifiedTeardownFailure(eaccesError), }); @@ -915,6 +959,8 @@ describe("composed ACPX run fault matrix", () => { describe("composed ACPX run per-phase telemetry", () => { beforeEach(() => { vi.clearAllMocks(); + bridgeStops.length = 0; + stagingLockMaps.length = 0; }); it("emits a run.phase.timing event for each lifecycle phase a clean host run reaches", async () => { diff --git a/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts b/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts deleted file mode 100644 index ff74f3c8f3c..00000000000 --- a/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts +++ /dev/null @@ -1,1164 +0,0 @@ -// Characterization baselines for ACP run settlement/teardown. These tests PIN -// the current behavior of the engine's teardown orchestration (Layer A), the -// `restoreWorkspace` internal order plus native-sync selection (Layer B), and -// the per-adapter sync-back registration seam (Layer C). They never change -// production code; each expectation records what the code does today. -// -// The harness top (imports, the execution-target mock, `makeTempRoot`, the local -// sandbox runner, `buildRuntime`, `runExecutor`) is copied from -// `execute.test.ts` (lines 1-336) so this file drives the same engine the same -// way. The teardown helpers (`stubBridges`, `throwingHandoffContext`, -// `completedTurn`, `throwingTurn`, `setupRemoteSandbox`, `remoteArgs`) mirror the -// F2/F3 describes in `execute.test.ts` (~:2602, ~:4858). -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { execFile as execFileCallback } from "node:child_process"; -import { promisify } from "node:util"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AdapterExecutionContext, AdapterRuntimeMcpAccess } from "@paperclipai/adapter-utils"; -import { - startAdapterExecutionTargetPaperclipBridge, - startAdapterExecutionTargetProcessSessionBridge, -} from "@paperclipai/adapter-utils/execution-target"; - -// Wrap the staging seam + both sandbox bridges in call-recording spies that -// still delegate to the real implementations. A runner-backed sandbox test -// exercises them end-to-end against a local runner, while a teardown test can -// override just the bridges with stop spies. (Copied from execute.test.ts.) -vi.mock("@paperclipai/adapter-utils/execution-target", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - prepareAdapterExecutionTargetRuntime: vi.fn(actual.prepareAdapterExecutionTargetRuntime), - startAdapterExecutionTargetPaperclipBridge: vi.fn(actual.startAdapterExecutionTargetPaperclipBridge), - startAdapterExecutionTargetProcessSessionBridge: vi.fn(actual.startAdapterExecutionTargetProcessSessionBridge), - }; -}); -import { - createAcpxEngineExecutor, - type AcpxEngineExecutorOptions, -} from "./execute.js"; -import { runChildProcess } from "../server-utils.js"; -import { - mirrorDirectory, - prepareSandboxManagedRuntime, - type SandboxManagedRuntimeClient, - type SandboxSyncOperation, - type SandboxSyncResult, -} from "../sandbox-managed-runtime.js"; - -const execFile = promisify(execFileCallback); -const tempRoots: string[] = []; - -async function makeTempRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-")); - tempRoots.push(root); - return root; -} - -afterEach(async () => { - await Promise.all( - tempRoots.splice(0).map((root) => - fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }), - ), - ); -}); - -function createLocalSandboxRunner( - onExecute?: (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - }) => void, -) { - let counter = 0; - return { - execute: async (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - stdin?: string; - timeoutMs?: number; - onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - onSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }) => { - counter += 1; - onExecute?.(input); - const command = input.command === "bash" ? "/bin/bash" : input.command; - return await runChildProcess(`acpx-sandbox-run-${counter}`, command, input.args ?? [], { - cwd: input.cwd ?? process.cwd(), - env: input.env ?? {}, - stdin: input.stdin, - timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)), - graceSec: 5, - onLog: input.onLog ?? (async () => {}), - onSpawn: input.onSpawn - ? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt }) - : undefined, - }); - }, - }; -} - -function buildRuntime( - onSetConfigOption?: (input: { key: string; value: string }) => void, - onEnsureSession?: (input: Record) => void, -) { - return { - ensureSession: async (input: Record) => { - onEnsureSession?.(input); - return { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }; - }, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }), - setConfigOption: async (input: { key: string; value: string }) => { - onSetConfigOption?.(input); - }, - close: async () => {}, - }; -} - -async function runExecutor( - config: Record, - options: { - context?: Record; - executionTransport?: Record; - authToken?: string; - executionTarget?: Record; - runtimeMcp?: AdapterRuntimeMcpAccess; - prepareRemoteManagedHome?: AcpxEngineExecutorOptions["prepareRemoteManagedHome"]; - startupTraceContext?: AdapterExecutionContext["startupTraceContext"]; - } = {}, -) { - const runtimeOptions: Record[] = []; - const configOptions: Array<{ key: string; value: string }> = []; - const sessionInputs: Record[] = []; - const meta: Record[] = []; - const logs: Array<{ stream: string; text: string }> = []; - const events: Array<{ eventType: string; payload?: Record }> = []; - const execute = createAcpxEngineExecutor({ - ...(options.prepareRemoteManagedHome - ? { prepareRemoteManagedHome: options.prepareRemoteManagedHome } - : {}), - createRuntime: (createOptions) => { - runtimeOptions.push(createOptions as unknown as Record); - return buildRuntime( - ({ key, value }) => configOptions.push({ key, value }), - (input) => sessionInputs.push(input), - ) as never; - }, - }); - - const result = await execute({ - runId: "run-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: options.context ?? {}, - executionTransport: options.executionTransport, - authToken: options.authToken, - executionTarget: options.executionTarget, - runtimeMcp: options.runtimeMcp, - startupTraceContext: options.startupTraceContext, - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - onMeta: async (payload: unknown) => { - meta.push(payload as Record); - }, - onEvent: async (event: { eventType: string; payload?: Record }) => { - events.push(event); - }, - } as never); - - expect(result.exitCode).toBe(0); - return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result }; -} - -// --------------------------------------------------------------------------- -// Shared teardown-test helpers (mirror execute.test.ts F3 describe, ~:4858). -// --------------------------------------------------------------------------- - -const okHandle = { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", -}; - -async function setupRemoteSandbox() { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const localCwd = path.join(root, "worktree"); - const remoteCwd = path.join(root, "remote-workspace"); - await fs.mkdir(localCwd, { recursive: true }); - await fs.mkdir(remoteCwd, { recursive: true }); - await fs.writeFile(path.join(localCwd, "hello.txt"), "hi", "utf8"); - const executionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "fake-plugin", - remoteCwd, - runner: createLocalSandboxRunner(), - }; - return { root, stateDir, localCwd, remoteCwd, executionTarget }; -} - -// Stub both sandbox bridges with stop spies collected per start, so a test can -// assert the bridges stopped without running the real bridge transport. -function stubBridges() { - const paperclipStops: Array> = []; - const processStops: Array> = []; - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => { - const stop = vi.fn(async () => {}); - paperclipStops.push(stop); - return { env: {}, stop } as never; - }); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementation(async () => { - const stop = vi.fn(async () => {}); - processStops.push(stop); - return { agentCommand: null, stop } as never; - }); - const anyStopped = (stops: Array>) => - stops.some((stop) => stop.mock.calls.length > 0); - return { paperclipStops, processStops, anyStopped }; -} - -function throwingHandoffContext(): Record { - const context: Record = {}; - Object.defineProperty(context, "paperclipSessionHandoffMarkdown", { - enumerable: false, - get() { - throw new Error("prompt build boom"); - }, - }); - return context; -} - -function completedTurn() { - return { - events: (async function* () {})(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }; -} - -function throwingTurn() { - return { - events: (async function* () { - throw new Error("turn upstream boom"); - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }; -} - -function remoteArgs( - stateDir: string, - localCwd: string, - executionTarget: unknown, - overrides: Record = {}, -) { - return { - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - context: {}, - authToken: "real-run-jwt", - executionTarget, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - ...overrides, - }; -} - -// =========================================================================== -// Layer A — engine teardown orchestration. -// =========================================================================== -describe("ACP settlement — Layer A: engine teardown orchestration", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it.each(["close_session", "stop_transport", "instruction_collection", "workspace_restore", "phase_reporting", "close_error_reporting"])( - "keeps %s visible while its real settlement await is stalled", - async (blockedPhase) => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - stubBridges(); - let resume!: () => void; - let entered!: () => void; - const gate = new Promise((resolve) => { resume = resolve; }); - const reached = new Promise((resolve) => { entered = resolve; }); - const block = () => { entered(); return gate; }; - const scopes = new Map(); - if (blockedPhase === "stop_transport") { - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => ({ env: {}, stop: block }) as never); - } - const execute = createAcpxEngineExecutor({ - createRuntime: () => ({ - ensureSession: async () => okHandle, - startTurn: () => blockedPhase === "close_error_reporting" ? throwingTurn() : completedTurn(), - close: blockedPhase === "close_session" ? block : async () => { - if (blockedPhase === "close_error_reporting") throw new Error("close fixture failed"); - }, - }) as never, - prepareRemoteManagedHome: async (input) => ({ - stagedRuntime: await input.stage([]), - teardown: async () => { - if (blockedPhase === "workspace_restore") await block(); - return { ok: true }; - }, - }), - }); - const execution = execute({ - runId: "pending-settlement-fixture", - ...remoteArgs(stateDir, localCwd, executionTarget), - onProviderStopped: blockedPhase === "instruction_collection" ? block : async () => {}, - onExecutionPhase: (phase: string) => { - const token = Symbol(); - scopes.set(token, phase); - return () => { scopes.delete(token); }; - }, - onEvent: async (event: { eventType: string; payload?: Record }) => { - if (blockedPhase === "phase_reporting" && event.eventType === "run.phase.timing" && event.payload?.phase === "end_session") await block(); - }, - onLog: async (_stream: string, text: string) => { - if (blockedPhase === "close_error_reporting" && text.includes("close fixture failed")) await block(); - }, - } as never); - try { - await reached; - expect([...scopes.values()].at(-1)).toBe(blockedPhase === "close_error_reporting" ? "phase_reporting" : blockedPhase); - } finally { - resume(); - expect((await execution).exitCode).toBe(blockedPhase === "close_error_reporting" ? 1 : 0); - } - expect(scopes.size).toBe(0); - }, - ); - - it("keeps the primary Stop cancellation visible until its await settles", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - stubBridges(); - const controller = new AbortController(); - let started!: () => void; - let cancelled!: () => void; - let resume!: () => void; - const turnStarted = new Promise((resolve) => { started = resolve; }); - const cancellationStarted = new Promise((resolve) => { cancelled = resolve; }); - const gate = new Promise((resolve) => { resume = resolve; }); - const scopes = new Map(); - const execute = createAcpxEngineExecutor({ - createRuntime: () => ({ - ensureSession: async () => okHandle, - startTurn: () => { - started(); - return { - events: (async function* () { await gate; })(), - result: gate.then(() => ({ status: "cancelled", stopReason: "cancelled" })), - cancel: async () => { cancelled(); await gate; }, - }; - }, - close: async () => {}, - }) as never, - }); - const execution = execute({ - runId: "pending-cancel-fixture", - ...remoteArgs(stateDir, localCwd, executionTarget), - signal: controller.signal, - onExecutionPhase: (phase: string) => { - const token = Symbol(); - scopes.set(token, phase); - return () => { scopes.delete(token); }; - }, - } as never); - try { - await turnStarted; - controller.abort(); - await cancellationStarted; - expect([...scopes.values()].at(-1)).toBe("cancel_turn"); - } finally { - resume(); - expect((await execution).exitCode).toBe(1); - } - expect(scopes.size).toBe(0); - }); - - it("test_clean_completed_remote_teardown_runs_bridge_stop_then_sync_back_then_lease_release", async () => { - // cleanupRemoteBridges (execute.ts:2306-2326) fixes the sub-order for a clean - // exit: stop both bridges (allSettled) → run the managed-home sync-back - // (remoteManagedHomeTeardown) → release the per-session staging lease LAST in - // a finally. Record the order through spies threaded into the stubbed bridges - // and the seam teardown, and read the still-held lease during the sync-back. - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const order: string[] = []; - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementation(async () => ({ - env: {}, - stop: vi.fn(async () => { - order.push("bridge-stop"); - }), - }) as never); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementation(async () => ({ - agentCommand: null, - stop: vi.fn(async () => { - order.push("bridge-stop"); - }), - }) as never); - - const stagingLocks = new Map>(); - let leaseSizeDuringSyncBack = -1; - const execute = createAcpxEngineExecutor({ - stagingLocks, - warmHandles: new Map(), - stagedRuntimes: new Map(), - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close: vi.fn(async () => {}), - }) as never, - prepareRemoteManagedHome: async (input) => { - const stagedRuntime = await input.stage([]); - return { - stagedRuntime, - teardown: async () => { - order.push("sync-back"); - // The lease is still held while the sync-back runs; it releases only - // afterward, in the cleanupRemoteBridges finally. - leaseSizeDuringSyncBack = stagingLocks.size; - return { ok: true }; - }, - }; - }, - }); - - const result = await execute({ - runId: "clean-order", - ...remoteArgs(stateDir, localCwd, executionTarget), - } as never); - - expect(result.exitCode).toBe(0); - // Both bridge stops precede the sync-back; the sync-back is the last recorded - // step before the lease release. - expect(order).toEqual(["bridge-stop", "bridge-stop", "sync-back"]); - // The lease was still held during the sync-back... - expect(leaseSizeDuringSyncBack).toBe(1); - // ...and released last, so the lock map never strands the next run. - expect(stagingLocks.size).toBe(0); - }); - - it("test_clean_persistent_local_run_closes_and_relaunches_amendment_b", async () => { - // Amendment B: a clean persistent local turn closes the runtime instead of - // warm-saving it. The host-lane reuse candidate carries the run-minted API key, - // which is never revoked, so the credential gate fails and the settlement - // discards (close-and-relaunch). - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - setConfigOption: async () => {}, - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: "warm-save", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir: path.join(root, "state"), - mode: "persistent", - // A long idle window so the scheduled cleanup timer cannot fire during the - // assertions and evict the warm handle. The warm-save decision only needs - // warmIdleMs>0. The test clears the timer below, so it never outlives the - // test. - warmHandleIdleMs: 60_000, - }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => {}, - } as never); - - expect(result.exitCode).toBe(0); - // The runtime closed and relaunched: one close, no warm entry. - expect(closeSpy).toHaveBeenCalledTimes(1); - expect(warmHandles.size).toBe(0); - }); - - it("test_completed_non_persistent_local_run_closes_runtime_once", async () => { - // The default (non-persistent) completed turn closes the runtime with the - // clean-completion reason (execute.ts:3922-3926). - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: "clean-close", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(0); - expect(closeSpy).toHaveBeenCalledTimes(1); - expect((closeSpy.mock.calls[0]! as unknown[])[0]).toMatchObject({ - reason: "paperclip completed turn cleanup", - discardPersistentState: false, - }); - }); - - it("test_pre_turn_failure_after_handle_closes_runtime_except_create_runtime", async () => { - // Once a runtime handle exists, every pre-turn failure closes the runtime — - // missing handle (execute.ts:3633, synthesized handle), configure_session - // (:3716), prepare_turn (:4016) — EXCEPT create_runtime, where no runtime was - // ever constructed (:3662-3697). Each path returns a settled error with its - // phase. (The handshake close path needs a handle obtained first via the warm - // hit — pinned separately below, mirroring the F2 test.) - const scenarios: Array<{ - name: string; - phase: string; - expectClose: boolean; - // Builds the engine `createRuntime` given the shared close spy. The - // create_runtime scenario ignores the spy and throws before a runtime exists. - makeCreateRuntime: (close: () => Promise) => AcpxEngineExecutorOptions["createRuntime"]; - config: Record; - context: Record; - }> = [ - { - name: "missing_handle", - phase: "ensure_session", - expectClose: true, - makeCreateRuntime: (close) => () => - ({ - ensureSession: async () => undefined, - startTurn: () => completedTurn(), - close, - }) as never, - config: {}, - context: {}, - }, - { - name: "configure_session", - phase: "configure_session", - expectClose: true, - makeCreateRuntime: (close) => () => - ({ - ensureSession: async () => okHandle, - // A configured model on a non-claude/non-codex agent yields one session - // config option; a throwing setConfigOption drives the configure failure. - setConfigOption: async () => { - throw new Error("config boom"); - }, - startTurn: () => completedTurn(), - close, - }) as never, - config: { model: "custom-model-x" }, - context: {}, - }, - { - name: "prepare_turn", - phase: "prepare_turn", - expectClose: true, - makeCreateRuntime: (close) => () => - ({ - ensureSession: async () => okHandle, - startTurn: () => completedTurn(), - close, - }) as never, - config: {}, - context: throwingHandoffContext(), - }, - { - name: "create_runtime", - phase: "create_runtime", - expectClose: false, - makeCreateRuntime: () => () => { - throw new Error("createRuntime boom"); - }, - config: {}, - context: {}, - }, - ]; - - for (const scenario of scenarios) { - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: scenario.makeCreateRuntime(closeSpy), - }); - - const result = await execute({ - runId: `preturn-${scenario.name}`, - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir: path.join(root, "state"), - ...scenario.config, - }, - context: scenario.context, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode, `${scenario.name} exit`).toBe(1); - expect(result.resultJson?.phase, `${scenario.name} phase`).toBe(scenario.phase); - if (scenario.expectClose) { - expect(closeSpy, `${scenario.name} closes the runtime`).toHaveBeenCalledTimes(1); - } else { - // create_runtime failed before a runtime existed, so there is nothing to close. - expect(closeSpy, `${scenario.name} never constructs a runtime`).not.toHaveBeenCalled(); - } - } - }); - - it("test_handshake_failure_on_second_run_closes_the_recreated_runtime", async () => { - // Amendment B: the first clean persistent turn closes and relaunches instead of - // warm-saving, so the second run finds no warm handle. The second run re-creates - // the runtime and fails while persisting process identity (onSpawn throws); the - // handshake failure closes the re-created runtime. The host-lane warm-hit - // teardown path itself is characterized by the composed fault matrix. - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const startedAt = "2026-01-01T00:00:00.000Z"; - const closeSpy = vi.fn(async () => {}); - let created = 0; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: (options) => { - created += 1; - const opts = options as { - onAgentSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }; - return { - ensureSession: async () => { - await opts.onAgentSpawn?.({ pid: 4242, startedAt }); - return okHandle; - }, - startTurn: () => completedTurn(), - close: closeSpy, - } as never; - }, - }); - const config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - mode: "persistent", - warmHandleIdleMs: 60_000, - }; - - const first = await execute({ - runId: "warm-handshake-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => {}, - } as never); - expect(first.exitCode).toBe(0); - // Amendment B: no warm entry survives the first clean persistent turn. - expect(warmHandles.size).toBe(0); - expect(created).toBe(1); - - const second = await execute({ - runId: "warm-handshake-2", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: first.sessionParams }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => { - throw new Error("onSpawn boom"); - }, - } as never); - - // The second run re-created the runtime (no warm reuse) and closed it on the - // failure, reporting the ensure_session phase. Both runtimes closed. - expect(created).toBe(2); - expect(second.exitCode).toBe(1); - expect(second.resultJson?.phase).toBe("ensure_session"); - expect(closeSpy).toHaveBeenCalledTimes(2); - expect(warmHandles.size).toBe(0); - }); - - it("test_terminal_close_reason_and_discard_persistent_state_per_outcome", async () => { - // The terminal block (execute.ts:3869-3928) closes with an outcome-specific - // reason and discards persistent state only on cancel/timeout. - const cases: Array<{ - status: "completed" | "failed" | "cancelled"; - reason: string; - discard: boolean; - exitCode: number; - }> = [ - { status: "completed", reason: "paperclip completed turn cleanup", discard: false, exitCode: 0 }, - { status: "failed", reason: "paperclip turn failed", discard: false, exitCode: 1 }, - { status: "cancelled", reason: "paperclip turn cancelled", discard: true, exitCode: 1 }, - ]; - - for (const testCase of cases) { - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: - testCase.status === "failed" - ? Promise.resolve({ status: "failed", error: new Error("turn boom") }) - : testCase.status === "cancelled" - ? Promise.resolve({ status: "cancelled", stopReason: "cancelled" }) - : Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }), - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: `terminal-${testCase.status}`, - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode, `${testCase.status} exit`).toBe(testCase.exitCode); - expect(closeSpy, `${testCase.status} closes once`).toHaveBeenCalledTimes(1); - expect((closeSpy.mock.calls[0]! as unknown[])[0], `${testCase.status} close args`).toMatchObject({ - reason: testCase.reason, - discardPersistentState: testCase.discard, - }); - } - }); - - it("test_teardown_continues_after_one_step_fails_and_records_it", async () => { - // Corrected F3 policy (execute.ts:3374-3393): a failing teardown step is - // recorded and swallowed; later steps still run and the lease still releases. - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const { paperclipStops, processStops, anyStopped } = stubBridges(); - const stagingLocks = new Map>(); - const logs: Array<{ stream: string; text: string }> = []; - const execute = createAcpxEngineExecutor({ - stagingLocks, - warmHandles: new Map(), - stagedRuntimes: new Map(), - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => throwingTurn(), - close: async () => { - throw new Error("close boom"); - }, - }) as never, - }); - - const result = await execute({ - runId: "td-continue", - ...remoteArgs(stateDir, localCwd, executionTarget, { - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - }), - } as never); - - expect(result.exitCode).toBe(1); - expect(anyStopped(paperclipStops)).toBe(true); - expect(anyStopped(processStops)).toBe(true); - expect(stagingLocks.size).toBe(0); - expect( - logs.some( - (entry) => - entry.stream === "stderr" && entry.text.includes('teardown step "runtime-close" failed'), - ), - ).toBe(true); - }); - - it("test_teardown_failure_does_not_change_external_result", async () => { - // A teardown fault never leaks into the external result; the exit cause (the - // turn failure) stands (execute.ts F3 policy). - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - stubBridges(); - const execute = createAcpxEngineExecutor({ - warmHandles: new Map(), - stagedRuntimes: new Map(), - stagingLocks: new Map(), - createRuntime: () => - ({ - ensureSession: async () => okHandle, - startTurn: () => throwingTurn(), - close: async () => { - throw new Error("close boom"); - }, - }) as never, - }); - - const result = await execute({ - runId: "td-result", - ...remoteArgs(stateDir, localCwd, executionTarget), - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("turn"); - expect(result.errorCode).toBe("acpx_turn_failed"); - expect(result.errorMessage).toContain("turn upstream boom"); - expect(result.errorMessage).not.toContain("close boom"); - }); -}); - -// =========================================================================== -// Layer B — restoreWorkspace internal order + native-sync selection. -// =========================================================================== - -function toArrayBuffer(bytes: Buffer): ArrayBuffer { - return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; -} - -// A filesystem-backed managed-runtime client with the non-native base64-tar -// FALLBACK `syncIn` (mirrors sandbox-managed-runtime.test.ts `makeFilesystemClient` -// / `attachFallbackSyncIn`). `runCommands` records every `run` so a test can prove -// which restore transfer path the orchestrator took. When `withNativeSyncOut` is -// set, a native `syncOut` is attached so the client advertises BOTH directions. -function makeFsClient(options: { - runCommands?: string[]; - syncOutCalls?: { count: number }; - withNativeSyncOut?: boolean; -}): SandboxManagedRuntimeClient { - const client: SandboxManagedRuntimeClient = { - makeDir: async (remotePath) => { - await fs.mkdir(remotePath, { recursive: true }); - }, - writeFile: async (remotePath, bytes) => { - await fs.mkdir(path.dirname(remotePath), { recursive: true }); - await fs.writeFile(remotePath, Buffer.from(bytes)); - }, - readFile: async (remotePath) => await fs.readFile(remotePath), - listFiles: async (remotePath) => { - const entries = await fs.readdir(remotePath, { withFileTypes: true }).catch(() => []); - return entries - .filter((entry) => entry.isFile()) - .map((entry) => entry.name) - .sort((left, right) => left.localeCompare(right)); - }, - remove: async (remotePath) => { - await fs.rm(remotePath, { recursive: true, force: true }); - }, - run: async (command) => { - options.runCommands?.push(command); - await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); - }, - }; - client.syncIn = async (operations: SandboxSyncOperation[]): Promise => { - const resultOperations: SandboxSyncResult["operations"] = []; - for (const operation of operations) { - let filesTransferred = 0; - let bytesTransferred = 0; - for (const mapping of operation.files) { - const bytes = await fs.readFile(mapping.sourcePath); - await client.makeDir(path.posix.dirname(mapping.targetPath)); - await client.writeFile(mapping.targetPath, toArrayBuffer(bytes)); - filesTransferred += 1; - bytesTransferred += bytes.byteLength; - } - for (const command of operation.postUploadCommands ?? []) { - await client.run(command.command, { timeoutMs: command.timeoutMs ?? 30_000 }); - } - resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred }); - } - return { operations: resultOperations }; - }; - if (options.withNativeSyncOut) { - // Native outbound: materialize the sandbox workspace tree directly into the - // host destination (a directory file mapping), never through the tarball run. - client.syncOut = async (operations: SandboxSyncOperation[]): Promise => { - if (options.syncOutCalls) options.syncOutCalls.count += 1; - const resultOperations: SandboxSyncResult["operations"] = []; - for (const operation of operations) { - for (const mapping of operation.files) { - await fs.mkdir(mapping.targetPath, { recursive: true }); - await mirrorDirectory(mapping.sourcePath, mapping.targetPath); - } - resultOperations.push({ operationId: operation.operationId, filesTransferred: 1, bytesTransferred: 0 }); - } - return { operations: resultOperations }; - }; - } - return client; -} - -async function git(cwd: string, args: string[]): Promise { - const { stdout } = await execFile("git", ["-C", cwd, ...args], { maxBuffer: 32 * 1024 * 1024 }); - return stdout.trim(); -} - -describe("ACP settlement — Layer B: restoreWorkspace order + native-sync selection", () => { - it("test_non_git_workspace_restore_phase_order_and_preserve_absent_via_tarball_fallback", async () => { - // Model on sandbox-managed-runtime.test.ts "syncs workspace and assets through - // a provider-neutral sandbox client" (~:342). A client that exposes only the - // fallback `syncIn` (no `syncOut`) drives the remote-tarball restore transfer - // (sandbox-managed-runtime.ts:1192-1218) and the finalize ordering. - const root = await makeTempRoot(); - const localWorkspaceDir = path.join(root, "local-workspace"); - const remoteWorkspaceDir = path.join(root, "remote-workspace"); - await fs.mkdir(path.join(localWorkspaceDir, ".claude"), { recursive: true }); - await fs.writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8"); - await fs.writeFile(path.join(localWorkspaceDir, ".claude", "settings.json"), '{"local":true}\n', "utf8"); - - const runCommands: string[] = []; - const client = makeFsClient({ runCommands }); - // A fallback-only client never advertises native outbound. - expect(client.syncOut).toBeUndefined(); - - const runtimeStatuses: string[] = []; - const prepared = await prepareSandboxManagedRuntime({ - spec: { - transport: "sandbox", - provider: "test", - sandboxId: "sandbox-1", - remoteCwd: remoteWorkspaceDir, - timeoutMs: 30_000, - apiKey: null, - }, - adapterKey: "test-adapter", - client, - workspaceLocalDir: localWorkspaceDir, - workspaceExclude: [".claude"], - preserveAbsentOnRestore: [".claude"], - onRuntimeProgress: async (status) => { - runtimeStatuses.push(`${status.phase}:${status.message}`); - }, - }); - - // Staging excluded the host-managed `.claude` from the sandbox. - await expect( - fs.readFile(path.join(remoteWorkspaceDir, ".claude", "settings.json"), "utf8"), - ).rejects.toMatchObject({ code: "ENOENT" }); - - await fs.writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8"); - await fs.writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "sync back\n", "utf8"); - await prepared.restoreWorkspace(); - - // Sync-back applied the remote edits; the preserved-absent `.claude` stayed. - await expect(fs.readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe( - "remote workspace\n", - ); - await expect(fs.readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe( - "sync back\n", - ); - await expect( - fs.readFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "utf8"), - ).resolves.toBe('{"local":true}\n'); - - // The fallback restore built the remote workspace-download tarball via `run`. - expect(runCommands.some((command) => command.includes("workspace-download.tar"))).toBe(true); - // Phase order: config_sync (staging) → restore → finalize (finalize last). - expect(runtimeStatuses).toEqual( - expect.arrayContaining([ - "config_sync:Syncing workspace to environment", - "restore:Restoring workspace from environment", - "finalize:Finalizing workspace", - ]), - ); - expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace"); - }); - - it("test_git_backed_workspace_restore_phase_order", async () => { - // Model on sandbox-managed-runtime.test.ts "syncs git-backed workspaces through - // a shallow standalone clone…" (~:441). A git workspace adds the git_sync and - // export phases around the same config_sync/restore/finalize spine. - const root = await makeTempRoot(); - const sourceRepoDir = path.join(root, "source-repo"); - const localWorkspaceDir = path.join(root, "local-worktree"); - const remoteWorkspaceDir = path.join(root, "remote-workspace"); - - await fs.mkdir(sourceRepoDir, { recursive: true }); - await git(sourceRepoDir, ["init"]); - await git(sourceRepoDir, ["checkout", "-b", "main"]); - await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]); - await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]); - await fs.writeFile(path.join(sourceRepoDir, "tracked.txt"), "base\n", "utf8"); - await git(sourceRepoDir, ["add", "tracked.txt"]); - await git(sourceRepoDir, ["commit", "-m", "base"]); - await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]); - await fs.writeFile(path.join(localWorkspaceDir, "tracked.txt"), "dirty local\n", "utf8"); - - const client = makeFsClient({}); - const phases: string[] = []; - const prepared = await prepareSandboxManagedRuntime({ - spec: { - transport: "sandbox", - provider: "test", - sandboxId: "sandbox-1", - remoteCwd: remoteWorkspaceDir, - timeoutMs: 30_000, - apiKey: null, - }, - adapterKey: "test-adapter", - client, - workspaceLocalDir: localWorkspaceDir, - onRuntimeProgress: async (status) => { - phases.push(status.phase); - }, - }); - - // The sandbox holds a real git worktree seeded from the host history. - expect((await git(remoteWorkspaceDir, ["rev-list", "--count", "HEAD"]))).toBe("1"); - await git(remoteWorkspaceDir, ["config", "user.name", "Paperclip Sandbox"]); - await git(remoteWorkspaceDir, ["config", "user.email", "sandbox@paperclip.dev"]); - await git(remoteWorkspaceDir, ["add", "-A"]); - await git(remoteWorkspaceDir, ["commit", "-m", "sandbox update"]); - await fs.writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "from sandbox\n", "utf8"); - - await prepared.restoreWorkspace(); - - // The sandbox commit imported back onto the host worktree. - expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update"); - await expect(fs.readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe( - "from sandbox\n", - ); - expect(phases).toEqual( - expect.arrayContaining(["git_sync", "config_sync", "export", "restore", "finalize"]), - ); - expect(phases.at(-1)).toBe("finalize"); - }); - - it("test_native_syncOut_client_restores_through_native_transfer_not_tarball", async () => { - // The consumer reads `nativeSyncOut = typeof client.syncOut === "function"` - // (sandbox-managed-runtime.ts:775) and branches at :1163: with both directions - // native it uses `client.syncOut`, never the workspace-download tarball. - const root = await makeTempRoot(); - const localWorkspaceDir = path.join(root, "local-workspace"); - const remoteWorkspaceDir = path.join(root, "remote-workspace"); - await fs.mkdir(localWorkspaceDir, { recursive: true }); - await fs.writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8"); - - const runCommands: string[] = []; - const syncOutCalls = { count: 0 }; - const client = makeFsClient({ runCommands, syncOutCalls, withNativeSyncOut: true }); - // With both directions the client advertises native outbound. - expect(client.syncOut).toBeTypeOf("function"); - - const prepared = await prepareSandboxManagedRuntime({ - spec: { - transport: "sandbox", - provider: "test", - sandboxId: "sandbox-1", - remoteCwd: remoteWorkspaceDir, - timeoutMs: 30_000, - apiKey: null, - }, - adapterKey: "test-adapter", - client, - workspaceLocalDir: localWorkspaceDir, - }); - - await fs.writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8"); - await fs.writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "native sync\n", "utf8"); - await prepared.restoreWorkspace(); - - // The native outbound transfer ran and applied the remote edits... - expect(syncOutCalls.count).toBeGreaterThanOrEqual(1); - await expect(fs.readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe( - "remote workspace\n", - ); - await expect(fs.readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe( - "native sync\n", - ); - // ...and the tarball fallback was NOT used. - expect(runCommands.some((command) => command.includes("workspace-download.tar"))).toBe(false); - }); -}); - -// =========================================================================== -// Layer C — sync-back registration at the engine seam. -// -// The three real adapters each register a restoreWorkspace-backed teardown via -// their managed-home seam: codex packages/adapters/codex-local/src/server/acp.ts -// (teardown ~:239-258, wired by withCodexAcpDefaults ~:277); claude -// packages/adapters/claude-local/src/server/acp.ts (registerWorkspaceSyncBack -// ~:201-215, wired by withClaudeAcpDefaults ~:292); gemini -// packages/adapters/gemini-local/src/server/acp.ts (registerWorkspaceSyncBack -// ~:163-177, wired by withGeminiAcpDefaults ~:249). Those adapter packages are -// NOT dependencies of adapter-utils and resolve only to the canonical app -// checkout (not this worktree), and the plugin-sdk env-sync-negotiation helpers -// (definePlugin/startWorkerRpcHost) are not public exports — importing them from -// an adapter-utils test is infeasible/fragile. So this pins the adapter-agnostic -// sync-back at the engine seam: `prepareRemoteManagedHome` returns a `teardown` -// (the same shape each adapter registers), and the engine fires it exactly once -// on the exit/cleanup path (mirrors execute.test.ts -// test_remote_seam_teardown_fires_once_on_exit :2556). -// =========================================================================== -describe("ACP settlement — Layer C: per-adapter sync-back teardown fires once at the engine seam", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("test_engine_fires_each_adapter_named_sync_back_teardown_exactly_once", async () => { - for (const adapter of ["codex", "claude", "gemini"] as const) { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - let teardownCalls = 0; - await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { - authToken: "real-run-jwt", - executionTarget, - // The seam stands in for `${adapter}`'s registerWorkspaceSyncBack: it - // returns a teardown that would call stagedRuntime.restoreWorkspace(...). - prepareRemoteManagedHome: async (input) => { - const stagedRuntime = await input.stage([]); - return { - stagedRuntime, - teardown: async () => { - teardownCalls += 1; - return { ok: true }; - }, - }; - }, - }, - ); - - expect(teardownCalls, `${adapter} sync-back teardown fires exactly once`).toBe(1); - } - }); -}); diff --git a/packages/adapter-utils/src/acpx-engine/startup-characterization.test.ts b/packages/adapter-utils/src/acpx-engine/startup-characterization.test.ts deleted file mode 100644 index 9367f933e81..00000000000 --- a/packages/adapter-utils/src/acpx-engine/startup-characterization.test.ts +++ /dev/null @@ -1,917 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AcpRuntimeOptions } from "acpx/runtime"; -import type { AdapterExecutionContext, AdapterRuntimeMcpAccess } from "@paperclipai/adapter-utils"; -import { - prepareAdapterExecutionTargetRuntime, - startAdapterExecutionTargetPaperclipBridge, - startAdapterExecutionTargetProcessSessionBridge, -} from "@paperclipai/adapter-utils/execution-target"; - -// Wrap the staging seam + both sandbox bridges in call-recording spies that -// still delegate to the real implementations. This copies the execute.test.ts -// harness verbatim so a startup test asserts the exact staging args and bridge -// hand-off the engine threads without changing any real behavior. -vi.mock("@paperclipai/adapter-utils/execution-target", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - prepareAdapterExecutionTargetRuntime: vi.fn(actual.prepareAdapterExecutionTargetRuntime), - startAdapterExecutionTargetPaperclipBridge: vi.fn(actual.startAdapterExecutionTargetPaperclipBridge), - startAdapterExecutionTargetProcessSessionBridge: vi.fn(actual.startAdapterExecutionTargetProcessSessionBridge), - }; -}); -import { createAcpxEngineExecutor, type AcpxEngineExecutorOptions } from "./execute.js"; -import { runChildProcess } from "../server-utils.js"; - -const tempRoots: string[] = []; - -async function makeTempRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-")); - tempRoots.push(root); - return root; -} - -afterEach(async () => { - // A remote run stages a process-session bridge whose detached event writer can - // still be flushing a trailing event file into `.../process-sessions//events` - // when the run's own best-effort `client.remove(sessionDir)` (which production - // catch-wraps) has already returned. Under CI load that trailing write can land - // between this recursive delete's directory snapshot and its `rmdir`, surfacing as - // `ENOTEMPTY`. `maxRetries`/`retryDelay` make the cleanup ride out that window the - // same way production tolerates it, instead of failing the just-passed test. - await Promise.all( - tempRoots.splice(0).map((root) => - fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }), - ), - ); -}); - -async function pathExists(candidate: string): Promise { - return fs.access(candidate).then(() => true).catch(() => false); -} - -void pathExists; - -function createLocalSandboxRunner( - onExecute?: (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - }) => void, -) { - let counter = 0; - return { - execute: async (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - stdin?: string; - timeoutMs?: number; - onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - onSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }) => { - counter += 1; - onExecute?.(input); - const command = input.command === "bash" ? "/bin/bash" : input.command; - return await runChildProcess(`acpx-sandbox-run-${counter}`, command, input.args ?? [], { - cwd: input.cwd ?? process.cwd(), - env: input.env ?? {}, - stdin: input.stdin, - timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)), - graceSec: 5, - onLog: input.onLog ?? (async () => {}), - onSpawn: input.onSpawn - ? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt }) - : undefined, - }); - }, - }; -} - -function buildRuntime( - onSetConfigOption?: (input: { key: string; value: string }) => void, - onEnsureSession?: (input: Record) => void, -) { - return { - ensureSession: async (input: Record) => { - onEnsureSession?.(input); - return ({ - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }); - }, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }), - setConfigOption: async (input: { key: string; value: string }) => { - onSetConfigOption?.(input); - }, - close: async () => {}, - }; -} - -async function runExecutor( - config: Record, - options: { - context?: Record; - executionTransport?: Record; - authToken?: string; - executionTarget?: Record; - runtimeMcp?: AdapterRuntimeMcpAccess; - prepareRemoteManagedHome?: AcpxEngineExecutorOptions["prepareRemoteManagedHome"]; - startupTraceContext?: AdapterExecutionContext["startupTraceContext"]; - } = {}, -) { - const runtimeOptions: Record[] = []; - const configOptions: Array<{ key: string; value: string }> = []; - const sessionInputs: Record[] = []; - const meta: Record[] = []; - const logs: Array<{ stream: string; text: string }> = []; - const events: Array<{ eventType: string; payload?: Record }> = []; - const execute = createAcpxEngineExecutor({ - ...(options.prepareRemoteManagedHome - ? { prepareRemoteManagedHome: options.prepareRemoteManagedHome } - : {}), - createRuntime: (options) => { - runtimeOptions.push(options as unknown as Record); - return buildRuntime( - ({ key, value }) => configOptions.push({ key, value }), - (input) => sessionInputs.push(input), - ) as never; - }, - }); - - const result = await execute({ - runId: "run-1", - agent: { - id: "agent-1", - companyId: "company-1", - }, - runtime: {}, - config, - context: options.context ?? {}, - executionTransport: options.executionTransport, - authToken: options.authToken, - executionTarget: options.executionTarget, - runtimeMcp: options.runtimeMcp, - startupTraceContext: options.startupTraceContext, - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - onMeta: async (payload: unknown) => { - meta.push(payload as Record); - }, - onEvent: async (event: { eventType: string; payload?: Record }) => { - events.push(event); - }, - } as never); - - expect(result.exitCode).toBe(0); - return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result }; -} - -// The staging-seam describe helper from execute.test.ts (~:2152). It builds a -// runner-backed remote target: the local runner extracts the staged tar into -// `remoteCwd`, so the run really ships the HOST worktree into the sandbox. -async function setupRemoteSandbox() { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const localCwd = path.join(root, "worktree"); - const remoteCwd = path.join(root, "remote-workspace"); - await fs.mkdir(localCwd, { recursive: true }); - await fs.mkdir(remoteCwd, { recursive: true }); - // A file present only in the HOST worktree proves the workspace is shipped - // into the sandbox: the local runner extracts the staged tar into remoteCwd. - await fs.writeFile(path.join(localCwd, "hello.txt"), "hi", "utf8"); - const runner = createLocalSandboxRunner(); - const executionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "fake-plugin", - remoteCwd, - runner, - }; - return { root, stateDir, localCwd, remoteCwd, executionTarget }; -} - -// Read the `configFingerprint` off a settled run result. -function fpOf(result: { sessionParams?: unknown }): string | undefined { - return (result.sessionParams as { configFingerprint?: string } | undefined)?.configFingerprint; -} - -const okHandle = { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", -}; - -function completedTurn() { - return { - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }; -} - -describe("ACPX engine startup characterization", () => { - // Item 1 + 8: the remote launch env and its finalization point. - describe("remote launch environment values", () => { - beforeEach(() => vi.clearAllMocks()); - - it("mints the bridge launch env into the process-session command payload", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - // Decode the process-session LAUNCH payload (the base64 command blob). The - // in-sandbox process env rides there, not in the exec's own `env`. - let launchPayload: Record | null = null; - (executionTarget as { runner: unknown }).runner = createLocalSandboxRunner((input) => { - if (input.env?.PAPERCLIP_SANDBOX_EXEC_CHANNEL === "bridge") { - const script = input.args?.[1] ?? ""; - const match = script.match(/PAPERCLIP_PROCESS_SESSION_COMMAND_B64='([^']+)'/); - if (match) { - launchPayload = JSON.parse(Buffer.from(match[1]!, "base64").toString("utf8")) as Record< - string, - unknown - >; - } - } - }); - - await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { authToken: "real-run-jwt", executionTarget }, - ); - - // The launch payload carries the MERGED paperclip bridge env: the queue - // transport mode, a loopback bridge base URL, and a minted bridge token. - const payloadEnv = ((launchPayload as Record | null)?.env ?? {}) as Record< - string, - unknown - >; - expect(payloadEnv).toMatchObject({ PAPERCLIP_API_BRIDGE_MODE: "queue_v1" }); - expect(String(payloadEnv.PAPERCLIP_API_URL ?? "")).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); - // The minted bridge token is present and is NOT the host run JWT. - expect(payloadEnv.PAPERCLIP_API_KEY).toBeTruthy(); - expect(payloadEnv.PAPERCLIP_API_KEY).not.toBe("real-run-jwt"); - }); - - it("finalizes the launch env at the bridge merge: the process env carries the merged bridge values", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - // Capture the launch payload AND the bridge-channel exec's own `env`. - let launchPayload: Record | null = null; - let bridgeExecEnv: Record | undefined; - (executionTarget as { runner: unknown }).runner = createLocalSandboxRunner((input) => { - if (input.env?.PAPERCLIP_SANDBOX_EXEC_CHANNEL === "bridge") { - bridgeExecEnv = input.env; - const script = input.args?.[1] ?? ""; - const match = script.match(/PAPERCLIP_PROCESS_SESSION_COMMAND_B64='([^']+)'/); - if (match) { - launchPayload = JSON.parse(Buffer.from(match[1]!, "base64").toString("utf8")) as Record< - string, - unknown - >; - } - } - }); - - await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { authToken: "real-run-jwt", executionTarget }, - ); - - // The launch payload is the finalized carrier of the process env. It already - // holds the merged bridge values at the point the run emits it, so no later - // write mutates the process env after the bridge merge. - const payloadEnv = ((launchPayload as Record | null)?.env ?? {}) as Record< - string, - unknown - >; - expect(payloadEnv.PAPERCLIP_API_BRIDGE_MODE).toBe("queue_v1"); - expect(payloadEnv.PAPERCLIP_API_KEY).toBeTruthy(); - // The bridge-channel exec's OWN env is the sandbox transport channel, not the - // agent process env: it does not carry the minted agent bridge key. This pins - // that the merged agent env lives only in the finalized launch payload. - expect(bridgeExecEnv?.PAPERCLIP_SANDBOX_EXEC_CHANNEL).toBe("bridge"); - expect(bridgeExecEnv?.PAPERCLIP_API_KEY).toBeUndefined(); - }); - }); - - // Item 2: the 17 fingerprint fields folded into `configFingerprint`, and the - // outer session key form that embeds it. - describe("session fingerprint and session key", () => { - beforeEach(() => vi.clearAllMocks()); - - it("forms the session key as paperclip:company:agent:taskKey:fingerprint and embeds the fingerprint", async () => { - const root = await makeTempRoot(); - const { result } = await runExecutor({ - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir: path.join(root, "state"), - cwd: path.join(root, "workspace"), - }); - - const fp = fpOf(result); - expect(fp).toBeTypeOf("string"); - expect(fp).toBeTruthy(); - // No taskId/issueId/workspaceId in the default context, so taskKey is "default". - const sessionKey = (result.sessionParams as { sessionKey?: string }).sessionKey; - expect(sessionKey).toBe(`paperclip:company-1:agent-1:default:${fp}`); - }); - - it("keeps the fingerprint stable across two identical runs and a same-config new wake", async () => { - const root = await makeTempRoot(); - const baseConfig = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir: path.join(root, "state"), - cwd: path.join(root, "workspace"), - }; - const first = await runExecutor(baseConfig, { - context: { taskId: "issue-1", wakeReason: "issue_assigned" }, - }); - const identical = await runExecutor(baseConfig, { - context: { taskId: "issue-1", wakeReason: "issue_assigned" }, - }); - // A fresh heartbeat with a different wake reason but the same config env. - const newWake = await runExecutor(baseConfig, { - context: { taskId: "issue-1", wakeReason: "comment", wakeCommentId: "c-9" }, - }); - - expect(fpOf(first.result)).toBeTruthy(); - expect(fpOf(identical.result)).toBe(fpOf(first.result)); - // Per-wake PAPERCLIP_* churn does not reset the session fingerprint. - expect(fpOf(newWake.result)).toBe(fpOf(first.result)); - }); - - it("busts the fingerprint when any representative folded dimension changes", async () => { - const root = await makeTempRoot(); - const cwd = path.join(root, "workspace"); - const stateDir = path.join(root, "state"); - const context = { context: { taskId: "issue-1", wakeReason: "issue_assigned" } }; - const base = { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd }; - - const baseFp = fpOf((await runExecutor(base, context)).result); - expect(baseFp).toBeTruthy(); - - // Each edit changes exactly one folded dimension and must bust the fingerprint. - // agentCommand. - expect(fpOf((await runExecutor({ ...base, agentCommand: "node ./other-acp.js" }, context)).result)).not.toBe(baseFp); - // cwd. - expect(fpOf((await runExecutor({ ...base, cwd: path.join(root, "other-cwd") }, context)).result)).not.toBe(baseFp); - // requestedModel. - expect(fpOf((await runExecutor({ ...base, model: "some-model" }, context)).result)).not.toBe(baseFp); - // requestedThinkingEffort. - expect(fpOf((await runExecutor({ ...base, thinkingEffort: "high" }, context)).result)).not.toBe(baseFp); - // mode. - expect(fpOf((await runExecutor({ ...base, mode: "oneshot" }, context)).result)).not.toBe(baseFp); - // adapterEnvHash (a resolved adapter env value). - expect(fpOf((await runExecutor({ ...base, env: { FOO: "bar" } }, context)).result)).not.toBe(baseFp); - // mcpServers identity (injected runtime MCP set). - expect( - fpOf( - ( - await runExecutor(base, { - ...context, - runtimeMcp: { - getServers: () => [ - { name: "github", url: "https://x.test/mcp", connectionId: "c-1", token: "t-1" }, - ], - }, - }) - ).result, - ), - ).not.toBe(baseFp); - // secretManifestHash. - expect( - fpOf( - ( - await runExecutor(base, { - ...context, - context: { - taskId: "issue-1", - wakeReason: "issue_assigned", - paperclipSecrets: { - manifest: [ - { - configPath: "env.API_TOKEN", - envKey: "API_TOKEN", - secretId: "secret-1", - bindingId: "binding-1", - secretKey: "api-token", - version: 1, - provider: "local_encrypted", - }, - ], - }, - }, - }) - ).result, - ), - ).not.toBe(baseFp); - // additionalSourcesIdentity (referenced-project set). - expect( - fpOf( - ( - await runExecutor(base, { - ...context, - context: { - taskId: "issue-1", - wakeReason: "issue_assigned", - paperclipWorkspace: { - cwd, - realization: { - additional: [ - { - path: "/host/project-a", - projectId: "a", - projectWorkspaceId: "ws-a", - repoUrl: "https://example.test/a.git", - repoRef: "ref-a-1", - }, - ], - }, - }, - }, - }) - ).result, - ), - ).not.toBe(baseFp); - - // fastMode folds only for codex, so pin it with a codex-vs-codex pair. - const codexBase = { agent: "codex", agentCommand: "node ./fake-acp.js", stateDir, cwd }; - const codexFp = fpOf((await runExecutor(codexBase, context)).result); - expect(fpOf((await runExecutor({ ...codexBase, fastMode: true }, context)).result)).not.toBe(codexFp); - }); - }); - - // Item 3: the staging seam call, its arguments, and its order (workspace then - // assets, serial). Modeled on the PR-1 staging-seam tests. - describe("staging seam calls, arguments, and order", () => { - beforeEach(() => vi.clearAllMocks()); - - it("stages the host workspace with no assets, exactly once, before the process launch", async () => { - const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox(); - const { sessionInputs, events } = await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { authToken: "real-run-jwt", executionTarget }, - ); - - // The staging seam crossed exactly once. - expect(vi.mocked(prepareAdapterExecutionTargetRuntime)).toHaveBeenCalledTimes(1); - const stageArgs = vi.mocked(prepareAdapterExecutionTargetRuntime).mock.calls[0]![0]; - // The HOST worktree is shipped first; no per-adapter home asset in this lane. - expect(stageArgs.workspaceLocalDir).toBe(localCwd); - expect(stageArgs.assets ?? []).toEqual([]); - expect(stageArgs.installCommand ?? null).toBeNull(); - expect(stageArgs.target).toMatchObject({ kind: "remote", transport: "sandbox" }); - - // The workspace really landed in the sandbox workspace dir. - await expect(fs.readFile(path.join(remoteCwd, "hello.txt"), "utf8")).resolves.toBe("hi"); - // A per-step timing event proves the sync ran inside its timed boundary. - const stageEvent = events.find( - (event) => event.eventType === "run.startup.step" && event.payload?.step === "stage.sync", - ); - expect(stageEvent).toBeTruthy(); - // And session/new binds to the in-sandbox workspace cwd the seam returned. - expect(sessionInputs[0]?.cwd).toBe(remoteCwd); - }); - - it("threads a managed-home asset through the same seam after the workspace", async () => { - const { root, stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const managedHomeDir = path.join(root, "managed-home"); - await fs.mkdir(managedHomeDir, { recursive: true }); - await fs.writeFile(path.join(managedHomeDir, "config.json"), "{}", "utf8"); - - await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { - authToken: "real-run-jwt", - executionTarget, - prepareRemoteManagedHome: async (input) => { - const stagedRuntime = await input.stage([ - { key: "home", localDir: managedHomeDir, followSymlinks: true }, - ]); - return { stagedRuntime }; - }, - }, - ); - - // The seam's home asset is threaded through the SAME staging seam, keyed by - // the workspace-local dir. Workspace ships first; the asset rides alongside it. - const stageArgs = vi.mocked(prepareAdapterExecutionTargetRuntime).mock.calls[0]![0]; - expect(stageArgs.workspaceLocalDir).toBe(localCwd); - expect(stageArgs.assets).toEqual([ - { key: "home", localDir: managedHomeDir, followSymlinks: true }, - ]); - }); - }); - - // Item 4: the two-bridge overlap and the ACP-initialization ordering. - describe("two-bridge overlap and ACP initialization order", () => { - beforeEach(() => vi.clearAllMocks()); - - it("defers the process bridge env, shares one runtimeRootDir, and runs session/new on the sandbox cwd", async () => { - const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox(); - const { sessionInputs, runtimeOptions } = await runExecutor( - { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - { authToken: "real-run-jwt", executionTarget }, - ); - - // The process-session bridge receives its launch env as a DEFERRED thunk, the - // seam that lets its env-independent setup overlap the paperclip bridge start. - const processArgs = vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mock.calls[0]![0]; - expect(typeof processArgs.env).toBe("function"); - - // Both bridges receive the SAME real (non-null) runtimeRootDir from staging. - const paperclipArgs = vi.mocked(startAdapterExecutionTargetPaperclipBridge).mock.calls[0]![0]; - expect(paperclipArgs.runtimeRootDir).toBeTruthy(); - expect(String(paperclipArgs.runtimeRootDir)).toContain(".paperclip-runtime"); - expect(processArgs.runtimeRootDir).toBe(paperclipArgs.runtimeRootDir); - - // The ACP runtime + session/new both bind to the in-sandbox workspace cwd, - // which the run resolves only after the bridges bring the sandbox up. - expect(runtimeOptions[0]?.cwd).toBe(remoteCwd); - expect(sessionInputs[0]?.cwd).toBe(remoteCwd); - expect(sessionInputs[0]?.cwd).not.toBe(localCwd); - }); - }); - - // Item 5 + 6: every startup exit path, its result phase, and the cleanup-call - // set (bridges stop / lease releases / runtime closes). - describe("startup exit paths and cleanup", () => { - beforeEach(() => vi.clearAllMocks()); - - it("create_runtime failure: settles an error result, stops both bridges, releases the lease", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const paperclipStop = vi.fn(async () => {}); - const processStop = vi.fn(async () => {}); - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementationOnce( - async () => ({ env: {}, stop: paperclipStop }) as never, - ); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementationOnce( - async () => ({ agentCommand: null, stop: processStop }) as never, - ); - const stagingLocks = new Map>(); - const execute = createAcpxEngineExecutor({ - stagingLocks, - warmHandles: new Map(), - stagedRuntimes: new Map(), - createRuntime: () => { - throw new Error("createRuntime boom"); - }, - }); - - const result = await execute({ - runId: "run-create-fail-remote", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - context: {}, - authToken: "real-run-jwt", - executionTarget, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never); - - // The post-build runtime-creation failure returns a settled error result. - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("create_runtime"); - // Both live bridges stop exactly once and the per-session lease releases. - expect(paperclipStop).toHaveBeenCalledTimes(1); - expect(processStop).toHaveBeenCalledTimes(1); - expect(stagingLocks.size).toBe(0); - }); - - it("partial-bridge failure: throws and stops the concurrently-started bridge exactly once", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - const stop = vi.fn(async () => {}); - vi.mocked(startAdapterExecutionTargetPaperclipBridge).mockImplementationOnce(async () => { - throw new Error("paperclip bridge boom"); - }); - vi.mocked(startAdapterExecutionTargetProcessSessionBridge).mockImplementationOnce( - async () => ({ agentCommand: null, stop }) as never, - ); - - const execute = createAcpxEngineExecutor({ - createRuntime: () => buildRuntime() as never, - }); - - // A partial bridge failure inside buildRuntime is one of the only two throw - // paths, so the run rethrows instead of settling a result. - await expect( - execute({ - runId: "run-bridge-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, - context: {}, - authToken: "real-run-jwt", - executionTarget, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - } as never), - ).rejects.toThrow("paperclip bridge boom"); - - // The concurrently-started process-session bridge was stopped exactly once. - expect(stop).toHaveBeenCalledTimes(1); - }); - - it("cold ensure_session throw: ensure_session error, and the runtime closes (the cold-handshake leak is fixed)", async () => { - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => { - throw new Error("ensureSession boom"); - }, - startTurn: () => completedTurn(), - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: "handshake-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("ensure_session"); - // The runtime enters the ledger the moment it is created, so a cold - // `ensureSession` throw (before any handle exists) still closes it through the - // settlement `endSession` step. This closes the former cold-handshake leak. - expect(closeSpy).toHaveBeenCalledTimes(1); - }); - - it("missing session handle: ensure_session error and the minimal runtime closes", async () => { - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - // A runtime that returns no session handle drives the missing-handle path. - ensureSession: async () => undefined, - startTurn: () => completedTurn(), - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: "missing-handle", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("ensure_session"); - expect(result.errorCode).toBe("acpx_runtime_error"); - expect(closeSpy).toHaveBeenCalledTimes(1); - }); - - it("configure_session failure: configure_session error and the runtime closes", async () => { - const root = await makeTempRoot(); - const closeSpy = vi.fn(async () => {}); - const execute = createAcpxEngineExecutor({ - createRuntime: () => - ({ - ensureSession: async () => okHandle, - // Gemini model/effort drive a session config option; a throwing setter - // fails the configure_session phase after the handshake succeeds. - setConfigOption: async () => { - throw new Error("setConfigOption boom"); - }, - startTurn: () => completedTurn(), - close: closeSpy, - }) as never, - }); - - const result = await execute({ - runId: "configure-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { - agent: "gemini", - model: "gemini-2.5-pro", - thinkingEffort: "high", - stateDir: path.join(root, "state"), - }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.resultJson?.phase).toBe("configure_session"); - expect(closeSpy).toHaveBeenCalledTimes(1); - }); - - it("persistent host failure: the first run closes and relaunches, so a failing second run re-creates and closes", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const startedAt = "2026-01-01T00:00:00.000Z"; - const closeSpy = vi.fn(async () => {}); - let created = 0; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: (options) => { - created += 1; - const opts = options as AcpRuntimeOptions & { - onAgentSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }; - return { - ensureSession: async () => { - await opts.onAgentSpawn?.({ pid: 4242, startedAt }); - return okHandle; - }, - startTurn: () => completedTurn(), - close: closeSpy, - } as never; - }, - }); - const config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - mode: "persistent", - warmHandleIdleMs: 60_000, - }; - - const first = await execute({ - runId: "warm-1", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => {}, - } as never); - expect(first.exitCode).toBe(0); - // Amendment B: the first clean persistent turn closes and relaunches instead - // of warm-saving the runtime, so no warm handle survives. - expect(warmHandles.size).toBe(0); - expect(created).toBe(1); - - // The second run finds no warm handle, so it re-creates the runtime and fails - // while persisting process identity during ensure_session. - const second = await execute({ - runId: "warm-2", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: first.sessionParams }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => { - throw new Error("onSpawn boom"); - }, - } as never); - - // The second run built runtime #2 (no warm reuse), closed it on the failure, - // and reported the ensure_session phase. Both runtimes closed (one per run). - expect(created).toBe(2); - expect(second.exitCode).toBe(1); - expect(second.resultJson?.phase).toBe("ensure_session"); - expect(closeSpy).toHaveBeenCalledTimes(2); - expect(warmHandles.size).toBe(0); - }); - }); - - // Item 7: the per-lane resource set. - describe("per-lane resource set", () => { - beforeEach(() => vi.clearAllMocks()); - - it("local lane: crosses no staging seam, starts no bridge, keeps session/new on the host cwd", async () => { - const root = await makeTempRoot(); - const localCwd = path.join(root, "worktree"); - await fs.mkdir(localCwd, { recursive: true }); - const { sessionInputs, runtimeOptions } = await runExecutor({ - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir: path.join(root, "state"), - cwd: localCwd, - }); - - expect(vi.mocked(prepareAdapterExecutionTargetRuntime)).not.toHaveBeenCalled(); - expect(vi.mocked(startAdapterExecutionTargetPaperclipBridge)).not.toHaveBeenCalled(); - expect(vi.mocked(startAdapterExecutionTargetProcessSessionBridge)).not.toHaveBeenCalled(); - expect(sessionInputs[0]?.cwd).toBe(localCwd); - expect(runtimeOptions[0]?.cwd).toBe(localCwd); - }); - - it("persistent host lane: closes and relaunches the handle, so a second run re-creates the runtime", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let created = 0; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - createRuntime: () => { - created += 1; - return buildRuntime() as never; - }, - }); - const config = { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - mode: "persistent", - warmHandleIdleMs: 60_000, - }; - const base = { - agent: { id: "agent-1", companyId: "company-1" }, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - onSpawn: async () => {}, - }; - - const first = await execute({ runId: "host-warm-1", runtime: {}, ...base } as never); - const second = await execute({ - runId: "host-warm-2", - runtime: { sessionParams: first.sessionParams }, - ...base, - } as never); - - expect(first.exitCode).toBe(0); - expect(second.exitCode).toBe(0); - // Amendment B: the host lane never warm-saves a live runtime (the run-minted - // API key is never revoked), so the first run closes and relaunches. No warm - // handle survives, so the second run re-creates runtime #2. - expect(created).toBe(2); - expect(warmHandles.size).toBe(0); - }); - - it("remote process-session lane: does NOT warm-save the handle, so a second run re-creates the runtime", async () => { - const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox(); - let created = 0; - const ensureInputs: Array> = []; - const warmHandles = new Map(); - const execute = createAcpxEngineExecutor({ - warmHandles, - stagedRuntimes: new Map(), - stagingLocks: new Map(), - createRuntime: () => { - created += 1; - return buildRuntime(undefined, (input) => ensureInputs.push(input)) as never; - }, - }); - const base = { - agent: { id: "agent-1", companyId: "company-1" }, - config: { - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - cwd: localCwd, - mode: "persistent", - warmHandleIdleMs: 60_000, - }, - context: {}, - authToken: "real-run-jwt", - executionTarget, - onLog: async () => {}, - onMeta: async () => {}, - onEvent: async () => {}, - }; - - const first = await execute({ runId: "remote-warm-1", runtime: {}, ...base } as never); - const second = await execute({ - runId: "remote-warm-2", - runtime: { sessionParams: first.sessionParams }, - ...base, - } as never); - - expect(first.exitCode).toBe(0); - expect(second.exitCode).toBe(0); - // The remote process-session lane never keeps the handle warm, so the second - // run re-creates the runtime and runs a fresh handshake instead of reusing one. - expect(created).toBe(2); - expect(warmHandles.size).toBe(0); - expect(ensureInputs).toHaveLength(2); - }); - }); -}); diff --git a/packages/adapter-utils/src/acpx-engine/turn-characterization.test.ts b/packages/adapter-utils/src/acpx-engine/turn-characterization.test.ts deleted file mode 100644 index f58e214182c..00000000000 --- a/packages/adapter-utils/src/acpx-engine/turn-characterization.test.ts +++ /dev/null @@ -1,686 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { AdapterExecutionContext, AdapterRuntimeMcpAccess } from "@paperclipai/adapter-utils"; -import { - prepareAdapterExecutionTargetRuntime, - startAdapterExecutionTargetPaperclipBridge, - startAdapterExecutionTargetProcessSessionBridge, -} from "@paperclipai/adapter-utils/execution-target"; - -// Wrap the staging seam + both sandbox bridges in call-recording spies that -// still delegate to the real implementations. This mirrors the execute.test.ts -// harness so the turn characterization tests share the same mocked module graph. -vi.mock("@paperclipai/adapter-utils/execution-target", async (importActual) => { - const actual = await importActual(); - return { - ...actual, - prepareAdapterExecutionTargetRuntime: vi.fn(actual.prepareAdapterExecutionTargetRuntime), - startAdapterExecutionTargetPaperclipBridge: vi.fn(actual.startAdapterExecutionTargetPaperclipBridge), - startAdapterExecutionTargetProcessSessionBridge: vi.fn(actual.startAdapterExecutionTargetProcessSessionBridge), - }; -}); -import { - createAcpxEngineExecutor, - summarizeAcpxTurnUsage, - type AcpxEngineExecutorOptions, -} from "./execute.js"; -import { runChildProcess } from "../server-utils.js"; - -const tempRoots: string[] = []; - -async function makeTempRoot() { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-")); - tempRoots.push(root); - return root; -} - -afterEach(async () => { - // A remote run stages a process-session bridge whose detached event writer can - // still be flushing a trailing event file into `.../process-sessions//events` - // when the run's own best-effort `client.remove(sessionDir)` (which production - // catch-wraps) has already returned. Under CI load that trailing write can land - // between this recursive delete's directory snapshot and its `rmdir`, surfacing as - // `ENOTEMPTY`. `maxRetries`/`retryDelay` make the cleanup ride out that window the - // same way production tolerates it, instead of failing the just-passed test. - await Promise.all( - tempRoots.splice(0).map((root) => - fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }), - ), - ); -}); - -function createLocalSandboxRunner( - onExecute?: (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - }) => void, -) { - let counter = 0; - return { - execute: async (input: { - command: string; - args?: string[]; - cwd?: string; - env?: Record; - stdin?: string; - timeoutMs?: number; - onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - onSpawn?: (meta: { pid: number; startedAt: string }) => Promise; - }) => { - counter += 1; - onExecute?.(input); - const command = input.command === "bash" ? "/bin/bash" : input.command; - return await runChildProcess(`acpx-sandbox-run-${counter}`, command, input.args ?? [], { - cwd: input.cwd ?? process.cwd(), - env: input.env ?? {}, - stdin: input.stdin, - timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)), - graceSec: 5, - onLog: input.onLog ?? (async () => {}), - onSpawn: input.onSpawn - ? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt }) - : undefined, - }); - }, - }; -} - -function buildRuntime( - onSetConfigOption?: (input: { key: string; value: string }) => void, - onEnsureSession?: (input: Record) => void, -) { - return { - ensureSession: async (input: Record) => { - onEnsureSession?.(input); - return ({ - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }); - }, - startTurn: () => ({ - events: (async function* () { - yield { type: "done", stopReason: "end_turn" }; - })(), - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - cancel: async () => {}, - }), - setConfigOption: async (input: { key: string; value: string }) => { - onSetConfigOption?.(input); - }, - close: async () => {}, - }; -} - -async function runExecutor( - config: Record, - options: { - context?: Record; - executionTransport?: Record; - authToken?: string; - executionTarget?: Record; - runtimeMcp?: AdapterRuntimeMcpAccess; - prepareRemoteManagedHome?: AcpxEngineExecutorOptions["prepareRemoteManagedHome"]; - startupTraceContext?: AdapterExecutionContext["startupTraceContext"]; - } = {}, -) { - const runtimeOptions: Record[] = []; - const configOptions: Array<{ key: string; value: string }> = []; - const sessionInputs: Record[] = []; - const meta: Record[] = []; - const logs: Array<{ stream: string; text: string }> = []; - const events: Array<{ eventType: string; payload?: Record }> = []; - const execute = createAcpxEngineExecutor({ - ...(options.prepareRemoteManagedHome - ? { prepareRemoteManagedHome: options.prepareRemoteManagedHome } - : {}), - createRuntime: (options) => { - runtimeOptions.push(options as unknown as Record); - return buildRuntime( - ({ key, value }) => configOptions.push({ key, value }), - (input) => sessionInputs.push(input), - ) as never; - }, - }); - - const result = await execute({ - runId: "run-1", - agent: { - id: "agent-1", - companyId: "company-1", - }, - runtime: {}, - config, - context: options.context ?? {}, - executionTransport: options.executionTransport, - authToken: options.authToken, - executionTarget: options.executionTarget, - runtimeMcp: options.runtimeMcp, - startupTraceContext: options.startupTraceContext, - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - onMeta: async (payload: unknown) => { - meta.push(payload as Record); - }, - onEvent: async (event: { eventType: string; payload?: Record }) => { - events.push(event); - }, - } as never); - - expect(result.exitCode).toBe(0); - return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result }; -} - -// A stub ACP runtime that yields a controlled event stream and terminal result. -// The tests supply the events/result/cancel behavior; the harness above only -// drives always-happy sessions, so the turn tests build their own runtime here. -function turnRuntime(input: { - events: () => AsyncGenerator>; - result: Promise>; - onStartTurn?: (turnInput: Record) => void; - onCancel?: (reason: string) => void; - onClose?: () => void; - getStatus?: () => Promise>; - onEnsureSession?: (session: Record) => void; - ensureSession?: (session: Record) => Promise>; -}) { - const runtime: Record = { - ensureSession: - input.ensureSession ?? - (async (session: Record) => { - input.onEnsureSession?.(session); - return { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }; - }), - startTurn: (turnInput: Record) => { - input.onStartTurn?.(turnInput); - return { - events: input.events(), - result: input.result, - cancel: async ({ reason }: { reason: string }) => { - input.onCancel?.(reason); - }, - }; - }, - close: async () => { - input.onClose?.(); - }, - }; - if (input.getStatus) runtime.getStatus = input.getStatus; - return runtime; -} - -describe("ACPX engine turn characterization", () => { - // The stub session handle every ensureSession returns. startTurn must receive - // this exact handle object. - const SESSION_HANDLE = { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }; - - it("passes the turn inputs and terminal failure callback to startTurn", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let captured: Record | null = null; - let metaPrompt = ""; - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - onStartTurn: (turnInput) => { - captured = turnInput; - }, - events: async function* () { - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - - const result = await execute({ - runId: "run-six-inputs", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, timeoutSec: 120 }, - context: {}, - onLog: async () => {}, - onMeta: async (payload: { prompt?: string }) => { - metaPrompt = payload.prompt ?? ""; - }, - } as never); - - expect(result.exitCode).toBe(0); - const input = captured!; - // The handle is the exact session handle ensureSession returned. - expect(input.handle).toEqual(SESSION_HANDLE); - // The text is the built run prompt, the same string reported to onMeta. - expect(input.text).toBe(metaPrompt); - expect(typeof input.text).toBe("string"); - expect((input.text as string).length).toBeGreaterThan(0); - // The turn always runs in prompt mode. - expect(input.mode).toBe("prompt"); - // The request id is the run id. - expect(input.requestId).toBe("run-six-inputs"); - // A positive timeoutSec becomes timeoutMs in milliseconds. - expect(input.timeoutMs).toBe(120_000); - // The abort signal is present and not yet aborted. - const signal = input.signal as AbortSignal; - expect(signal).toBeInstanceOf(AbortSignal); - expect(signal.aborted).toBe(false); - // Every adapter receives diagnostics, even without a failure classifier. - expect(input.onTerminalSessionFailure).toBeTypeOf("function"); - expect(Object.keys(input).sort()).toEqual( - ["handle", "mode", "onTerminalSessionFailure", "requestId", "signal", "text", "timeoutMs"].sort(), - ); - }); - - it("joins text_delta events into the trimmed result summary", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - events: async function* () { - yield { type: "text_delta", text: " Hello, ", stream: "output", tag: "agent_message_chunk" }; - yield { type: "text_delta", text: "world ", stream: "output", tag: "agent_message_chunk" }; - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - - const result = await execute({ - runId: "run-summary", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(0); - // The summary is the concatenation of the deltas, trimmed. - expect(result.summary).toBe("Hello, world"); - }); - - it("falls back to the stop reason for the summary when no text streams", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - events: async function* () { - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - - const result = await execute({ - runId: "run-no-text", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(0); - // With no streamed text, the summary is the terminal stop reason. - expect(result.summary).toBe("end_turn"); - }); - - it("pins one event sequence across the log transcript and structured event transports", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const logs: Array<{ stream: string; text: string }> = []; - const events: Array<{ eventType: string; payload?: Record }> = []; - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - events: async function* () { - yield { type: "text_delta", text: "streamed hello", stream: "output", tag: "agent_message_chunk" }; - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - - const result = await execute({ - runId: "run-transports", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context: {}, - onLog: async (stream: "stdout" | "stderr", text: string) => { - logs.push({ stream, text }); - }, - onMeta: async () => {}, - onEvent: async (event: { eventType: string; payload?: Record }) => { - events.push(event); - }, - } as never); - - expect(result.exitCode).toBe(0); - // Transport 1 — the stdout transcript carries the text_delta as an acpx record. - expect(logs).toContainEqual({ - stream: "stdout", - text: `${JSON.stringify({ - type: "acpx.text_delta", - text: "streamed hello", - channel: "output", - tag: "agent_message_chunk", - })}\n`, - }); - // The joined text also lands in the result summary. - expect(result.summary).toBe("streamed hello"); - // Transport 2 — the structured onEvent stream reflects the run bring-up - // sequence as run.startup.step events, including the acp handshake. - const steps = events.filter((event) => event.eventType === "run.startup.step"); - const stepNames = steps.map((event) => String(event.payload?.step)); - expect(stepNames).toContain("acp.handshake"); - expect(stepNames).toContain("workspace.resolve"); - }); - - it("aborts a hung turn on the wall-clock timer and cancels with the timeout message", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const cwd = path.join(root, "worktree"); - await fs.mkdir(cwd, { recursive: true }); - - const cancelReasons: string[] = []; - let releaseTurn: (() => void) | null = null; - const turnCancelled = new Promise((resolve) => { - releaseTurn = resolve; - }); - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - // The stream never yields on its own. Only the wall-clock timer's cancel - // unblocks it, simulating a hung run. - events: async function* () { - await turnCancelled; - }, - result: turnCancelled.then(() => ({ status: "cancelled", stopReason: "cancelled" })), - onCancel: (reason) => { - cancelReasons.push(reason); - releaseTurn?.(); - }, - }) as never, - }); - - const result = await execute({ - runId: "run-timeout", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd, timeoutSec: 1 }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - const expectedMessage = - "Run exceeded the adapter execution timeout (timeoutSec=1, configured via adapterConfig.timeoutSec). " + - "Set adapterConfig.timeoutSec to raise it."; - expect(result.timedOut).toBe(true); - expect(result.signal).toBe("SIGTERM"); - expect(result.errorCode).toBe("acpx_timeout"); - expect(result.errorMessage).toBe(expectedMessage); - // The cancel ran with the formatted timeout message. - expect(cancelReasons).toContain(expectedMessage); - }, 15_000); - - it("cancels the turn before closing the runtime when the turn throws", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const order: string[] = []; - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - // The event stream throws mid-turn, so the catch path runs teardown. - events: async function* () { - throw new Error("turn boom"); - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - onCancel: () => order.push("cancel"), - onClose: () => order.push("close"), - }) as never, - }); - - const result = await execute({ - runId: "run-throw", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - // A failure after startTurn returned reports phase "turn". - expect((result.resultJson as Record)?.phase).toBe("turn"); - // Cancel runs before close, in that exact order. - expect(order).toEqual(["cancel", "close"]); - }); - - it("retries the session resume once and never retries the turn", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const cwd = path.join(root, "worktree"); - await fs.mkdir(cwd, { recursive: true }); - const config = { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd }; - - const ensureInputs: Array> = []; - let startTurnCalls = 0; - let resumeLogged = false; - - // A first clean run mints the session params the second run resumes from. - const firstExecute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - onEnsureSession: (session) => ensureInputs.push(session), - onStartTurn: () => { - startTurnCalls += 1; - }, - events: async function* () { - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - const first = await firstExecute({ - runId: "run-resume-a", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - expect(first.exitCode).toBe(0); - - // The second run resumes. Its first ensureSession (the resume) fails with a - // resume-shaped error; the fresh retry (no resumeSessionId) succeeds. - const secondExecute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - ensureSession: async (session: Record) => { - ensureInputs.push(session); - if (session.resumeSessionId) { - throw new Error("resume session not found"); - } - return { - backendSessionId: "backend-session", - agentSessionId: "agent-session", - runtimeSessionName: "runtime-session", - }; - }, - onStartTurn: () => { - startTurnCalls += 1; - }, - events: async function* () { - yield { type: "done", stopReason: "end_turn" }; - }, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - const second = await secondExecute({ - runId: "run-resume-b", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: { sessionParams: (first as { sessionParams?: unknown }).sessionParams }, - config, - context: {}, - onLog: async (_stream: string, text: string) => { - if (text.includes("is unavailable; retrying with a fresh session")) resumeLogged = true; - }, - onMeta: async () => {}, - } as never); - - expect(second.exitCode).toBe(0); - // The first run's single ensureSession, plus the second run's resume and - // fresh retry, make three ensureSession calls total; two on the second run. - expect(ensureInputs).toHaveLength(3); - expect(ensureInputs[1]?.resumeSessionId).toBe(first.sessionId); - expect(ensureInputs[2]?.resumeSessionId).toBeUndefined(); - // The turn is never retried: one startTurn per run, two across both runs. - expect(startTurnCalls).toBe(2); - // The engine logged the resume fallback. - expect(resumeLogged).toBe(true); - // The fresh retry clears the stale session for the caller. - expect(second.clearSession).toBe(true); - }); - - it("maps a failed terminal to acpx_turn_failed and folds in the reported usage", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - events: async function* () { - yield { - type: "status", - text: "usage", - tag: "usage_update", - cost: { amount: 0.31, currency: "USD" }, - breakdown: { inputTokens: 40, outputTokens: 700, cachedReadTokens: 60 }, - }; - yield { type: "done", stopReason: "failed" }; - }, - result: Promise.resolve({ status: "failed", error: new Error("boom") }), - }) as never, - }); - - const result = await execute({ - runId: "run-failed", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context: {}, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - expect(result.errorCode).toBe("acpx_turn_failed"); - // The failed error message becomes the terminal stop reason and the summary. - expect(result.summary).toBe("boom"); - expect((result.resultJson as Record)?.stopReason).toBe("boom"); - // The usage math folds the usage_update event into per-run usage and cost. - expect(result.usage).toEqual({ inputTokens: 40, outputTokens: 700, cachedInputTokens: 60 }); - expect(result.usageBasis).toBe("per_run"); - expect(result.costUsd).toBeCloseTo(0.31); - }); - - it("computes usage the same way summarizeAcpxTurnUsage does for the event fallback", () => { - // Pin the exported helper the turn path calls: with no getStatus snapshots the - // event breakdown and cost drive the per-run usage. - const summary = summarizeAcpxTurnUsage({ - preStatus: null, - postStatus: null, - eventBreakdown: { inputTokens: 40, outputTokens: 700, cachedReadTokens: 60 }, - eventCostUsd: 0.31, - }); - expect(summary.usage).toEqual({ inputTokens: 40, outputTokens: 700, cachedInputTokens: 60 }); - expect(summary.costUsd).toBeCloseTo(0.31); - }); - - it("returns a prepare_turn error result when the prompt build throws", async () => { - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - let startTurnCalls = 0; - let closes = 0; - - const execute = createAcpxEngineExecutor({ - createRuntime: () => - turnRuntime({ - onStartTurn: () => { - startTurnCalls += 1; - }, - onClose: () => { - closes += 1; - }, - events: async function* () {}, - result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), - }) as never, - }); - - // A throwing accessor on a field only buildPrompt reads makes the prompt build - // fail after the session handshake succeeds but before startTurn runs. - const context: Record = {}; - Object.defineProperty(context, "paperclipSessionHandoffMarkdown", { - enumerable: false, - get() { - throw new Error("prompt build boom"); - }, - }); - - const result = await execute({ - runId: "run-prepare-fail", - agent: { id: "agent-1", companyId: "company-1" }, - runtime: {}, - config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir }, - context, - onLog: async () => {}, - onMeta: async () => {}, - } as never); - - expect(result.exitCode).toBe(1); - // The pre-turn failure reports phase "prepare_turn". - expect((result.resultJson as Record)?.phase).toBe("prepare_turn"); - // The turn never started, and the runtime closed once. - expect(startTurnCalls).toBe(0); - expect(closes).toBe(1); - }); - - it("threads the same happy-path turn through the shared runExecutor harness", async () => { - // A smoke pin on the copied harness: the always-happy buildRuntime turn exits - // clean with the done stop reason as the summary. - const root = await makeTempRoot(); - const stateDir = path.join(root, "state"); - const { result } = await runExecutor({ - agent: "custom", - agentCommand: "node ./fake-acp.js", - stateDir, - }); - expect(result.exitCode).toBe(0); - expect(result.summary).toBe("end_turn"); - }); -}); diff --git a/packages/adapter-utils/src/execution-target-sandbox.test.ts b/packages/adapter-utils/src/execution-target-sandbox.test.ts index 0655c9f0b26..a8a37861970 100644 --- a/packages/adapter-utils/src/execution-target-sandbox.test.ts +++ b/packages/adapter-utils/src/execution-target-sandbox.test.ts @@ -119,6 +119,81 @@ function createRecordingTraceContext(): { describe("sandbox adapter execution targets", () => { const cleanupDirs: string[] = []; + async function createSandboxRoot(prefix: string): Promise { + const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix)); + cleanupDirs.push(rootDir); + return rootDir; + } + + async function createSandboxWorkspace(prefix: string) { + const rootDir = await createSandboxRoot(prefix); + const remoteCwd = path.join(rootDir, "workspace"); + await mkdir(remoteCwd, { recursive: true }); + return { rootDir, remoteCwd }; + } + + function localSandboxTarget( + remoteCwd: string, + runner: NonNullable = createLocalSandboxRunner(), + ): AdapterSandboxExecutionTarget { + return { + kind: "remote", + transport: "sandbox", + providerKey: "local-test", + remoteCwd, + timeoutMs: 30_000, + runner, + }; + } + + function duplexSandboxTarget( + remoteCwd: string, + runner: NonNullable, + ): AdapterSandboxExecutionTarget { + return { + kind: "remote", + transport: "sandbox", + providerKey: "daytona", + remoteCwd, + timeoutMs: 30_000, + runner, + effectiveCapabilities: duplexCapabilities(true), + }; + } + + function queueSandboxTarget(remoteCwd: string): AdapterSandboxExecutionTarget { + return { + kind: "remote", + transport: "sandbox", + providerKey: "e2b", + environmentId: "env-1", + leaseId: "lease-1", + remoteCwd, + runner: createLocalSandboxRunner(), + timeoutMs: 30_000, + }; + } + + function startTestDuplexBridge( + runId: string, + target: AdapterSandboxExecutionTarget, + hostApiUrl: string, + options: Omit[0], + "runId" | "target" | "runtimeRootDir" | "adapterKey" | "hostApiToken" | "hostApiUrl" | "enableSandboxDuplexBridge" + > = {}, + ) { + return startAdapterExecutionTargetPaperclipBridge({ + runId, + target, + runtimeRootDir: path.join(target.remoteCwd, ".paperclip-runtime", "codex"), + adapterKey: "codex", + hostApiToken: "real-run-jwt", + hostApiUrl, + enableSandboxDuplexBridge: true, + ...options, + }); + } + it("records successful issue comment ids for attribution recovery", () => { expect(postedIssueCommentLogMarker("POST", "/api/issues/issue-1/comments", 201, '{"id":"comment-1"}')) .toBe("comment id: comment-1\n"); @@ -311,18 +386,14 @@ describe("sandbox adapter execution targets", () => { } it("delivers cryptographic identity through a remote sandbox process transport", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "identity-remote-process-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("identity-remote-process-"); const keys = generateKeyPairSync("ed25519"); const identity = { keyId: "sha256:remote-identity", publicKeyPem: keys.publicKey.export({ type: "spki", format: "pem" }).toString(), privateKeyPem: keys.privateKey.export({ type: "pkcs8", format: "pem" }).toString(), }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", transport: "sandbox", providerKey: "local-test", remoteCwd: rootDir, - timeoutMs: 30_000, runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const challenge = randomBytes(32); const result = await runAdapterExecutionTargetProcess("identity-remote", target, process.execPath, ["-e", `const {sign}=require('node:crypto');process.stdout.write(sign(null,Buffer.from(process.argv[1],'hex'),process.env.PAPERCLIP_AGENT_PRIVATE_KEY).toString('base64'));`, challenge.toString("hex")], { @@ -386,8 +457,7 @@ describe("sandbox adapter execution targets", () => { }); it("preserves stdin when wrapping sandbox adapter commands for run-log streaming", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-run-log-stdin-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-run-log-stdin-"); const target: AdapterSandboxExecutionTarget = { kind: "remote", transport: "sandbox", @@ -428,8 +498,7 @@ describe("sandbox adapter execution targets", () => { }); it("creates the process session directories only in the launch exec, not in upfront makeDir execs", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-makedir-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-makedir-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -441,14 +510,7 @@ describe("sandbox adapter execution targets", () => { return delegate.execute(input); }), }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-makedir", @@ -487,10 +549,7 @@ describe("sandbox adapter execution targets", () => { ])( "preserves an explicit remote PATH equal to host PATH in $outputMode mode", async ({ outputMode, streamOutputViaSession }) => { - const rootDir = await mkdtemp( - path.join(os.tmpdir(), `paperclip-process-session-${outputMode}-path-`), - ); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot(`paperclip-process-session-${outputMode}-path-`); const childPath = path.join(rootDir, "print-path-child.mjs"); await writeFile( childPath, @@ -516,14 +575,7 @@ describe("sandbox adapter execution targets", () => { }), ), }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: `run-process-session-${outputMode}-path`, @@ -551,8 +603,7 @@ describe("sandbox adapter execution targets", () => { ); it.each([false, true])("launches a large environment without oversized exec arguments (streamed=%s)", async (streamOutputViaSession) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-large-process-env-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-large-process-env-"); const value = "x".repeat(110_000); const childPath = path.join(rootDir, "child.mjs"); await writeFile(childPath, 'process.stdout.write(process.env.LARGE_CONTEXT ?? "missing");\n'); @@ -600,8 +651,7 @@ describe("sandbox adapter execution targets", () => { }); it("logs a streamed wrapper launch failure before forwarding its exit", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-wrapper-launch-error-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-wrapper-launch-error-"); const delegate = createLocalSandboxRunner(); const logs: string[] = []; const bridge = await startAdapterExecutionTargetProcessSessionBridge({ @@ -626,8 +676,7 @@ describe("sandbox adapter execution targets", () => { }, 10_000); it("removes an incomplete private command payload when upload fails", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-payload-upload-error-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-payload-upload-error-"); const delegate = createLocalSandboxRunner(); await expect(startAdapterExecutionTargetProcessSessionBridge({ runId: "payload-upload-error", adapterKey: "acpx", runtimeRootDir: rootDir, @@ -654,8 +703,7 @@ describe("sandbox adapter execution targets", () => { { streamed: true, corrupt: false }, { streamed: true, corrupt: true }, ])("reports payload read failures without hanging (streamed=$streamed, corrupt=$corrupt)", async ({ streamed, corrupt }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-payload-read-error-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-payload-read-error-"); const delegate = createLocalSandboxRunner(); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "payload-read-error", adapterKey: "acpx", runtimeRootDir: rootDir, @@ -693,8 +741,7 @@ describe("sandbox adapter execution targets", () => { // that token. This test drives the bridge with a getter that returns a known // token, lets the first poll tick fire, and proves the poll exec reads that // token from the active step store. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-poll-parent-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-poll-parent-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -719,14 +766,7 @@ describe("sandbox adapter execution targets", () => { return delegate.execute(input); }, }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-poll-parent", @@ -761,8 +801,7 @@ describe("sandbox adapter execution targets", () => { // With no `getRuntimeParentContext`, the poll tick runs with an empty active // step store, exactly like the earlier `runWithoutActiveStep` behavior. So a // poll `sandbox.exec` span opens unparented with no stale startup flag. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-poll-nogetter-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-poll-nogetter-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -783,14 +822,7 @@ describe("sandbox adapter execution targets", () => { return delegate.execute(input); }, }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-poll-nogetter", @@ -823,8 +855,7 @@ describe("sandbox adapter execution targets", () => { // message in the `data` handler, not once at connect time. This test opens a // socket while `connectParent` is live, switches the getter to `turnParent`, // sends one stdin line, and proves the stdin write ran under `turnParent`. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stdin-parent-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stdin-parent-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -852,14 +883,7 @@ describe("sandbox adapter execution targets", () => { return delegate.execute(input); }, }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-stdin-parent", @@ -923,8 +947,7 @@ describe("sandbox adapter execution targets", () => { // message to the agent in a `sandbox.agentSession.sendInput` span. This test // connects a socket, sends one stdin line, and proves the handler opens that // wrapper span around the write. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-sendinput-span-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-sendinput-span-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -934,14 +957,7 @@ describe("sandbox adapter execution targets", () => { resolveSendInput = resolve; }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-sendinput-span", @@ -995,8 +1011,7 @@ describe("sandbox adapter execution targets", () => { // With a span runner injected, the poll timer wraps each 100 ms poll tick in // a `sandbox.agentSession.pollOutput` span. This test lets the first poll tick // fire and proves the timer opens that wrapper span. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-poll-span-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-poll-span-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); @@ -1006,14 +1021,7 @@ describe("sandbox adapter execution targets", () => { resolvePoll = resolve; }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-poll-span", @@ -1044,8 +1052,7 @@ describe("sandbox adapter execution targets", () => { }); it("bridges bidirectional sandbox process sessions through a local ACPX-spawnable proxy", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-"); const childPath = path.join(rootDir, "fake-acp-child.mjs"); await writeFile( childPath, @@ -1057,14 +1064,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session", @@ -1102,8 +1102,7 @@ describe("sandbox adapter execution targets", () => { streamOutputViaSession, exitCode, }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-open-stdin-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-open-stdin-"); // ACP keeps stdin open while it waits for a handshake. A remote child can // exit before replying; that must close the proxy and fail the handshake. const output = "final output\n".repeat(16_384); @@ -1148,8 +1147,7 @@ describe("sandbox adapter execution targets", () => { }, 15_000); it("flushes remote errors under backpressure and retains only bounded terminal evidence", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-flush-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-terminal-flush-"); const delegate = createLocalSandboxRunner(); const logs: string[] = []; let emit!: (frame: string) => Promise; @@ -1235,8 +1233,7 @@ describe("sandbox adapter execution targets", () => { }, 15_000); it("reports a streamed transport failure even when diagnostic persistence stalls", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-stream-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-terminal-stream-"); const delegate = createLocalSandboxRunner(); const logs: string[] = []; let releaseLog!: () => void; @@ -1268,8 +1265,7 @@ describe("sandbox adapter execution targets", () => { }, 10_000); it("closes a failed output poll without waiting for the run log", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-poll-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-terminal-poll-"); const delegate = createLocalSandboxRunner(); const logs: string[] = []; let releaseLog!: () => void; @@ -1302,8 +1298,7 @@ describe("sandbox adapter execution targets", () => { }, 10_000); it("records an unexpected authenticated proxy close once and ignores unauthenticated peers", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-proxy-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-terminal-proxy-"); const logs: string[] = []; const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "terminal-proxy", adapterKey: "acpx", runtimeRootDir: rootDir, @@ -1336,8 +1331,7 @@ describe("sandbox adapter execution targets", () => { }, 10_000); it("buffers sandbox process session output until the local proxy connects", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-buffer-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-buffer-"); const childPath = path.join(rootDir, "fast-acp-child.mjs"); await writeFile( childPath, @@ -1348,14 +1342,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-buffer", @@ -1383,8 +1370,7 @@ describe("sandbox adapter execution targets", () => { }); it("delivers full output when the sandbox child exits immediately after writing", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-fast-exit-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-fast-exit-"); const childPath = path.join(rootDir, "instant-exit-acp-child.mjs"); await writeFile( childPath, @@ -1394,14 +1380,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-fast-exit", @@ -1428,18 +1407,10 @@ describe("sandbox adapter execution targets", () => { }); it("ignores unauthenticated connections to the process session bridge", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-auth-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-auth-"); const childPath = path.join(rootDir, "guarded-acp-child.mjs"); await writeFile(childPath, "process.stdout.write('guarded-out\\n');", "utf8"); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-auth", @@ -1494,8 +1465,7 @@ describe("sandbox adapter execution targets", () => { }); it("streams sandbox process session output before the remote child exits", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-"); const childPath = path.join(rootDir, "streaming-acp-child.mjs"); await writeFile( childPath, @@ -1512,14 +1482,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-process-session-stream", @@ -1585,8 +1548,7 @@ describe("sandbox adapter execution targets", () => { describe("streamed output (streamOutputViaSession)", () => { it("bridges bidirectional sessions when the wrapper streams output to stdout", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-echo-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-echo-"); const childPath = path.join(rootDir, "echo-acp-child.mjs"); await writeFile( childPath, @@ -1598,14 +1560,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-stream-echo", @@ -1639,8 +1594,7 @@ describe("sandbox adapter execution targets", () => { // step) and stay open around the launch. Record the opened span names and // prove `sandbox.agentProcess` is among them, and that a normal exchange // still works through the wrap. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-span-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-span-"); const childPath = path.join(rootDir, "echo-acp-child.mjs"); await writeFile( childPath, @@ -1651,14 +1605,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const spanNames: string[] = []; const bridge = await startAdapterExecutionTargetProcessSessionBridge({ @@ -1700,18 +1647,10 @@ describe("sandbox adapter execution targets", () => { // `bridge.process-session` bring-up step — otherwise it dangles past its // parent and overlaps `agent.turn`. Build the real run-rooted runner from a // recording trace context and assert the recorded parent. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-parent-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-parent-"); const childPath = path.join(rootDir, "noop-acp-child.mjs"); await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8"); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const { traceContext, spans } = createRecordingTraceContext(); // The run root stands in for `task.run` — the parent the run-rooted runner @@ -1751,8 +1690,7 @@ describe("sandbox adapter execution targets", () => { // at `stop()`, which the caller awaits before it ends `task.run`. Use a // child that ignores stdin and never exits on its own, so the launch // command stays pending across `stop()`, and prove the span ends anyway. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-linger-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-linger-"); const childPath = path.join(rootDir, "linger-acp-child.mjs"); await writeFile( childPath, @@ -1764,14 +1702,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); // Track when each wrapper span's work settles (i.e. when its span ends). const spanRecords: Array<{ name: string; ended: boolean }> = []; @@ -1816,8 +1747,7 @@ describe("sandbox adapter execution targets", () => { }); it("buffers streamed output until the local proxy connects", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-buffer-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-buffer-"); const childPath = path.join(rootDir, "fast-stream-child.mjs"); await writeFile( childPath, @@ -1828,14 +1758,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-stream-buffer", @@ -1866,8 +1789,7 @@ describe("sandbox adapter execution targets", () => { }); it("delivers full streamed output when the sandbox child exits immediately", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-fast-exit-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-fast-exit-"); const childPath = path.join(rootDir, "instant-stream-child.mjs"); await writeFile( childPath, @@ -1877,14 +1799,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-stream-fast-exit", @@ -1912,8 +1827,7 @@ describe("sandbox adapter execution targets", () => { }); it("streams live output before the child exits and never writes output event files", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-live-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-live-"); const childPath = path.join(rootDir, "live-stream-child.mjs"); await writeFile( childPath, @@ -1930,14 +1844,7 @@ describe("sandbox adapter execution targets", () => { ].join("\n"), "utf8", ); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner: createLocalSandboxRunner(), - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-stream-live", @@ -2020,8 +1927,7 @@ describe("sandbox adapter execution targets", () => { // teardown) must run concurrently with the agent, so each must force // itself off the session. On the session they queue behind the agent // command that never returns, and the first handshake write never drains. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-stream-isolation-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-process-session-stream-isolation-"); const childPath = path.join(rootDir, "echo-acp-child.mjs"); await writeFile( childPath, @@ -2053,14 +1959,7 @@ describe("sandbox adapter execution targets", () => { }, ), }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "local-test", - remoteCwd: rootDir, - timeoutMs: 30_000, - runner, - }; + const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner); const bridge = await startAdapterExecutionTargetProcessSessionBridge({ runId: "run-stream-isolation", @@ -2530,8 +2429,7 @@ describe("sandbox adapter execution targets", () => { }); it("starts a localhost Paperclip bridge for sandbox targets in bridge mode", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -2556,16 +2454,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the bridge test API server to listen on a TCP port."); } - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge", @@ -2603,8 +2492,7 @@ describe("sandbox adapter execution targets", () => { }); it("creates a sandbox run log tail factory when bridge streaming is enabled", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-stream-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-stream-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -2647,22 +2535,12 @@ describe("sandbox adapter execution targets", () => { }); it("defaults sandbox run log streaming on and honors the explicit opt-out", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-stream-default-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-stream-default-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); - const baseTarget: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const baseTarget: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const defaultBridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-stream-default", @@ -2842,8 +2720,7 @@ describe("sandbox adapter execution targets", () => { }); it("exposes the Paperclip bridge to the sandbox shell surface", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-shell-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-shell-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "claude"); await mkdir(runtimeRootDir, { recursive: true }); @@ -2956,8 +2833,7 @@ describe("sandbox adapter execution targets", () => { }); it("bounds callback bridge operations independently of the adapter run timeout", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-timeout-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-timeout-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3020,8 +2896,7 @@ describe("sandbox adapter execution targets", () => { // a retryable 502. The in-sandbox server maps the indeterminate 504 to a // non-retryable 409. A retryable status would repeat the mutation with a new // request id outside the broker deduplication set. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-limit-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-limit-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3053,16 +2928,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the bridge test API server to listen on a TCP port."); } - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-limit", @@ -3112,8 +2978,7 @@ describe("sandbox adapter execution targets", () => { // retryable 502 with no indeterminate marker, not the non-retryable 504 the // forward returns for a mutating method. The in-sandbox server passes the 502 // through, so the caller can retry the safe read. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-safe-limit-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-safe-limit-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3142,16 +3007,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the bridge test API server to listen on a TCP port."); } - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-safe-limit", @@ -3195,8 +3051,7 @@ describe("sandbox adapter execution targets", () => { // that header, so the in-sandbox server maps the 504 to a non-retryable 409. // If the forward drops the header, the client sees a retryable 504 and a // retry repeats a mutation that already committed. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-outcome-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-outcome-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3218,16 +3073,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the bridge outcome test API server to listen on a TCP port."); } - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-outcome", @@ -3264,8 +3110,7 @@ describe("sandbox adapter execution targets", () => { }); it("forwards bridge traffic to the local listen origin even when public API URLs are configured", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-local-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-local-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "claude"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3300,16 +3145,7 @@ describe("sandbox adapter execution targets", () => { vi.stubEnv("PAPERCLIP_LISTEN_HOST", "0.0.0.0"); vi.stubEnv("PAPERCLIP_LISTEN_PORT", String(address.port)); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-local", @@ -3342,8 +3178,7 @@ describe("sandbox adapter execution targets", () => { }); it("lets an explicit hostApiUrl input override the bridge forward target", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-override-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-execution-target-bridge-override-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "claude"); await mkdir(runtimeRootDir, { recursive: true }); @@ -3370,16 +3205,7 @@ describe("sandbox adapter execution targets", () => { vi.stubEnv("PAPERCLIP_LISTEN_HOST", "203.0.113.1"); vi.stubEnv("PAPERCLIP_LISTEN_PORT", "9"); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-bridge-override", @@ -3749,10 +3575,7 @@ describe("sandbox adapter execution targets", () => { // The channel open itself fails (a provider startup fault, before any // READY line or preface is possible). The host must fall back to the // file bridge and record the typed open-failure reason, never hang. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-startup-fail-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-startup-fail-"); const api = await startRecordingApiServer(); const base = createLocalSandboxRunner(); let openCount = 0; @@ -3762,24 +3585,9 @@ describe("sandbox adapter execution targets", () => { }; const runner = { ...base, openDuplexChannel }; const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-startup-fail", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-startup-fail", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -3812,10 +3620,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("test_daytona_selects_http2_v1", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-select-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-select-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -3836,15 +3641,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-http2", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-http2", target, api.origin); try { expect(bridge).not.toBeNull(); expect(control.openCount).toBe(1); @@ -3881,10 +3678,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("streams run logs on the http2 path under the same gate and log line as the file path", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-runlog-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-runlog-"); const api = await startRecordingApiServer(); const { runner } = makeHttp2SelectionRunner(); const logs: Array<{ stream: "stdout" | "stderr"; chunk: string }> = []; @@ -3899,14 +3693,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-duplex-log", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-duplex-log", target, api.origin, { onLog: async (stream, chunk) => { logs.push({ stream, chunk }); }, @@ -3927,10 +3714,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("returns no run-log tail on the http2 path when streaming is opted out", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-runlog-off-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-runlog-off-"); const api = await startRecordingApiServer(); const { runner } = makeHttp2SelectionRunner(); const target: AdapterSandboxExecutionTarget = { @@ -3944,15 +3728,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-http2-log-off", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-http2-log-off", target, api.origin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); expect(bridge?.runLogTail ?? null).toBeNull(); @@ -3963,10 +3739,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("routes http2 channel-open and fallback records to a recorder attached on the server seam", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-recorder-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-recorder-"); const api = await startRecordingApiServer(); const counters: DuplexObservabilityCounterRecord[] = []; @@ -4048,10 +3821,7 @@ describe("sandbox adapter execution targets", () => { { name: "the kill switch is off with the capability granted", enable: false, capability: true }, { name: "the capability is absent with the kill switch on", enable: true, capability: false }, ])("selects the file bridge when $name", async ({ enable, capability }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-gate-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-gate-"); const api = await startRecordingApiServer(); const { runner, control } = makeDuplexSelectionRunner(); const target: AdapterSandboxExecutionTarget = { @@ -4107,10 +3877,7 @@ describe("sandbox adapter execution targets", () => { onOpen: () => {}, }, ])("fails closed to the file bridge on $name and leaves no live session", async ({ onOpen }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-fail-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-fail-"); const api = await startRecordingApiServer(); const { runner, control } = makeDuplexSelectionRunner(onOpen); const target: AdapterSandboxExecutionTarget = { @@ -4123,14 +3890,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-fail", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-fail", target, api.origin, { duplexReadinessTimeoutMs: 400, }); try { @@ -4160,10 +3920,7 @@ describe("sandbox adapter execution targets", () => { ])( "rejects a READY frame that carries $name and never sends the bridge token there", async ({ buildReady }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-addr-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-addr-"); // An endpoint an attacker controls. No request that carries the bridge // token may reach it, because the host never derives the endpoint from a @@ -4198,14 +3955,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-addr", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-addr", target, api.origin, { duplexReadinessTimeoutMs: 400, }); try { @@ -4228,10 +3978,7 @@ describe("sandbox adapter execution targets", () => { ); it("test_route_allowlist_header_cleanup_and_token_replacement_hold_on_http2", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-403-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-403-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -4240,25 +3987,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-http2-403", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-http2-403", target, api.origin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -4356,10 +4087,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the echo server to listen on a TCP port."); } - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-raw-bytes-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-raw-bytes-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -4367,25 +4095,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-raw-bytes", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: `http://127.0.0.1:${echoAddress.port}`, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-raw-bytes", target, `http://127.0.0.1:${echoAddress.port}`); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -4461,10 +4173,7 @@ describe("sandbox adapter execution targets", () => { }); it("records an http2 request span with latency and the fixed dimension keys", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-obs-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-obs-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -4474,24 +4183,9 @@ describe("sandbox adapter execution targets", () => { sessionRef.current = ctx.connectHttp2(); }); const { recorder, spans, counters, events } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-obs", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-obs", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4533,10 +4227,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("increments the fallback counter with an approved reason when the capability is absent", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-fb-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-fb-"); const api = await startRecordingApiServer(); const { runner } = makeDuplexSelectionRunner(); const { recorder, counters, events } = createRecordingDuplexRecorder(); @@ -4550,14 +4241,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(false), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-fb", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-fb", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4605,10 +4289,7 @@ describe("sandbox adapter execution targets", () => { ])( "names the open-failure stage $expectedReason and falls back to the file bridge on $name", async ({ error, expectedReason }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-stage-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-stage-"); const api = await startRecordingApiServer(); // A runner whose duplex open rejects. The host binds the caught error and // names the exact open-failure stage. @@ -4618,24 +4299,9 @@ describe("sandbox adapter execution targets", () => { }; const runner = { ...base, openDuplexChannel }; const { recorder, spans, counters, events } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-stage", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-stage", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4666,10 +4332,7 @@ describe("sandbox adapter execution targets", () => { { name: "before any dispatch", dispatchFirst: false, expectedClass: "pre_dispatch" }, { name: "after a dispatch", dispatchFirst: true, expectedClass: "post_dispatch" }, ])("increments the loss counter with the loss class $name", async ({ dispatchFirst, expectedClass }) => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-loss-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-loss-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -4681,24 +4344,9 @@ describe("sandbox adapter execution targets", () => { sessionRef.current = ctx.connectHttp2(); }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-loss", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-loss", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4731,10 +4379,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("keeps serving the request path when the telemetry recorder throws", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-guard-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-guard-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -4744,24 +4389,9 @@ describe("sandbox adapter execution targets", () => { sessionRef.current = ctx.connectHttp2(); }); const { recorder } = createRecordingDuplexRecorder({ failEvery: true }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-guard", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-guard", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4791,10 +4421,7 @@ describe("sandbox adapter execution targets", () => { // error string has no code path into a sink on the http2_v1 transport, so // this test proves the property that does need a live run: the route, // the query, the body, and both tokens never ride a sink either. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-redact-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-redact-"); const api = await startRecordingApiServer(); const ROUTE_SENTINEL = "sentinelroute8f21"; @@ -4813,15 +4440,7 @@ describe("sandbox adapter execution targets", () => { const logLines: string[] = []; const { recorder, spans, counters, events } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); const previousDebug = process.env.PAPERCLIP_BRIDGE_DEBUG; process.env.PAPERCLIP_BRIDGE_DEBUG = "1"; @@ -4881,10 +4500,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("maps a sentinel provider key to the constant other across every sink", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-prov-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-prov-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -4905,14 +4521,7 @@ describe("sandbox adapter execution targets", () => { effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-prov", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-prov", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -4951,10 +4560,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("caps the pre-READY readiness buffer and falls back with a contaminated reason", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-cap-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-cap-"); const api = await startRecordingApiServer(); // The fake gateway sends a large pre-READY blob with no newline, then one // more blob after the gate settles. The gate must cap the buffer, finish with @@ -4966,24 +4572,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitRaw(oversizedBlob); }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-cap", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-cap", target, api.origin, { // A long readiness timeout, so the buffer cap, not the timeout, drives the // failure. duplexReadinessTimeoutMs: 5_000, @@ -5005,10 +4596,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("caps the pre-READY buffer under many small newline-less chunks", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-cap-small-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-cap-small-"); const api = await startRecordingApiServer(); // An adversarial provider controls the chunk size. It sends many small // newline-less chunks that together pass the cap. The gate must scan each @@ -5024,24 +4612,9 @@ describe("sandbox adapter execution targets", () => { } }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-cap-small", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-cap-small", target, api.origin, { // A long readiness timeout, so the buffer cap, not the timeout, drives the // failure. duplexReadinessTimeoutMs: 5_000, @@ -5062,10 +4635,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("bounds the pre-READY newline-scan work by the bytes received", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-scan-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-scan-"); const api = await startRecordingApiServer(); // An adversarial provider sends many small newline-less chunks before the // cap fires. Each chunk must scan only the new bytes, not the whole buffer, @@ -5081,25 +4651,10 @@ describe("sandbox adapter execution targets", () => { } }); const { recorder } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); __duplexReadinessTesting.resetNewlineScanUnits(); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-scan-bound", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-scan-bound", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5118,10 +4673,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("bounds the pre-READY buffer growth-copy work by the bytes received", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-growth-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-growth-"); const api = await startRecordingApiServer(); // An adversarial provider sends many small newline-less fragments before the // cap fires. A one-copy-per-fragment append copies the whole retained buffer @@ -5138,25 +4690,10 @@ describe("sandbox adapter execution targets", () => { } }); const { recorder } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); __duplexReadinessTesting.resetBufferGrowthCopyUnits(); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-growth-bound", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-growth-bound", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5176,10 +4713,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("bounds the pre-READY skip scan work by the bytes received", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-blank-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-blank-"); const api = await startRecordingApiServer(); // An adversarial provider sends one pre-READY chunk of many blank lines and a // noise line, then a valid READY frame. The gate must skip each blank line and @@ -5196,25 +4730,10 @@ describe("sandbox adapter execution targets", () => { const readyLine = '{"version":2,"type":"ready","nonce":""}\n'; const totalBytes = noisePrefix.length + readyLine.length; const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); __duplexReadinessTesting.resetNewlineScanUnits(); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-blank-scan", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-blank-scan", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5238,10 +4757,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("test_a_prologue_of_any_length_before_the_ready_line_is_discarded", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-noise-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-noise-"); const api = await startRecordingApiServer(); // A PTY channel echoes the launch wrapper line before it sets raw mode, so the // first line the host reads is a non-frame echo, not the READY frame. The gate @@ -5257,24 +4773,9 @@ describe("sandbox adapter execution targets", () => { ctx.connectHttp2(); }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-noise-ready", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-noise-ready", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5294,10 +4795,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("settles a wrong-nonce READY frame as a nonce mismatch, even after a noise line", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-noise-nonce-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-noise-nonce-"); const api = await startRecordingApiServer(); // The gate skips the echo line, then reads a READY frame that decodes cleanly // but carries a wrong nonce. A wrong-nonce READY authenticates as a failure, @@ -5308,24 +4806,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitFrame({ version: 2, type: "ready", nonce: "00000000000000000000000000000000" }); }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-noise-nonce", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-noise-nonce", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5345,10 +4828,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("enforces the buffer cap on an over-cap blank prefix before it accepts a valid READY frame", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-capbypass-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-capbypass-"); const api = await startRecordingApiServer(); // The cap must bound every pre-READY path, including a skipped blank line. An // adversarial provider sends one chunk: an over-cap blank prefix followed by a @@ -5365,24 +4845,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitRaw("\n".repeat(readinessBufferCapBytes + 1) + readyLine); }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-cap-bypass", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-cap-bypass", target, api.origin, { // A long readiness timeout, so the cap, not the timeout, drives the failure. duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, @@ -5403,10 +4868,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("records the channel-open span with the fallback_reason dimension on the fallback path", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-duplex-openspan-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-duplex-openspan-"); const api = await startRecordingApiServer(); // A wrong-nonce READY frame fails the handshake, so the gate falls back. The // channel-open span records the failed attempt on the duplex transport and now @@ -5416,24 +4878,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitFrame({ version: 2, type: "ready", nonce: "00000000000000000000000000000000" }), ); const { recorder, spans } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-open-span", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-open-span", target, api.origin, { duplexReadinessTimeoutMs: 5_000, duplexObservabilityRecorder: recorder, }); @@ -5456,10 +4903,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("drops a header outside the allowlist on the host http2 forward path", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-hdr-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-hdr-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -5468,25 +4912,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-hdr", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-hdr", target, api.origin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -5519,10 +4947,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("selects the http2 transport for a large forward budget", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-budget-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-budget-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -5531,28 +4956,13 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); // A forward budget past the default response budget (32 s). The http2 path // holds no nested-budget derivation (that budget set belonged to the retired // duplex_v1 broker only), so a large forward budget must still select and // serve normally. - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-budget", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-budget", target, api.origin, { forwardTimeoutMs: 60_000, }); try { @@ -5572,10 +4982,7 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("test_a_missing_preface_aborts_the_open_and_falls_back_to_queue_v1", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-no-preface-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-no-preface-"); const api = await startRecordingApiServer(); // The gateway sends a valid READY line, but no HTTP/2 client preface ever // follows (a suppressed or a stalled sandbox client). Readiness passes, @@ -5587,24 +4994,9 @@ describe("sandbox adapter execution targets", () => { // No connectHttp2() call: the client preface never arrives. }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-no-preface", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-no-preface", target, api.origin, { // A short readiness timeout, so the preface-missing bound (which reapplies // this same value) fires quickly in the test. duplexReadinessTimeoutMs: 500, @@ -5626,22 +5018,11 @@ describe("sandbox adapter execution targets", () => { }, 20000); it("test_disabled_flag_selects_queue_v1", async () => { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-disabled-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-disabled-"); const api = await startRecordingApiServer(); const { runner, control } = makeHttp2SelectionRunner(); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); // The kill switch is off. The host must never open the channel at all. const bridge = await startAdapterExecutionTargetPaperclipBridge({ @@ -5670,34 +5051,16 @@ describe("sandbox adapter execution targets", () => { // The open attempt runs once per run, with no retry loop: a preface // failure falls through to the file bridge exactly one time, and the // host never re-attempts http2_v1 afterward in the same run. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-one-way-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-one-way-"); const api = await startRecordingApiServer(); const { runner, control } = makeHttp2SelectionRunner((ctx) => { ctx.emitReady(); // No connectHttp2() call: the preface never arrives, so the open fails. }); const { recorder, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-one-way", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-one-way", target, api.origin, { duplexReadinessTimeoutMs: 500, duplexObservabilityRecorder: recorder, }); @@ -5725,10 +5088,7 @@ describe("sandbox adapter execution targets", () => { // READY: it holds the channel open, with no client preface arriving // (so the http2 server, if bound, would try to write its own SETTINGS // frame), and asserts zero bytes crossed the channel the whole time. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-no-early-write-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-no-early-write-"); const api = await startRecordingApiServer(); const base = createLocalSandboxRunner(); const hostWrites: Buffer[] = []; @@ -5766,24 +5126,9 @@ describe("sandbox adapter execution targets", () => { return channel; }; const runner = { ...base, openDuplexChannel }; - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-no-early-write", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-no-early-write", target, api.origin, { duplexReadinessTimeoutMs: 300, }); try { @@ -5805,10 +5150,7 @@ describe("sandbox adapter execution targets", () => { // bytes are deliberately sentinel-marked and syntactically invalid (not // valid UTF-8 JSON), then a valid READY frame. No log line — on any // stream — may contain the sentinel bytes. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-invalid-ready-bytes-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-invalid-ready-bytes-"); const api = await startRecordingApiServer(); const INVALID_LINE_SENTINEL = "sentinelinvalidreadyline62fa"; const logLines: string[] = []; @@ -5819,24 +5161,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-invalid-ready-bytes", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-invalid-ready-bytes", target, api.origin, { onLog: async (_stream, chunk) => { logLines.push(chunk); }, @@ -5856,10 +5183,7 @@ describe("sandbox adapter execution targets", () => { // A loss ordered after a host-observed orderly completion is a normal // teardown, not a failure: the run already completed. The disposition // latch must keep the success and emit no loss event for it. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-orderly-close-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-orderly-close-"); const api = await startRecordingApiServer(); let emitExit: (() => void) | null = null; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -5868,24 +5192,9 @@ describe("sandbox adapter execution targets", () => { ctx.connectHttp2(); }); const { recorder, events, counters } = createRecordingDuplexRecorder(); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-orderly-close", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-orderly-close", target, api.origin, { duplexObservabilityRecorder: recorder, }); try { @@ -5911,10 +5220,7 @@ describe("sandbox adapter execution targets", () => { // retryable: the host answers 502 with no indeterminate marker, the same // rule `forwardBridgeRequest` already applies on every transport. This // proves the http2_v1 forward handler reuses that one function unchanged. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-safe-retry-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-safe-retry-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -5923,24 +5229,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-safe-retry", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-safe-retry", target, api.origin, { // A ceiling far under any real response, so every response-body read // fails the size check deterministically. maxBodyBytes: 1, @@ -5972,10 +5263,7 @@ describe("sandbox adapter execution targets", () => { // host and the gateway share one resolved ceiling: this request carries // no body, so only the mock host's own response — comfortably over one // byte — trips the size check this test exists to force. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-unsafe-indeterminate-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-unsafe-indeterminate-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -5984,24 +5272,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-unsafe-indeterminate", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-unsafe-indeterminate", target, api.origin, { maxBodyBytes: 1, }); try { @@ -6048,10 +5321,7 @@ describe("sandbox adapter execution targets", () => { } const apiOrigin = `http://127.0.0.1:${apiAddress.port}`; - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-abort-forward-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-abort-forward-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -6059,24 +5329,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-abort-forward", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: apiOrigin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-abort-forward", target, apiOrigin, { // Far longer than this test waits, so only the sandbox-side stream // close — not this ceiling — can end the forward here. forwardTimeoutMs: 60_000, @@ -6133,10 +5388,7 @@ describe("sandbox adapter execution targets", () => { } const apiOrigin = `http://127.0.0.1:${apiAddress.port}`; - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-denied-response-chunk-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-denied-response-chunk-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -6144,25 +5396,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-denied-response-chunk", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: apiOrigin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-denied-response-chunk", target, apiOrigin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -6221,10 +5457,7 @@ describe("sandbox adapter execution targets", () => { } const apiOrigin = `http://127.0.0.1:${apiAddress.port}`; - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-denied-response-concat-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-denied-response-concat-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -6232,25 +5465,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-denied-response-concat", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: apiOrigin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-denied-response-concat", target, apiOrigin); const concatSpy = vi.spyOn(Buffer, "concat"); const readerCancelSpy = vi.spyOn(ReadableStreamDefaultReader.prototype, "cancel"); try { @@ -6336,10 +5553,7 @@ describe("sandbox adapter execution targets", () => { } const apiOrigin = `http://127.0.0.1:${apiAddress.port}`; - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-denied-response-mutation-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-denied-response-mutation-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -6347,25 +5561,9 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-denied-response-mutation", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: apiOrigin, - enableSandboxDuplexBridge: true, - }); + const bridge = await startTestDuplexBridge("run-denied-response-mutation", target, apiOrigin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -6416,10 +5614,7 @@ describe("sandbox adapter execution targets", () => { } const apiOrigin = `http://127.0.0.1:${apiAddress.port}`; - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-ceiling-aggregate-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-ceiling-aggregate-"); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; const { runner } = makeHttp2SelectionRunner((ctx) => { @@ -6427,24 +5622,8 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-ceiling-aggregate", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: apiOrigin, - enableSandboxDuplexBridge: true, - }); + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); + const bridge = await startTestDuplexBridge("run-ceiling-aggregate", target, apiOrigin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -6490,28 +5669,10 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-host-body-limit-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-host-body-limit-"); + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-host-body-limit", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-host-body-limit", target, api.origin, { // A ceiling far under the body this test sends, resolved for this // run. `createHttp2BridgeServer()` must receive this same resolved // value, so the host itself enforces it on the raw wire — this test @@ -6549,8 +5710,7 @@ describe("sandbox adapter execution targets", () => { // with no `reservation` option at all. `readBridgeForwardResponseBody` // must behave exactly as it did before that option existed: it still // forwards the request and still returns the host's body. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-queue-no-reservation-")); - cleanupDirs.push(rootDir); + const rootDir = await createSandboxRoot("paperclip-queue-no-reservation-"); const remoteCwd = path.join(rootDir, "workspace"); const runtimeRootDir = path.join(remoteCwd, ".paperclip-runtime", "codex"); await mkdir(runtimeRootDir, { recursive: true }); @@ -6568,16 +5728,7 @@ describe("sandbox adapter execution targets", () => { throw new Error("Expected the queue-transport test API server to listen on a TCP port."); } - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "e2b", - environmentId: "env-1", - leaseId: "lease-1", - remoteCwd, - runner: createLocalSandboxRunner(), - timeoutMs: 30_000, - }; + const target: AdapterSandboxExecutionTarget = queueSandboxTarget(remoteCwd); const bridge = await startAdapterExecutionTargetPaperclipBridge({ runId: "run-queue-no-reservation", @@ -6616,10 +5767,7 @@ describe("sandbox adapter execution targets", () => { // --------------------------------------------------------------------------- async function runReadinessReplay(emit: (ctx: Http2OpenContext) => void) { - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pty-replay-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-pty-replay-"); const api = await startRecordingApiServer(); const { runner, control } = makeHttp2SelectionRunner(emit); const target: AdapterSandboxExecutionTarget = { @@ -6633,14 +5781,7 @@ describe("sandbox adapter execution targets", () => { runner, effectiveCapabilities: duplexCapabilities(true), }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-pty-replay", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, + const bridge = await startTestDuplexBridge("run-pty-replay", target, api.origin, { duplexReadinessTimeoutMs: 2_000, }); const mode = bridge?.env.PAPERCLIP_API_BRIDGE_MODE; @@ -6753,10 +5894,7 @@ describe("sandbox adapter execution targets", () => { // HTTP/2 server. A pre-preface byte would make the server report a // `PROTOCOL_ERROR`; this test proves the real session opens cleanly // instead, which only holds when the offset is exact. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-preface-offset-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-preface-offset-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -6767,24 +5905,8 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-preface-offset", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); + const bridge = await startTestDuplexBridge("run-preface-offset", target, api.origin); try { expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("http2_v1"); await waitForCondition(() => sessionRef.current !== null, "the http2 client session to open", 4000); @@ -6808,10 +5930,7 @@ describe("sandbox adapter execution targets", () => { // the shell. This test embeds the exact preface bytes in the pre-READY // noise, then proves a session still starts only at the REAL preface // that follows READY. - const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-http2-preface-lookalike-")); - cleanupDirs.push(rootDir); - const remoteCwd = path.join(rootDir, "workspace"); - await mkdir(remoteCwd, { recursive: true }); + const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-preface-lookalike-"); const api = await startRecordingApiServer(); const sessionRef: { current: http2.ClientHttp2Session | null } = { current: null }; let bridgeToken = ""; @@ -6822,24 +5941,8 @@ describe("sandbox adapter execution targets", () => { ctx.emitReady(); sessionRef.current = ctx.connectHttp2(); }); - const target: AdapterSandboxExecutionTarget = { - kind: "remote", - transport: "sandbox", - providerKey: "daytona", - remoteCwd, - timeoutMs: 30_000, - runner, - effectiveCapabilities: duplexCapabilities(true), - }; - const bridge = await startAdapterExecutionTargetPaperclipBridge({ - runId: "run-preface-lookalike", - target, - runtimeRootDir: path.join(remoteCwd, ".paperclip-runtime", "codex"), - adapterKey: "codex", - hostApiToken: "real-run-jwt", - hostApiUrl: api.origin, - enableSandboxDuplexBridge: true, - }); + const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner); + const bridge = await startTestDuplexBridge("run-preface-lookalike", target, api.origin); try { expect(bridge).not.toBeNull(); expect(control.openCount).toBe(1); diff --git a/packages/adapter-utils/src/execution-target.test.ts b/packages/adapter-utils/src/execution-target.test.ts index 54fc73c639d..be9dfde46e3 100644 --- a/packages/adapter-utils/src/execution-target.test.ts +++ b/packages/adapter-utils/src/execution-target.test.ts @@ -11,8 +11,27 @@ import { resolveAdapterExecutionTargetCwd, runAdapterExecutionTargetProcess, runAdapterExecutionTargetShellCommand, + type AdapterSshExecutionTarget, } from "./execution-target.js"; +function sshExecutionTarget(): AdapterSshExecutionTarget { + return { + kind: "remote", + transport: "ssh", + remoteCwd: "/srv/paperclip/workspace", + spec: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteCwd: "/srv/paperclip/workspace", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }; +} + describe("runAdapterExecutionTargetShellCommand", () => { afterEach(() => { vi.restoreAllMocks(); @@ -27,21 +46,7 @@ describe("runAdapterExecutionTargetShellCommand", () => { await runAdapterExecutionTargetShellCommand( "run-1", - { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + sshExecutionTarget(), `printf '%s\\n' "$HOME" && echo "it's ok"`, { cwd: "/tmp/local", @@ -74,21 +79,7 @@ describe("runAdapterExecutionTargetShellCommand", () => { await runAdapterExecutionTargetShellCommand( "run-1b", - { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + sshExecutionTarget(), "env", { cwd: "/tmp/local", @@ -122,21 +113,7 @@ describe("runAdapterExecutionTargetShellCommand", () => { const result = await runAdapterExecutionTargetShellCommand( "run-2", - { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + sshExecutionTarget(), "sleep 10", { cwd: "/tmp/local", @@ -167,21 +144,7 @@ describe("runAdapterExecutionTargetShellCommand", () => { const result = await runAdapterExecutionTargetShellCommand( "run-3", - { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + sshExecutionTarget(), "false", { cwd: "/tmp/local", @@ -272,21 +235,7 @@ describe("runAdapterExecutionTargetProcess", () => { await runAdapterExecutionTargetProcess( "run-ssh-process", - { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + sshExecutionTarget(), "agent-cli", ["--json"], { @@ -367,21 +316,7 @@ describe("ensureAdapterExecutionTargetRuntimeCommandInstalled", () => { await ensureAdapterExecutionTargetRuntimeCommandInstalled({ runId: "run-skip", - target: { - kind: "remote", - transport: "ssh", - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }, + target: sshExecutionTarget(), installCommand: "npm install -g @google/gemini-cli", cwd: "/tmp/local", env: {}, @@ -392,21 +327,7 @@ describe("ensureAdapterExecutionTargetRuntimeCommandInstalled", () => { }); describe("resolveAdapterExecutionTargetCwd", () => { - const sshTarget = { - kind: "remote" as const, - transport: "ssh" as const, - remoteCwd: "/srv/paperclip/workspace", - spec: { - host: "ssh.example.test", - port: 22, - username: "ssh-user", - remoteCwd: "/srv/paperclip/workspace", - remoteWorkspacePath: "/srv/paperclip/workspace", - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, - }; + const sshTarget = sshExecutionTarget(); it("falls back to the remote cwd when no adapter cwd is configured", () => { expect(resolveAdapterExecutionTargetCwd(sshTarget, "", "/Users/host/repo/server")).toBe( diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts index 0f479a0121d..e77ca65d159 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts @@ -664,6 +664,7 @@ describe("sandbox managed runtime", () => { }, }; const runtimeStatuses: string[] = []; + const run = vi.spyOn(client, "run"); attachFallbackSyncIn(client); const prepared = await prepareSandboxManagedRuntime({ @@ -720,6 +721,7 @@ describe("sandbox managed runtime", () => { expect.stringMatching(/^restore:Restoring workspace from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/), ])); expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace"); + expect(run.mock.calls.some(([command]) => command.includes("workspace-download.tar"))).toBe(true); }); it("falls back to the host-known byte count when the provider reports 0 for inbound workspace sync", async () => { @@ -1057,6 +1059,7 @@ describe("sandbox managed runtime", () => { await expect(readFile(path.join(localWorkspaceDir, "deleted.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); await expect(readFile(path.join(localWorkspaceDir, "node_modules", "cache.bin"), "utf8")).resolves.toBe("do not upload\n"); + expect(runtimeStatuses.at(-1)?.phase).toBe("finalize"); expect(downloadedTars).toHaveLength(1); const downloadMembers = await listTarMembers(rootDir, "workspace-download-list.tar", downloadedTars[0]!.bytes); expect(downloadMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false); @@ -4096,6 +4099,26 @@ describe("sandbox managed runtime outbound coordinator", () => { expect(control.settled).toEqual(expect.arrayContaining(["workspace", "home", "private-asset"])); }); + it("restores non-git workspace bytes through native syncOut without a fallback tar", async () => { + const { workspaceDir, remoteWorkspaceDir } = await makeOutboundDirs([]); + const client = makeGatedOutboundClient(false, async (_label, run) => run()); + const syncOut = vi.spyOn(client, "syncOut"); + const run = vi.spyOn(client, "run"); + const prepared = await prepareSandboxManagedRuntime({ + spec: makeSpec(remoteWorkspaceDir), adapterKey: "test-adapter", client, workspaceLocalDir: workspaceDir, + }); + await writeFile(path.join(remoteWorkspaceDir, "file.txt"), "updated native bytes\n", "utf8"); + await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "native sync\n", "utf8"); + await prepared.restoreWorkspace(); + expect(syncOut).toHaveBeenCalledTimes(1); + expect(syncOut.mock.calls[0]?.[0].flatMap((operation) => operation.files)).toEqual([ + expect.objectContaining({ kind: "directory", sourcePath: remoteWorkspaceDir }), + ]); + await expect(readFile(path.join(workspaceDir, "file.txt"), "utf8")).resolves.toBe("updated native bytes\n"); + await expect(readFile(path.join(workspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("native sync\n"); + expect(run.mock.calls.some(([command]) => command.includes("workspace-download.tar"))).toBe(false); + }); + it("with concurrency permitted, an asset restore starts while the workspace restore is held open", async () => { const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]); const { control, gate } = makeOutboundControl(); diff --git a/packages/paperclip-runner/docs/durable-recovery.md b/packages/paperclip-runner/docs/durable-recovery.md index 6a9b6e3fbb0..1e8bc2acf22 100644 --- a/packages/paperclip-runner/docs/durable-recovery.md +++ b/packages/paperclip-runner/docs/durable-recovery.md @@ -283,7 +283,7 @@ model or rely on an LLM choosing the desired timing. | Concurrent writes held at filesystem commit; authority restart | One write, one audit record, same receipt; revoked authorization rejected | Server `agent-instruction-tools.integration.test.ts` | | Failure after file rename or rollback while saving the receipt | Durable attempt and audit evidence survive; no success claim and no second write | Server `agent-instruction-tools.integration.test.ts` | | Receipt commits but its database acknowledgement is lost | Exact original result replays; one file-update audit and one attempt record | Server `agent-instruction-tools.integration.test.ts` | -| Tool handler throws an unknown-outcome error versus a validation error | Unknown propagates without a completed tool event; validation returns an ordinary failed response | `codex-app-server-driver.test.ts` | +| Tool handler throws an unknown-outcome error versus a validation error | Unknown propagates without a completed tool event; validation returns an ordinary failed response | `codex-app-server-driver.semantic-results.test.ts` | | Invalid arguments, oversized input, or stale base; restart and replay | Original failure replays with zero writes, even after the base becomes valid | Server `agent-instruction-tools.integration.test.ts` | | Failure receipt cannot be stored | No unrecorded final failure; attempt remains unsettled and never reexecutes | Server `agent-instruction-tools.integration.test.ts` | | Long Unicode input and result; corrupted input digest | Exact bytes across persistence/wire; no copied input in result; bad proof blocks settlement | Rust durable-state tests; `durable-prp-control-plane.test.ts` | diff --git a/packages/paperclip-runner/docs/live-console-protocol-server.md b/packages/paperclip-runner/docs/live-console-protocol-server.md index aa8e9de4409..4377e6b6d03 100644 --- a/packages/paperclip-runner/docs/live-console-protocol-server.md +++ b/packages/paperclip-runner/docs/live-console-protocol-server.md @@ -130,6 +130,6 @@ Run deterministic conformance first, then the real Codex boundary: ```sh pnpm --filter @paperclipai/paperclip-runner exec vitest run \ - src/drivers/codex/codex-app-server-driver.test.ts \ + src/drivers/codex/codex-app-server-driver \ src/mock-core/live-console-demo-server.test.ts ``` diff --git a/packages/paperclip-runner/docs/tutorials/codex.md b/packages/paperclip-runner/docs/tutorials/codex.md index e4c0d1126d5..33317529338 100644 --- a/packages/paperclip-runner/docs/tutorials/codex.md +++ b/packages/paperclip-runner/docs/tutorials/codex.md @@ -42,7 +42,7 @@ use a separate clean operator workspace rather than bypassing that guard. pnpm --filter @paperclipai/paperclip-runner typecheck:typescript pnpm --filter @paperclipai/paperclip-runner exec vitest run \ src/drivers/codex/app-server-transport.test.ts \ - src/drivers/codex/codex-app-server-driver.test.ts + src/drivers/codex/codex-app-server-driver pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports ``` diff --git a/packages/paperclip-runner/docs/tutorials/live-console-protocol-server.md b/packages/paperclip-runner/docs/tutorials/live-console-protocol-server.md index 6c13ccdad4c..c8062fd778a 100644 --- a/packages/paperclip-runner/docs/tutorials/live-console-protocol-server.md +++ b/packages/paperclip-runner/docs/tutorials/live-console-protocol-server.md @@ -14,11 +14,11 @@ From the repository root, run: ```sh pnpm --filter @paperclipai/paperclip-runner exec vitest run \ - src/drivers/codex/codex-app-server-driver.test.ts \ + src/drivers/codex/codex-app-server-driver \ src/mock-core/live-console-demo-server.test.ts ``` -Expected result: both files pass. The tests cover requests, goals, lineage, +Expected result: the Codex driver suites and Live console server suite pass. The tests cover requests, goals, lineage, steering, interrupt races, reconnect, replay, and redaction. ## Step 2: Start the server diff --git a/packages/paperclip-runner/src/drivers/codex/AGENTS.md b/packages/paperclip-runner/src/drivers/codex/AGENTS.md new file mode 100644 index 00000000000..40c61e10501 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/codex/AGENTS.md @@ -0,0 +1,21 @@ +# Codex driver test ownership + +The driver tests are split by contract: + +- lifecycle: startup, session lifetime, shutdown and provider admission +- context-control: instructions, models, permissions, goals and turn controls +- events: canonical event and provider-notification projection +- runtime-requests: question and approval request/response protocols +- semantic-results: dynamic tools, completion receipts and unknown outcomes +- recovery: snapshot, resume, reconciliation and current tool refresh +- conformance: replay validation and controller-owned result contracts + +Use the existing suites and `codex-app-server-driver.test-support.ts`. The +retired `codex-app-server-driver.test.ts` duplicated the split suites; do not +recreate it or replay one complete contract in multiple files. When splitting +another test file, move its cases and retire the old copies in the same change. +Preserve table rows, waits, actions and assertions, including failure cases. + +Keep provider protocol and native lifecycle layers independent when they test +different owners. Test workspaces must use the disposable real `WORKSPACE` +fixture; do not use a managed Codex checkout as model-writable workspace. diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts index 021c0ab99f7..332522cb698 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts @@ -289,18 +289,20 @@ describe("Codex app-server Codex driver", () => { workingDirectory: WORKSPACE, }), ).rejects.toThrow("cannot overlap host CODEX_HOME"); - await expect( - makeDriver([], { - environment: { - HOME: `${WORKSPACE}/host-home`, - CODEX_HOME: "/isolated/codex", - }, - }).openSession({ - runId: "run-host-home", - normalizedSessionId: "normalized-host-home", - workingDirectory: WORKSPACE, - }), - ).rejects.toThrow("cannot contain the host HOME"); + for (const hostHome of [WORKSPACE, `${WORKSPACE}/host-home`]) { + await expect( + makeDriver([], { + environment: { + HOME: hostHome, + CODEX_HOME: "/isolated/codex", + }, + }).openSession({ + runId: "run-host-home", + normalizedSessionId: "normalized-host-home", + workingDirectory: WORKSPACE, + }), + ).rejects.toThrow("cannot contain the host HOME"); + } await expect( makeDriver([]).openSession({ runId: "run-root", @@ -614,15 +616,16 @@ describe("Codex app-server Codex driver", () => { // Both denials a real app-server sends: the method is absent, and the // build has the feature switched off. - for (const denial of [ - new CodexRpcError( - '{"code":-32601,"message":"method not found"}', - CODEX_METHOD_NOT_FOUND, - ), - new CodexRpcError( - '{"code":-32600,"message":"goals feature is disabled"}', - CODEX_INVALID_REQUEST, - ), + for (const { denial, availability } of [ + { denial: new CodexRpcError( + '{"code":-32601,"message":"method not found"}', CODEX_METHOD_NOT_FOUND, + ), availability: "unsupported" }, + { denial: new CodexRpcError( + '{"code":-32600,"message":"goals feature is disabled"}', CODEX_INVALID_REQUEST, + ), availability: "policy_disabled" }, + { denial: new CodexRpcError( + '{"code":-32004,"message":"goals feature is disabled by policy"}', -32_004, + ), availability: "policy_disabled" }, ]) { const unsupportedTransport = new FakeCodexTransport(); unsupportedTransport.rejectMethods.set("thread/goal/get", denial); @@ -635,6 +638,11 @@ describe("Codex app-server Codex driver", () => { expect((await unsupportedDriver.descriptor()).capabilities).toMatchObject( { goals: false }, ); + const unsupportedEvents = unsupported.events()[Symbol.asyncIterator](); + await unsupportedEvents.next(); + await expect(unsupportedEvents.next()).resolves.toMatchObject({ + value: { eventType: "session.capabilities.updated", payload: { sessionGoals: { availability } } }, + }); await expect( unsupported.goal?.({ action: "get" }), ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); @@ -681,4 +689,189 @@ describe("Codex app-server Codex driver", () => { } }); + it.each([false, true])("requires trusted continuation metadata before omitting task context (%s)", async (continuation) => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport], { + skillInputs: [{ type: "skill", name: "first-task", path: "/skills/first-task/SKILL.md" }], + }).openSession({ runId: "run-delta", normalizedSessionId: "session-delta", workingDirectory: WORKSPACE }); + const text = JSON.stringify({ schema: "paperclip.native-continuation.v1", events: '{"messages":[{"body":"Go ahead"}]}', completion: { revision: "2", criterionIds: ["comment"] } }); + await session.startTurn({ message: { role: "user", text }, ...(continuation ? { continuation: true as const } : {}) }); + const params = transport.calls.find((call) => call.method === "turn/start")!.params; + if (continuation) { + expect(params.input).toEqual([{ type: "text", text, text_elements: [] }]); + expect(JSON.stringify(params.input)).not.toContain("constraints"); + expect(JSON.stringify(params.input)).not.toContain("first-task"); + } else { + expect(JSON.stringify(params.input)).toContain("constraints"); + expect(params.input).toContainEqual({ type: "skill", name: "first-task", path: "/skills/first-task/SKILL.md" }); + } + await session.close({ reason: "test complete" }); + }); + + it("sends the selected reasoning effort with the Codex turn", async () => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport], { model: "gpt-6-astra", reasoningEffort: "ultra" }).openSession({ + runId: "run-effort", + normalizedSessionId: "session-effort", + workingDirectory: WORKSPACE, + }); + await session.startTurn({ message: { role: "user", text: "Continue" } }); + expect(transport.calls.find((call) => call.method === "turn/start")?.params).toMatchObject({ effort: "ultra" }); + await session.close({ reason: "test complete" }); + }); + + it("uses negotiated ACP controls through the Codex transport facade", async () => { + let controls = { steering: false, queuedFollowUp: false }; + const transport = Object.assign(new FakeCodexTransport(), { turnControlCapabilities: () => ({ ...controls }) }); + const session = await makeDriver([transport], { + driverIdentity: { kind: "acpx_runtime", displayName: "Pi ACP", version: "test" }, + capabilities: { steering: false }, + }).openSession({ runId: "run-pi", normalizedSessionId: "session-pi", workingDirectory: WORKSPACE }); + try { + expect(session.turnControlCapabilities?.()).toEqual(controls); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); + await expect(session.steer?.({ turnId, message: { role: "user", text: "Change" } })).rejects.toThrow("not negotiated"); + controls = { steering: true, queuedFollowUp: true }; + expect(session.turnControlCapabilities?.()).toEqual(controls); + const input = { turnId, correlationId: "queued-1", mode: "follow_up" as const, message: { role: "user" as const, text: "Then validate" } }; + await session.steer?.(input); + expect(transport.calls.find(call => call.method === "turn/steer")?.params).toMatchObject({ expectedTurnId: turnId, mode: "follow_up", correlationId: "queued-1" }); + await expect(session.steer?.(input)).rejects.toThrow("already acknowledged"); + expect(transport.calls.filter(call => call.method === "turn/steer")).toHaveLength(1); + } finally { await session.close({ reason: "verified" }); } + }); + + it("keeps native follow-up disabled for the Codex app-server driver", async () => { + const transport = Object.assign(new FakeCodexTransport(), { turnControlCapabilities: () => ({ steering: true, queuedFollowUp: true }) }); + const session = await makeDriver([transport]).openSession({ runId: "run-codex", normalizedSessionId: "session-codex", workingDirectory: WORKSPACE }); + try { + expect(session.turnControlCapabilities?.()).toEqual({ steering: true, queuedFollowUp: false }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); + await expect(session.steer?.({ turnId, mode: "follow_up", message: { role: "user", text: "Queue" } })).rejects.toThrow("not negotiated"); + expect(transport.calls.filter(call => call.method === "turn/steer")).toHaveLength(0); + } finally { await session.close({ reason: "verified" }); } + }); + + it("preserves an inactive goal status while editing its objective", async () => { + const transport = new FakeCodexTransport(); + const session = await makeDriver([transport]).openSession({ + runId: "run-goal-inactive-edit", + normalizedSessionId: "normalized-goal-inactive-edit", + workingDirectory: WORKSPACE, + }); + + await session.goal?.({ + action: "set", + objective: "Updated while paused", + status: "paused", + }); + + expect( + transport.calls.filter(({ method }) => method === "thread/goal/set"), + ).toContainEqual({ + method: "thread/goal/set", + params: { + threadId: "thread-1", + objective: "Updated while paused", + status: "paused", + }, + }); + await session.close({ reason: "fixture complete" }); + }); + + it("rolls back only a definitely rejected idle goal autostart", async () => { + const definiteTransport = new FakeCodexTransport(); + const definiteSession = await makeDriver([definiteTransport]).openSession({ + runId: "run-goal-autostart-definite-rejection", + normalizedSessionId: "normalized-goal-autostart-definite-rejection", + workingDirectory: WORKSPACE, + }); + definiteTransport.rejectMethods.set( + "thread/goal/set", + new CodexRpcError('{"code":-32603,"message":"internal error"}', -32_603), + ); + await expect( + definiteSession.goal?.({ + action: "set", + objective: "Rejected before a turn starts", + }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + definiteTransport.rejectMethods.delete("thread/goal/set"); + await expect( + definiteSession.startTurn({ + message: { role: "user", text: "A normal turn may still start." }, + }), + ).resolves.toMatchObject({ turnId: "turn-1" }); + await definiteSession.close({ reason: "fixture complete" }); + + const ambiguousTransport = new FakeCodexTransport(); + const ambiguousSession = await makeDriver([ambiguousTransport]).openSession({ + runId: "run-goal-autostart-ambiguous", + normalizedSessionId: "normalized-goal-autostart-ambiguous", + workingDirectory: WORKSPACE, + }); + ambiguousTransport.rejectMethods.set( + "thread/goal/set", + new Error("codex app-server transport closed"), + ); + await expect( + ambiguousSession.goal?.({ + action: "set", + objective: "The provider may have started this goal", + }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + ambiguousTransport.rejectMethods.delete("thread/goal/set"); + await expect( + ambiguousSession.startTurn({ + message: { role: "user", text: "Do not create competing work." }, + }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + await ambiguousSession.close({ reason: "fixture complete" }); + }); + + it("preserves a provider-neutral goal action subset through the Codex transport facade", async () => { + const transport = new FakeCodexTransport(); + const driver = makeDriver([transport], { + goalCapability: { + actions: ["set", "clear"], + autonomousUpdates: true, + persistentAcrossResume: true, + maxObjectiveChars: 4_000, + tokenBudgetControl: false, + usageReporting: true, + }, + }); + const session = await driver.openSession({ + runId: "run-goals-action-subset", + normalizedSessionId: "normalized-goals-action-subset", + workingDirectory: WORKSPACE, + }); + const events = session.events()[Symbol.asyncIterator](); + await events.next(); + await expect(events.next()).resolves.toMatchObject({ + value: { + eventType: "session.capabilities.updated", + payload: { + sessionGoals: { + availability: "available", + actions: ["set", "clear"], + tokenBudgetControl: false, + }, + }, + }, + }); + + await expect( + session.goal?.({ action: "pause" }), + ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); + expect( + transport.calls.filter(({ method }) => method === "thread/goal/set"), + ).toHaveLength(0); + await expect( + session.goal?.({ action: "set", objective: "Finish the task" }), + ).resolves.toMatchObject({ objective: "Finish the task" }); + await expect(session.goal?.({ action: "clear" })).resolves.toBeNull(); + await session.close({ reason: "fixture complete" }); + }); + }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.recovery.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.recovery.test.ts index 329e2fc00e6..f1b967f161a 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.recovery.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.recovery.test.ts @@ -1302,4 +1302,29 @@ describe("Codex app-server Codex driver", () => { }); }); + it("refreshes authorized tools on resume without replacing the provider thread", async () => { + const first = new FakeCodexTransport(); + const second = new FakeCodexTransport(); + const original = await makeDriver([first], { conversationMode: "prepared", dynamicTools: [] }).openSession({ + runId: "run-connect", normalizedSessionId: "normalized-connect", workingDirectory: WORKSPACE, + }); + const snapshot = await original.snapshot(); + await original.close({ reason: "waiting for connection" }); + const githubTool = { name: "github_search", description: "Search authorized repositories", inputSchema: { type: "object" } }; + const handler = vi.fn(async () => ({ repositories: ["paperclip"] })); + const resumedDriver = makeDriver([second], { conversationMode: "prepared", dynamicTools: [githubTool], dynamicToolHandler: handler }); + expect((await resumedDriver.descriptor()).capabilities.toolRefreshOnResume).toBe(true); + const recovery = await resumedDriver.recoverSession(snapshot); + expect(recovery.recovered).toBe(true); + await recovery.session!.startTurn({ message: { role: "user", text: "Continue with GitHub connected" } }); + expect(second.calls.some(call => call.method === "thread/start")).toBe(false); + expect(second.calls.find(call => call.method === "thread/resume")?.params).toMatchObject({ threadId: "thread-1", dynamicTools: expect.arrayContaining([githubTool]) }); + const result = await second.invoke({ id: "rpc-github", method: "item/tool/call", params: { + threadId: "thread-1", turnId: "turn-1", callId: "github-call", tool: "github_search", arguments: { query: "paperclip" }, + } }); + expect(result).toMatchObject({ success: true }); + expect(handler).toHaveBeenCalledWith(expect.objectContaining({ tool: "github_search", threadId: "thread-1" })); + await recovery.session?.close({ reason: "test done" }); + }); + }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts index 33a70e741fe..0ca38f1554f 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts @@ -1,3 +1,4 @@ +import { SemanticToolOutcomeUnknownError } from "../../contracts/native-session-backend.js"; import { CODEX_BLOCK_RESULT_OUTPUT_SCHEMA, CODEX_INVALID_REQUEST, @@ -862,4 +863,159 @@ describe("Codex app-server Codex driver", () => { ?.payload, ).toMatchObject({ reportedWorkDisposition: "blocked" }); }); + it("exposes and dispatches only explicit chat tools across fresh and resumed direct chat", async () => { + const first = new FakeCodexTransport(); + const second = new FakeCodexTransport(); + const registerDeliverable = { + name: "register_deliverable", + description: "Prepare one requested file.", + inputSchema: { type: "object", properties: {} }, + }; + const readCurrentWakeComments = { + name: "read_current_wake_comments", + description: "Read only comments bound into the current wake.", + inputSchema: { type: "object", properties: {} }, + }; + const requestHumanInput = { + name: "request_human_input", + description: "Ask one structured question through Paperclip.", + inputSchema: { type: "object", properties: {} }, + }; + const listChatAttachments = { + name: "list_chat_attachments", + description: "List same-conversation attachment metadata.", + inputSchema: { type: "object", properties: {} }, + }; + const reuseChatAttachment = { + name: "reuse_chat_attachment", + description: "Prepare one same-conversation attachment again.", + inputSchema: { type: "object", properties: {} }, + }; + const readChatAttachment = { + name: "read_chat_attachment", + description: "Read one same-conversation file without resending it.", + inputSchema: { type: "object", properties: {} }, + }; + const handler = vi.fn(async (call) => ({ + interaction: { id: "interaction-direct-question", status: "pending" }, + callId: call.callId, + })); + const driver = makeDriver([first, second], { + conversationMode: "direct", + dynamicTools: [ + registerDeliverable, + readCurrentWakeComments, + requestHumanInput, + listChatAttachments, + reuseChatAttachment, + readChatAttachment, + { + name: "report_progress", + description: "Must remain unavailable in direct chat.", + inputSchema: { type: "object", properties: {} }, + }, + ], + dynamicToolHandler: handler, + }); + const original = await driver.openSession({ + runId: "run-direct-file", + normalizedSessionId: "normalized-direct-file", + workingDirectory: WORKSPACE, + }); + await original.startTurn({ + message: { role: "user", text: "Please return one file." }, + }); + const snapshot = await original.snapshot(); + await original.close({ reason: "transport lost" }); + + expect( + first.calls.find((call) => call.method === "thread/start")?.params + .dynamicTools, + ).toEqual([ + registerDeliverable, + readCurrentWakeComments, + requestHumanInput, + listChatAttachments, + reuseChatAttachment, + readChatAttachment, + ]); + + const freshQuestion = await first.invoke({ + id: "rpc-direct-question-fresh", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-direct-question-fresh", + tool: "request_human_input", + arguments: { interactionKind: "questions" }, + }, + }); + expect(freshQuestion).toMatchObject({ success: true }); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + tool: "request_human_input", + callId: "call-direct-question-fresh", + arguments: { interactionKind: "questions" }, + }), + ); + + const recovery = await driver.recoverSession?.(snapshot); + expect(recovery).toMatchObject({ recovered: true }); + expect( + second.calls.find((call) => call.method === "thread/resume")?.params + .dynamicTools, + ).toEqual([ + registerDeliverable, + readCurrentWakeComments, + requestHumanInput, + listChatAttachments, + reuseChatAttachment, + readChatAttachment, + ]); + const resumedQuestion = await second.invoke({ + id: "rpc-direct-question-resumed", + method: "item/tool/call", + params: { + threadId: "thread-1", + turnId: "turn-1", + callId: "call-direct-question-resumed", + tool: "request_human_input", + arguments: { interactionKind: "confirmation" }, + }, + }); + expect(resumedQuestion).toMatchObject({ success: true }); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + tool: "request_human_input", + callId: "call-direct-question-resumed", + arguments: { interactionKind: "confirmation" }, + }), + ); + await recovery?.session?.close({ reason: "test complete" }); + }); + + it.each(["unknown", "validation"])("preserves the dynamic tool outcome boundary (%s)", async (outcome) => { + const transport = new FakeCodexTransport(); + const error = outcome === "unknown" + ? new SemanticToolOutcomeUnknownError("write may have committed") + : new Error("invalid instruction input"); + const handler = vi.fn(async () => { throw error; }); + const session = await makeDriver([transport], { + dynamicTools: [{ name: "update_agent_instructions", description: "Write instructions.", inputSchema: { type: "object" } }], + dynamicToolHandler: handler, + }).openSession({ runId: "run-uncertain-write", normalizedSessionId: "session-uncertain-write", workingDirectory: WORKSPACE }); + await session.startTurn({ message: { role: "user", text: "Update instructions." } }); + const response = transport.invoke({ id: "rpc-write", method: "item/tool/call", params: { + threadId: "thread-1", turnId: "turn-1", callId: "uncertain-write", tool: "update_agent_instructions", arguments: {}, + } }); + if (outcome === "unknown") await expect(response).rejects.toBe(error); + else await expect(response).resolves.toMatchObject({ success: false }); + transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } }); + const events = await collectUntilTerminal(session.events()); + const completions = events.filter((event) => event.eventType === "item.completed" && event.itemId === "uncertain-write"); + expect(completions).toHaveLength(outcome === "unknown" ? 0 : 1); + expect(handler).toHaveBeenCalledTimes(1); + }); + }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts deleted file mode 100644 index b5a8ee9ec12..00000000000 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts +++ /dev/null @@ -1,4153 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { resolve as resolvePath } from "node:path"; -import { fileURLToPath } from "node:url"; -import { SemanticToolOutcomeUnknownError } from "../../contracts/native-session-backend.js"; - -import { - CODEX_BLOCK_RESULT_OUTPUT_SCHEMA, - CODEX_RESULT_OUTPUT_SCHEMA, - createCodexTaskEnvelope, - isSkilllessCodexContext, -} from "../../contracts/codex.js"; -import { - HarnessCapabilityUnavailableError, - HarnessOperationAlreadyTerminalError, - HarnessReconciliationError, - HarnessStaleTurnError, - type HarnessRuntimeRequestResolution, -} from "../../contracts/harness-driver.js"; -import { - applyPrpEvent, - createSessionSnapshotFromMetadata, -} from "../../reducer/session-reducer.js"; -import { - validatePrpEvent, - type PrpCapabilities, - type PrpEvent, - type PrpStructuredRunResult, -} from "../../protocol/replay-contract.js"; -import { loadLiveConsoleConformanceFixture } from "../../protocol/live-console-fixture.js"; -import { - CodexAppServerDriver, - createIsolatedCodexAppServerArgs, - parseCodexTurnDiff, -} from "./codex-app-server-driver.js"; -import { - runCodexCodexTracer, - replayPersistedCodexEvents, - validateCodexResultProposal, -} from "../../mock-core/codex-runner.js"; -import { - CODEX_METHOD_NOT_FOUND, - CodexRpcError, - type CodexAppServerTransport, - type CodexRpcNotification, - type CodexRpcServerRequest, - type CodexServerRequestHandler, - type CodexTraceInterpretation, -} from "./app-server-transport.js"; - -const TEST_WORKING_DIRECTORY = resolvePath( - fileURLToPath(new URL("../../../", import.meta.url)), -); - -class TestQueue implements AsyncIterable { - values: T[] = []; - waiters: Array<{ - resolve: (value: IteratorResult) => void; - reject: (error: Error) => void; - }> = []; - closed = false; - error: Error | null = null; - - push(value: T): void { - const waiter = this.waiters.shift(); - if (waiter) waiter.resolve({ value, done: false }); - else this.values.push(value); - } - - close(): void { - this.closed = true; - for (const waiter of this.waiters.splice(0)) - waiter.resolve({ value: undefined, done: true }); - } - - fail(error: Error): void { - this.error = error; - for (const waiter of this.waiters.splice(0)) waiter.reject(error); - } - - [Symbol.asyncIterator](): AsyncIterator { - return { - next: async () => { - const value = this.values.shift(); - if (value) return { value, done: false }; - if (this.error) throw this.error; - if (this.closed) return { value: undefined, done: true }; - return new Promise((resolve, reject) => this.waiters.push({ resolve, reject })); - }, - }; - } -} - -class FakeCodexTransport implements CodexAppServerTransport { - readonly calls: Array<{ method: string; params: Record }> = - []; - readonly sentNotifications: Array<{ - method: string; - params?: Record; - }> = []; - readonly traceInterpretations: CodexTraceInterpretation[] = []; - readonly queue = new TestQueue(); - handler: CodexServerRequestHandler = async () => ({}); - rejectMethods = new Map(); - readResponse: Record | null = null; - turnStartResponse: Promise> | null = null; - goalState: Record | null = null; - confirmCollaborationMode = true; - runtimeRequestResolver: ((input: { - requestId: string; - turnId: string; - resolution: HarnessRuntimeRequestResolution; - }) => Promise) | null = null; - - constructor( - readonly threadId = "thread-1", - readonly providerSessionId = "provider-session-1", - ) {} - - async request( - method: string, - params: Record, - ): Promise> { - this.calls.push({ method, params: structuredClone(params) }); - const rejection = this.rejectMethods.get(method); - if (rejection) throw rejection; - if (method === "initialize") { - return { - userAgent: "codex-cli/0.132.0", - codexHome: "/isolated/codex", - platformFamily: "unix", - platformOs: "linux", - }; - } - if (method === "collaborationMode/list") { - return this.confirmCollaborationMode - ? { - data: [ - { - name: "Plan", - mode: "plan", - model: "gpt-test", - reasoning_effort: "high", - }, - ], - } - : { data: [{ name: "Default", mode: "default", model: "gpt-test" }] }; - } - if (method === "thread/start" || method === "thread/resume") { - const planMode = - params.permissions === "paperclip-runner-workspace-read-only"; - return { - thread: { - id: this.threadId, - sessionId: this.providerSessionId, - modelProvider: "openai", - cwd: TEST_WORKING_DIRECTORY, - turns: [], - activePermissionProfile: { - id: params.permissions ?? (planMode - ? "paperclip-runner-workspace-read-only" - : "paperclip-runner-workspace-only"), - }, - }, - model: "gpt-test", - modelProvider: "openai", - cwd: TEST_WORKING_DIRECTORY, - sandbox: { type: "workspaceWrite" }, - approvalPolicy: params.approvalPolicy, - instructionSources: [], - }; - } - if (method === "turn/start") { - return ( - this.turnStartResponse ?? { - turn: { id: "turn-1", status: "inProgress", items: [] }, - } - ); - } - if (method === "thread/goal/get") return { goal: this.goalState }; - if (method === "thread/goal/set") { - this.goalState = { - threadId: this.threadId, - objective: - typeof params.objective === "string" - ? params.objective - : String( - this.goalState?.objective ?? "Ship the Live console tracer", - ), - status: params.status ?? this.goalState?.status ?? "active", - tokenBudget: params.tokenBudget ?? this.goalState?.tokenBudget ?? null, - tokensUsed: 0, - timeUsedSeconds: 0, - createdAt: 1, - updatedAt: 2, - }; - return { goal: this.goalState }; - } - if (method === "thread/goal/clear") { - this.goalState = null; - return {}; - } - if (method === "thread/turns/list") { - const snapshot = this.readResponse ?? { thread: { turns: [{ id: "turn-1", status: "inProgress", items: [] }] } }; - const turns = (snapshot.thread as Record).turns; - return { data: Array.isArray(turns) ? turns.map(turn => ({ ...turn, items: [], itemsView: "notLoaded" })) : turns, nextCursor: null }; - } - if (method === "thread/items/list") { - const turns = ((this.readResponse?.thread as Record | undefined)?.turns ?? []) as Array>; - const turn = turns.find(value => value.id === params.turnId); - return { data: ((turn?.items ?? []) as Array>).map(item => ({ turnId: params.turnId, item })), nextCursor: null }; - } - if (method === "thread/read") { - return structuredClone( - this.readResponse ?? { - thread: { - id: this.threadId, - sessionId: this.providerSessionId, - cwd: TEST_WORKING_DIRECTORY, - turns: [{ id: "turn-1", status: "inProgress", items: [] }], - }, - } - ); - } - return {}; - } - - notify(method: string, params?: Record): void { - this.sentNotifications.push( - params === undefined ? { method } : { method, params }, - ); - } - - notifications(): AsyncIterable { - return this.queue; - } - - setServerRequestHandler(handler: CodexServerRequestHandler): void { - this.handler = handler; - } - - async resolveRuntimeRequest(input: { - requestId: string; - turnId: string; - resolution: HarnessRuntimeRequestResolution; - }): Promise { - await this.runtimeRequestResolver?.(input); - } - - recordTraceInterpretation(input: CodexTraceInterpretation): void { - this.traceInterpretations.push(structuredClone(input)); - } - - async close(): Promise { - this.queue.close(); - } - - push(method: string, params: Record): void { - this.queue.push({ method, params }); - } - - pushTraced( - method: string, - params: Record, - sourceEventId: string, - sourceEventType: string, - ): void { - this.queue.push({ - method, - params, - paperclipTrace: { sourceEventId, sourceEventType }, - }); - } - - invoke(request: CodexRpcServerRequest): Promise> { - return this.handler(request); - } -} - -const envelope = createCodexTaskEnvelope({ - objective: "Create hello.txt with the text hello.", - criteria: [{ id: "file", requirement: "hello.txt contains hello" }], -}); - -const liveConsoleFixturePath = fileURLToPath( - new URL( - "../../../protocol/fixtures/codex-driver/driver-conformance.json", - import.meta.url, - ), -); - -const result: PrpStructuredRunResult = { - schema: "paperclip.run_result.v1", - reportedWorkDisposition: "done", - summary: "Created hello.txt.", - completionClaim: { - contractRevision: "codex-demo-v1", - objectiveSatisfied: true, - criteria: [ - { criterionId: "file", status: "satisfied", evidenceRefs: ["hello.txt"] }, - ], - remainingWork: [], - }, - evidence: [{ ref: "hello.txt" }], - verification: [{ commandOrCheck: "read hello.txt", status: "passed" }], - attentionRequests: [], - artifacts: [{ kind: "file", ref: "hello.txt" }], -}; - -function makeDriver( - transports: FakeCodexTransport[], - options: Record = {}, -) { - let index = 0; - return new CodexAppServerDriver({ - taskEnvelope: envelope, - environment: { - PATH: "/bin", - HOME: "/isolated/home", - CODEX_HOME: "/isolated/codex", - LANG: "C.UTF-8", - PAPERCLIP_API_KEY: "must-not-pass", - RANDOM_SKILL_PATH: "/skills/unrelated", - }, - now: () => new Date("2026-08-08T12:00:00.000Z"), - transportFactory: () => transports[index++]!, - ...options, - }); -} - -it("refreshes authorized tools on resume without replacing the provider thread", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const original = await makeDriver([first], { conversationMode: "prepared", dynamicTools: [] }).openSession({ - runId: "run-connect", normalizedSessionId: "normalized-connect", workingDirectory: TEST_WORKING_DIRECTORY, - }); - const snapshot = await original.snapshot(); - await original.close({ reason: "waiting for connection" }); - const githubTool = { name: "github_search", description: "Search authorized repositories", inputSchema: { type: "object" } }; - const handler = vi.fn(async () => ({ repositories: ["paperclip"] })); - const resumedDriver = makeDriver([second], { conversationMode: "prepared", dynamicTools: [githubTool], dynamicToolHandler: handler }); - expect((await resumedDriver.descriptor()).capabilities.toolRefreshOnResume).toBe(true); - const recovery = await resumedDriver.recoverSession(snapshot); - expect(recovery.recovered).toBe(true); - await recovery.session!.startTurn({ message: { role: "user", text: "Continue with GitHub connected" } }); - expect(second.calls.some(call => call.method === "thread/start")).toBe(false); - expect(second.calls.find(call => call.method === "thread/resume")?.params).toMatchObject({ threadId: "thread-1", dynamicTools: expect.arrayContaining([githubTool]) }); - const result = await second.invoke({ id: "rpc-github", method: "item/tool/call", params: { - threadId: "thread-1", turnId: "turn-1", callId: "github-call", tool: "github_search", arguments: { query: "paperclip" }, - } }); - expect(result).toMatchObject({ success: true }); - expect(handler).toHaveBeenCalledWith(expect.objectContaining({ tool: "github_search", threadId: "thread-1" })); - await recovery.session?.close({ reason: "test done" }); -}); - -async function collectUntilTerminal( - events: AsyncIterable, -): Promise { - const collected: PrpEvent[] = []; - for await (const event of events) { - collected.push(event); - if ( - [ - "turn.completed", - "turn.failed", - "turn.interrupted", - "turn.cancelled", - ].includes(event.eventType) - ) - break; - } - return collected; -} - -function reverseObjectKeys(value: unknown): unknown { - if (Array.isArray(value)) return value.map(reverseObjectKeys); - if (typeof value !== "object" || value === null) return value; - return Object.fromEntries( - Object.entries(value as Record) - .reverse() - .map(([key, entry]) => [key, reverseObjectKeys(entry)]), - ); -} - -async function traceCompletedProposal( - proposal: PrpStructuredRunResult | null, - options: { - runId?: string; - normalizedSessionId?: string; - capabilities?: Record; - steer?: string; - interrupt?: boolean; - } = {}, -) { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { - capabilities: options.capabilities, - }); - const traced = runCodexCodexTracer({ - driver, - taskEnvelope: envelope, - workingDirectory: TEST_WORKING_DIRECTORY, - runId: options.runId, - normalizedSessionId: options.normalizedSessionId, - steer: options.steer, - interrupt: options.interrupt, - }); - while (!transport.calls.some((call) => call.method === "turn/start")) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: "turn-1", status: "inProgress" }, - }); - if (proposal !== null) { - transport.push("item/completed", { - threadId: "thread-1", - turnId: "turn-1", - item: { - id: "answer-1", - type: "agentMessage", - text: JSON.stringify(proposal), - }, - }); - } - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - return { trace: await traced, transport }; -} - -describe("Codex app-server Codex driver", () => { - it("parses a complete Codex turn diff snapshot with bounded file statistics", () => { - expect(parseCodexTurnDiff([ - "diff --git a/src/old.ts b/src/new.ts", - "similarity index 95%", - "rename from src/old.ts", - "rename to src/new.ts", - "--- a/src/old.ts", - "+++ b/src/new.ts", - "@@ -1 +1,2 @@", - "-old", - "+new", - "+another", - "diff --git a/assets/image.png b/assets/image.png", - "Binary files a/assets/image.png and b/assets/image.png differ", - ].join("\n"))).toEqual([ - expect.objectContaining({ - path: "src/new.ts", - previousPath: "src/old.ts", - operation: "rename", - additions: 2, - deletions: 1, - binary: false, - }), - expect.objectContaining({ - path: "assets/image.png", - operation: "modify", - additions: null, - deletions: null, - binary: true, - }), - ]); - }); - - it("bounds aggregate Codex diffs and rejects unsafe workspace paths", () => { - const patches = Array.from({ length: 2_001 }, (_, index) => [ - `diff --git a/src/file-${index}.ts b/src/file-${index}.ts`, - `--- a/src/file-${index}.ts`, - `+++ b/src/file-${index}.ts`, - "@@ -1 +1 @@", - "-old", - "+new", - ].join("\n")); - expect(parseCodexTurnDiff(patches.join("\n"))).toHaveLength(2_000); - - const oversized = parseCodexTurnDiff([ - "diff --git a/src/large.ts b/src/large.ts", - "--- a/src/large.ts", - "+++ b/src/large.ts", - "@@ -0,0 +1 @@", - `+${"x".repeat(300_000)}`, - ].join("\n")); - expect(oversized[0]?.diff).toHaveLength(256 * 1_024); - - expect(parseCodexTurnDiff([ - "diff --git a/../../secret.txt b/../../secret.txt", - "--- a/../../secret.txt", - "+++ b/../../secret.txt", - "@@ -1 +1 @@", - "-old", - "+new", - ].join("\n"))).toEqual([]); - }); - - it("persists process ownership before sending provider requests", async () => { - const transport = new FakeCodexTransport(); - Object.assign(transport, { - processInfo: () => ({ - pid: 71_001, - processGroupId: 71_001, - startedAt: "2026-08-18T18:00:00.000Z", - exited: false, - exitCode: null, - signal: null, - }), - }); - let releaseOwnership!: () => void; - const ownershipPersisted = new Promise((resolve) => { - releaseOwnership = resolve; - }); - const onSpawn = vi.fn(() => ownershipPersisted); - const driver = makeDriver([transport], { onSpawn }); - - const opening = driver.openSession({ - runId: "run-owned", - normalizedSessionId: "normalized-owned", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await vi.waitFor(() => expect(onSpawn).toHaveBeenCalledOnce()); - expect(transport.calls).toEqual([]); - - releaseOwnership(); - await opening; - expect(onSpawn).toHaveBeenCalledWith({ - pid: 71_001, - processGroupId: 71_001, - startedAt: "2026-08-18T18:00:00.000Z", - }); - expect(transport.calls[0]?.method).toBe("initialize"); - }); - - it("sends direct chat as plain text and permits a follow-up turn", async () => { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { conversationMode: "direct" }); - const session = await driver.openSession({ - runId: "run-chat", - normalizedSessionId: "normalized-chat", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - const threadStart = transport.calls.find( - (call) => call.method === "thread/start", - ); - expect(threadStart?.params).not.toHaveProperty("baseInstructions"); - expect(threadStart?.params.dynamicTools).toEqual([]); - - const first = await session.startTurn({ - message: { role: "user", text: "Hello Codex" }, - }); - const firstStart = transport.calls.find( - (call) => call.method === "turn/start", - ); - expect(firstStart?.params).not.toHaveProperty("outputSchema"); - expect(firstStart?.params.input).toEqual([ - { type: "text", text: "Hello Codex", text_elements: [] }, - ]); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: first.turnId, status: "inProgress" }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: first.turnId, status: "completed", items: [] }, - }); - await collectUntilTerminal(session.events()); - - transport.turnStartResponse = Promise.resolve({ - turn: { id: "turn-2", status: "inProgress", items: [] }, - }); - await expect( - session.startTurn({ message: { role: "user", text: "And a follow-up" } }), - ).resolves.toEqual({ - turnId: "turn-2", - effectiveCollaborationMode: "default", - }); - }); - - it("forwards the persisted native model to the runner transport", async () => { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { model: "qualified-provider-model" }); - - await driver.openSession({ - runId: "run-qualified-model", - normalizedSessionId: "normalized-qualified-model", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - expect( - transport.calls.find((call) => call.method === "thread/start")?.params, - ).toMatchObject({ - model: "qualified-provider-model", - completionContract: { - revision: "codex-demo-v1", - criterionIds: ["file"], - }, - }); - }); - - it("adds Paperclip developer instructions without replacing the Codex base and enables selected skills", async () => { - const transport = new FakeCodexTransport(); - const baseInstructions = [ - "You are running as a Paperclip agent.", - "Follow the attached AGENTS.md instructions.", - "Read-only instruction sibling root: /paperclip/context/instructions", - ].join("\n\n"); - const driver = makeDriver([transport], { - baseInstructions, - includeSkillInstructions: true, - }); - - await driver.openSession({ - runId: "run-runtime-context", - normalizedSessionId: "normalized-runtime-context", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - const threadStart = transport.calls.find((call) => call.method === "thread/start"); - expect(threadStart?.params).toMatchObject({ - developerInstructions: baseInstructions, - config: { - "skills.include_instructions": true, - include_apps_instructions: false, - }, - }); - expect(threadStart?.params).not.toHaveProperty("baseInstructions"); - expect(JSON.stringify(threadStart?.params.input ?? null)).not.toContain(baseInstructions); - }); - - it("passes the common typed-event contract and reports one provider turn terminal", async () => { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport]); - const descriptor = await driver.descriptor(); - const session = await driver.openSession({ - runId: "run-1", - normalizedSessionId: "normalized-1", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Do the safe task." }, - }); - - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - transport.push("turn/plan/updated", { - threadId: "thread-1", - turnId: turn.turnId, - revision: 1, - plan: [ - { step: "Inspect", status: "completed" }, - { step: "Implement", status: "inProgress" }, - ], - }); - transport.push("turn/diff/updated", { - threadId: "thread-1", - turnId: turn.turnId, - revision: 1, - diff: [ - "diff --git a/hello.txt b/hello.txt", - "--- a/hello.txt", - "+++ b/hello.txt", - "@@ -1 +1,2 @@", - "-hello", - "+hello world", - "+again", - ].join("\n"), - }); - transport.push("turn/plan/updated", { - threadId: "thread-1", - turnId: turn.turnId, - revision: 2, - plan: [ - { step: "Inspect", status: "completed" }, - { step: "Implement", status: "completed" }, - ], - }); - transport.push("item/started", { - threadId: "thread-1", - turnId: turn.turnId, - item: { id: "cmd-1", type: "commandExecution", command: "printf hello" }, - }); - transport.push("item/commandExecution/outputDelta", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "cmd-1", - delta: "hello", - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "file-1", - type: "fileChange", - changes: [{ path: "hello.txt" }], - }, - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "reference-1", - type: "agentMessage", - text: "Open [hello.txt](hello.txt).", - }, - }); - const requestResolution = transport.invoke({ - id: "request-1", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "question-1", - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(session.pendingRuntimeRequests?.()).toHaveLength(1); - await session.resolveRuntimeRequest?.({ - requestId: "request-1", - turnId: turn.turnId, - resolution: { action: "cancel" }, - }); - expect(await requestResolution).toEqual({ answers: {} }); - transport.push("thread/tokenUsage/updated", { - threadId: "thread-1", - turnId: turn.turnId, - tokenUsage: { - total: { inputTokens: 10, outputTokens: 4 }, - modelContextWindow: 128000, - }, - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "answer-1", - type: "agentMessage", - text: JSON.stringify(result), - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect(events.every((event) => validatePrpEvent(event).ok)).toBe(true); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.filter((event) => event.eventType === "turn.completed"), - ).toHaveLength(1); - expect( - events.filter((event) => event.eventType === "run.terminal"), - ).toHaveLength(0); - const workspaceEvents = events.filter((event) => event.eventType === "workspace.change.updated"); - expect(workspaceEvents[0]?.payload).toMatchObject({ - schema: "paperclip.workspace.diff.v1", - changeSetId: `${turn.turnId}:workspace`, - complete: false, - totals: { files: 1, additions: 2, deletions: 1 }, - }); - expect(workspaceEvents.at(-1)?.payload).toMatchObject({ - schema: "paperclip.workspace.diff.v1", - complete: true, - totals: { files: 1 }, - }); - const planEvents = events.filter((event) => event.eventType === "plan.updated"); - expect(planEvents).toHaveLength(2); - expect(new Set(planEvents.map((event) => event.itemId))).toEqual(new Set([turn.turnId])); - expect(planEvents.at(-1)?.payload).toMatchObject({ - planId: turn.turnId, - revision: 2, - complete: true, - syncStatus: "not_applicable", - }); - expect( - events.find((event) => event.eventType === "workspace.diff.recorded") - ?.payload, - ).toMatchObject({ schema: "paperclip.workspace.diff.v1", complete: true }); - expect( - events.find((event) => event.eventType === "workspace.file.referenced") - ?.payload, - ).toMatchObject({ - schema: "paperclip.workspace.file_reference.v1", - path: "hello.txt", - }); - expect(events.map((event) => event.eventType)).toEqual( - expect.arrayContaining([ - "session.started", - "turn.started", - "item.started", - "item.delta", - "item.completed", - "run.result.proposed", - "turn.completed", - "runtime_request.created", - "runtime_request.resolved", - "workspace.change.updated", - "workspace.diff.recorded", - ]), - ); - - const capabilities: PrpCapabilities = { - schema: "paperclip.prp.capabilities.v1", - sessionReusePolicy: "reuse_per_issue", - driver: { kind: descriptor.kind, version: descriptor.version }, - steer: true, - interrupt: true, - resume: true, - runtimeRequests: true, - structuredResult: true, - typedEvents: true, - }; - const metadata = { - fixtureName: "codex-conformance", - identity: { - schema: "paperclip.prp.identity.v1" as const, - companyId: "company-1", - issueId: "issue-1", - runId: "run-1", - environmentLeaseId: "lease-1", - runnerInstanceId: "runner-codex", - normalizedSessionId: "normalized-1", - }, - capabilities, - }; - const live = events.reduce( - applyPrpEvent, - createSessionSnapshotFromMetadata(metadata), - ); - const replay = events.reduce( - applyPrpEvent, - createSessionSnapshotFromMetadata(metadata), - ); - expect(live).toEqual(replay); - expect(live.integrity).toBe("complete"); - expect(await session.usage?.()).toMatchObject({ - modelContextWindow: 128000, - }); - }); - - it("admits a strictly bound semantic result from the durable runner", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-durable-result", - normalizedSessionId: "normalized-durable-result", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Finish through runnerd." }, - }); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - transport.push("paperclip/runResult", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "semantic-result-1", - result, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.find((event) => event.eventType === "run.result.proposed"), - ).toMatchObject({ turnId: turn.turnId, itemId: "semantic-result-1" }); - }); - - it("correlates rehydrated notifications with the final canonical PRP event ids", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-traced", - normalizedSessionId: "normalized-traced", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Trace the response." }, - }); - - transport.pushTraced( - "turn/started", - { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }, - "event_runner_000001", - "turn.started", - ); - transport.pushTraced( - "item/started", - { - threadId: "thread-1", - turnId: turn.turnId, - item: { id: "answer-1", type: "agentMessage", phase: "final_answer" }, - }, - "event_runner_000002", - "item.started", - ); - transport.pushTraced( - "turn/completed", - { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }, - "event_runner_000003", - "turn.completed", - ); - - const events = await collectUntilTerminal(session.events()); - const itemMapping = transport.traceInterpretations.find( - (entry) => entry.providerMethod === "item/started", - ); - expect(itemMapping).toMatchObject({ - sourceEventId: "event_runner_000002", - sourceEventType: "item.started", - disposition: "mapped", - }); - expect(itemMapping?.emittedEventIds.length).toBeGreaterThan(0); - expect(events.map((event) => event.sourceEventId)).toEqual( - expect.arrayContaining(itemMapping?.emittedEventIds ?? []), - ); - expect(JSON.stringify(events)).not.toContain("paperclipTrace"); - }); - - it("maps canonical workspace snapshots with monotonic revisions and one terminal diff", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-canonical-workspace", - normalizedSessionId: "normalized-canonical-workspace", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Change the workspace." }, - }); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - const first = { - schema: "paperclip.workspace.diff.v1", - changeSetId: `${turn.turnId}:workspace`, - revision: 1, - source: "harness_reported", - complete: false, - files: [{ - path: "src/index.ts", - operation: "modify", - previousPath: null, - additions: 1, - deletions: 0, - binary: false, - diff: "+first\n", - }], - totals: { files: 1, additions: 1, deletions: 0 }, - patchArtifactRef: null, - }; - const second = { - ...first, - revision: 1, - files: [ - first.files[0], - { - path: "src/new-name.ts", - operation: "rename", - previousPath: "src/old-name.ts", - additions: 0, - deletions: 0, - binary: false, - diff: "rename from src/old-name.ts\nrename to src/new-name.ts\n", - }, - { - path: "public/image.png", - operation: "modify", - previousPath: null, - additions: null, - deletions: null, - binary: true, - diff: null, - }, - ], - totals: { files: 3, additions: null, deletions: null }, - }; - transport.pushTraced( - "paperclip/workspaceChange/updated", - { threadId: "thread-1", turnId: turn.turnId, workspaceChange: first }, - "event_workspace_1", - "workspace.change.updated", - ); - transport.pushTraced( - "paperclip/workspaceChange/updated", - { threadId: "thread-1", turnId: turn.turnId, workspaceChange: second }, - "event_workspace_2", - "workspace.change.updated", - ); - transport.push("paperclip/workspaceChange/updated", { - threadId: "thread-1", - turnId: turn.turnId, - workspaceChange: second, - }); - transport.push("paperclip/workspaceChange/updated", { - threadId: "thread-1", - turnId: turn.turnId, - workspaceChange: { - ...second, - files: [{ ...second.files[0], path: "/absolute/not-allowed.ts" }], - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - const updates = events.filter( - (event) => event.eventType === "workspace.change.updated", - ); - expect( - updates.map( - (event) => (event.payload as Record).revision, - ), - ).toEqual([1, 2]); - expect( - (updates[1]?.payload as Record).files, - ).toHaveLength(3); - expect(events.filter((event) => event.eventType === "workspace.diff.recorded")) - .toHaveLength(1); - expect( - events.find((event) => event.eventType === "workspace.diff.recorded")?.payload, - ).toMatchObject({ - revision: 2, - source: "runner_verified", - complete: true, - totals: { files: 3, additions: null, deletions: null }, - }); - expect( - transport.traceInterpretations.filter((entry) => - entry.providerMethod === "paperclip/workspaceChange/updated" - ).map((entry) => entry.disposition), - ).toEqual(["mapped", "mapped"]); - }); - - it("finalizes an authoritative empty workspace snapshot when a turn is interrupted", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-empty-interrupted-workspace", - normalizedSessionId: "normalized-empty-interrupted-workspace", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Inspect without changing files." }, - }); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - transport.push("paperclip/workspaceChange/updated", { - threadId: "thread-1", - turnId: turn.turnId, - workspaceChange: { - schema: "paperclip.workspace.diff.v1", - changeSetId: `${turn.turnId}:workspace`, - revision: 1, - source: "harness_reported", - complete: false, - files: [], - totals: { files: 0, additions: 0, deletions: 0 }, - patchArtifactRef: null, - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "interrupted", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect(events.find((event) => event.eventType === "workspace.change.updated")?.payload) - .toMatchObject({ revision: 1, complete: false, totals: { files: 0 } }); - expect(events.filter((event) => event.eventType === "workspace.diff.recorded")) - .toHaveLength(1); - expect(events.find((event) => event.eventType === "workspace.diff.recorded")?.payload) - .toMatchObject({ revision: 1, source: "runner_verified", complete: true, totals: { files: 0 } }); - expect(events.some((event) => event.eventType === "turn.interrupted")).toBe(true); - }); - - it("accepts a thread usage snapshot replayed before a resumed turn starts", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-usage-replay", - normalizedSessionId: "normalized-usage-replay", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - transport.push("thread/tokenUsage/updated", { - threadId: "thread-1", - turnId: "prior-turn", - tokenUsage: { total: { inputTokens: 10, outputTokens: 4 } }, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Continue." }, - }); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect(events.some((event) => event.eventType === "session.failed")).toBe( - false, - ); - expect(await session.usage()).toMatchObject({ - total: { inputTokens: 10, outputTokens: 4 }, - }); - }); - - it("normalizes provider message and reasoning phases onto every streamed item event", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-stream-channels", - normalizedSessionId: "normalized-stream-channels", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const turn = await session.startTurn({ - message: { role: "user", text: "Stream progress." }, - }); - transport.push("turn/started", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "inProgress" }, - }); - transport.push("item/started", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "progress-1", - type: "agentMessage", - phase: "commentary", - text: "", - }, - }); - transport.push("item/agentMessage/delta", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "progress-1", - delta: "Running it now.", - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "progress-1", - type: "agentMessage", - phase: "commentary", - text: "Running it now.", - }, - }); - transport.push("item/started", { - threadId: "thread-1", - turnId: turn.turnId, - item: { id: "reason-1", type: "reasoning", summary: [] }, - }); - transport.push("item/reasoning/summaryTextDelta", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "reason-1", - delta: "Summary", - }); - transport.push("item/reasoning/textDelta", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "reason-1", - delta: "Detail", - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "reason-1", - type: "reasoning", - summary: [{ text: "Summary" }], - }, - }); - transport.push("item/started", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "final-1", - type: "agentMessage", - phase: "final_answer", - text: "", - }, - }); - transport.push("item/agentMessage/delta", { - threadId: "thread-1", - turnId: turn.turnId, - itemId: "final-1", - delta: JSON.stringify(result), - }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: turn.turnId, - item: { - id: "final-1", - type: "agentMessage", - phase: "final_answer", - text: JSON.stringify(result), - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: turn.turnId, status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - const pick = (kind: string, channel: string) => - events.find( - (event) => - event.payload.kind === kind && event.payload.channel === channel, - ); - expect(pick("agentMessage", "progress")?.payload).toMatchObject({ - providerPhase: "commentary", - }); - expect( - events.find( - (event) => - event.eventType === "item.delta" && - event.payload.channel === "progress", - )?.payload, - ).toMatchObject({ - providerMethod: "item/agentMessage/delta", - text: "Running it now.", - }); - expect( - events.find( - (event) => - event.eventType === "item.delta" && - event.payload.channel === "summary", - )?.payload, - ).toMatchObject({ providerMethod: "item/reasoning/summaryTextDelta" }); - expect( - events.find( - (event) => - event.eventType === "item.delta" && - event.payload.channel === "detail", - )?.payload, - ).toMatchObject({ providerMethod: "item/reasoning/textDelta" }); - expect(pick("agentMessage", "final")?.payload).toMatchObject({ - providerPhase: "final_answer", - }); - }); - - it("captures an exact skillless model/environment snapshot with credentials absent", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-context", - normalizedSessionId: "normalized-context", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const iterator = session.events()[Symbol.asyncIterator](); - const first = await iterator.next(); - const context = first.value?.payload.context as Parameters< - typeof isSkilllessCodexContext - >[0]; - expect(context).toMatchObject({ - codexVersion: "codex-cli/0.132.0", - model: "gpt-test", - modelProvider: "openai", - workingDirectory: TEST_WORKING_DIRECTORY, - approvalPolicy: "never", - instructionSources: [], - instructionPolicy: { - skillInstructions: false, - appInstructions: false, - collaborationInstructions: true, - }, - environmentKeys: ["LANG", "PATH"], - envelope, - }); - expect(isSkilllessCodexContext(context)).toBe(true); - expect(JSON.stringify(context)).not.toContain("must-not-pass"); - expect(JSON.stringify(transport.calls)).not.toContain("RANDOM_SKILL_PATH"); - expect( - transport.calls.find((call) => call.method === "thread/start")?.params, - ).toMatchObject({ - approvalPolicy: "never", - config: { - "skills.include_instructions": false, - include_apps_instructions: false, - include_collaboration_mode_instructions: true, - "features.image_generation": false, - }, - permissions: "paperclip-runner-workspace-only", - runtimeWorkspaceRoots: [TEST_WORKING_DIRECTORY], - dynamicTools: [{ name: "paperclip_finish" }, { name: "paperclip_block" }], - }); - const commandPolicy = transport.calls.find( - (call) => call.method === "thread/start", - )?.params.config as Record; - expect(JSON.stringify(commandPolicy)).not.toContain("/isolated/home"); - expect(JSON.stringify(commandPolicy)).not.toContain("/isolated/codex"); - expect(JSON.stringify(commandPolicy)).not.toContain("CODEX_HOME"); - const appServerArgs = createIsolatedCodexAppServerArgs({ - PATH: "/bin", - LANG: "C.UTF-8", - HOME: "/isolated/home", - CODEX_HOME: "/isolated/codex", - }); - expect(appServerArgs).toEqual( - expect.arrayContaining([ - expect.stringContaining( - 'default_permissions="paperclip-runner-workspace-only"', - ), - expect.stringContaining( - "permissions.paperclip-runner-workspace-only.filesystem=", - ), - "permissions.paperclip-runner-workspace-only.network.enabled=false", - 'shell_environment_policy.inherit="none"', - expect.stringContaining( - 'shell_environment_policy.set={PATH="/bin",LANG="C.UTF-8"}', - ), - "--disable", - "image_generation", - ]), - ); - expect( - appServerArgs.find((arg) => arg.startsWith("permissions.")), - ).toContain('"/isolated/home"="none"'); - expect( - appServerArgs.find((arg) => arg.startsWith("permissions.")), - ).toContain('"/isolated/codex"="none"'); - expect(JSON.stringify(appServerArgs)).not.toContain("HOME="); - expect(CODEX_RESULT_OUTPUT_SCHEMA.properties.schema).toEqual({ - type: "string", - const: "paperclip.run_result.v1", - }); - expect( - CODEX_BLOCK_RESULT_OUTPUT_SCHEMA.properties.reportedWorkDisposition, - ).toEqual({ - type: "string", - const: "blocked", - }); - }); - - it.each(["never", "on-request", "untrusted"] as const)( - "pins approval policy %s for both thread start and resume", - async (approvalPolicy) => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const driver = makeDriver([first, second], { approvalPolicy }); - const original = await driver.openSession({ - runId: `run-policy-${approvalPolicy}`, - normalizedSessionId: `normalized-policy-${approvalPolicy}`, - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const started = await original.events()[Symbol.asyncIterator]().next(); - const snapshot = await original.snapshot(); - await original.close({ reason: "policy recovery test" }); - const recovered = await driver.recoverSession?.(snapshot); - expect(recovered).toMatchObject({ recovered: true }); - const resumed = await recovered!.session!.events()[Symbol.asyncIterator]().next(); - expect(first.calls.find((call) => call.method === "thread/start")?.params) - .toMatchObject({ approvalPolicy }); - expect(second.calls.find((call) => call.method === "thread/resume")?.params) - .toMatchObject({ approvalPolicy }); - expect(started.value).toMatchObject({ eventType: "session.started", payload: { context: { approvalPolicy } } }); - expect(resumed.value).toMatchObject({ eventType: "session.resumed", payload: { context: { approvalPolicy } } }); - await recovered?.session?.close({ reason: "policy recovery complete" }); - }, - ); - - it.each([false, true])("requires trusted continuation metadata before omitting task context (%s)", async (continuation) => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport], { - skillInputs: [{ type: "skill", name: "first-task", path: "/skills/first-task/SKILL.md" }], - }).openSession({ runId: "run-delta", normalizedSessionId: "session-delta", workingDirectory: TEST_WORKING_DIRECTORY }); - const text = JSON.stringify({ schema: "paperclip.native-continuation.v1", events: '{"messages":[{"body":"Go ahead"}]}', completion: { revision: "2", criterionIds: ["comment"] } }); - await session.startTurn({ message: { role: "user", text }, ...(continuation ? { continuation: true as const } : {}) }); - const params = transport.calls.find((call) => call.method === "turn/start")!.params; - if (continuation) { - expect(params.input).toEqual([{ type: "text", text, text_elements: [] }]); - expect(JSON.stringify(params.input)).not.toContain("constraints"); - expect(JSON.stringify(params.input)).not.toContain("first-task"); - } else { - expect(JSON.stringify(params.input)).toContain("constraints"); - expect(params.input).toContainEqual({ type: "skill", name: "first-task", path: "/skills/first-task/SKILL.md" }); - } - await session.close({ reason: "test complete" }); - }); - - it("allows eval fixtures to opt out of Codex collaboration instructions", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport], { - includeCollaborationModeInstructions: false, - }).openSession({ - runId: "run-no-collaboration-instructions", - normalizedSessionId: "session-no-collaboration-instructions", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const first = await session.events()[Symbol.asyncIterator]().next(); - - expect(first.value?.payload.context).toMatchObject({ - instructionPolicy: { collaborationInstructions: false }, - }); - expect( - transport.calls.find((call) => call.method === "thread/start")?.params, - ).toMatchObject({ - config: { include_collaboration_mode_instructions: false }, - }); - }); - - it("negotiates genuine plan mode with collaboration instructions and read-only workspace access", async () => { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { - requestedCollaborationMode: "plan", - }); - const session = await driver.openSession({ - runId: "run-plan", - normalizedSessionId: "session-plan", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - expect( - transport.calls.find((call) => call.method === "thread/start")?.params, - ).toMatchObject({ - permissions: "paperclip-runner-workspace-read-only", - config: { include_collaboration_mode_instructions: true }, - }); - expect( - transport.calls.find((call) => call.method === "thread/start")?.params, - ).not.toHaveProperty("collaborationMode"); - expect( - transport.calls.some((call) => call.method === "collaborationMode/list"), - ).toBe(true); - await expect( - session.startTurn({ - message: { role: "user", text: "Author a plan." }, - requestedCollaborationMode: "plan", - }), - ).resolves.toMatchObject({ effectiveCollaborationMode: "plan" }); - expect( - transport.calls.find((call) => call.method === "turn/start")?.params, - ).toMatchObject({ - collaborationMode: { - mode: "plan", - settings: { - model: "gpt-test", - reasoning_effort: "high", - developer_instructions: null, - }, - }, - permissions: "paperclip-runner-workspace-read-only", - }); - }); - - it("sends the selected reasoning effort with the Codex turn", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport], { model: "gpt-6-astra", reasoningEffort: "ultra" }).openSession({ - runId: "run-effort", - normalizedSessionId: "session-effort", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Continue" } }); - expect(transport.calls.find((call) => call.method === "turn/start")?.params).toMatchObject({ effort: "ultra" }); - await session.close({ reason: "test complete" }); - }); - - it("fails closed when the installed app-server does not confirm plan mode", async () => { - const transport = new FakeCodexTransport(); - transport.confirmCollaborationMode = false; - await expect( - makeDriver([transport], { - requestedCollaborationMode: "plan", - }).openSession({ - runId: "run-plan-unsupported", - normalizedSessionId: "session-plan-unsupported", - workingDirectory: TEST_WORKING_DIRECTORY, - }), - ).rejects.toThrow("planning_mode_unsupported"); - }); - - it("refuses to turn host credential roots into model-writable workspaces", async () => { - await expect( - makeDriver([], { - environment: { - PATH: "/bin", - HOME: "/isolated/home", - CODEX_HOME: TEST_WORKING_DIRECTORY, - }, - }).openSession({ - runId: "run-codex-home", - normalizedSessionId: "normalized-codex-home", - workingDirectory: TEST_WORKING_DIRECTORY, - }), - ).rejects.toThrow("cannot overlap host CODEX_HOME"); - await expect( - makeDriver([], { - environment: { - PATH: "/bin", - HOME: TEST_WORKING_DIRECTORY, - CODEX_HOME: "/isolated/codex", - }, - }).openSession({ - runId: "run-host-home", - normalizedSessionId: "normalized-host-home", - workingDirectory: TEST_WORKING_DIRECTORY, - }), - ).rejects.toThrow("cannot contain the host HOME"); - await expect( - makeDriver([]).openSession({ - runId: "run-root", - normalizedSessionId: "normalized-root", - workingDirectory: "/", - }), - ).rejects.toThrow("cannot be a filesystem root"); - }); - - it("uses negotiated ACP controls through the Codex transport facade", async () => { - let controls = { steering: false, queuedFollowUp: false }; - const transport = Object.assign(new FakeCodexTransport(), { turnControlCapabilities: () => ({ ...controls }) }); - const session = await makeDriver([transport], { - driverIdentity: { kind: "acpx_runtime", displayName: "Pi ACP", version: "test" }, - capabilities: { steering: false }, - }).openSession({ runId: "run-pi", normalizedSessionId: "session-pi", workingDirectory: TEST_WORKING_DIRECTORY }); - try { - expect(session.turnControlCapabilities?.()).toEqual(controls); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); - await expect(session.steer?.({ turnId, message: { role: "user", text: "Change" } })).rejects.toThrow("not negotiated"); - controls = { steering: true, queuedFollowUp: true }; - expect(session.turnControlCapabilities?.()).toEqual(controls); - const input = { turnId, correlationId: "queued-1", mode: "follow_up" as const, message: { role: "user" as const, text: "Then validate" } }; - await session.steer?.(input); - expect(transport.calls.find(call => call.method === "turn/steer")?.params).toMatchObject({ expectedTurnId: turnId, mode: "follow_up", correlationId: "queued-1" }); - await expect(session.steer?.(input)).rejects.toThrow("already acknowledged"); - expect(transport.calls.filter(call => call.method === "turn/steer")).toHaveLength(1); - } finally { await session.close({ reason: "verified" }); } - }); - - it("keeps native follow-up disabled for the Codex app-server driver", async () => { - const transport = Object.assign(new FakeCodexTransport(), { turnControlCapabilities: () => ({ steering: true, queuedFollowUp: true }) }); - const session = await makeDriver([transport]).openSession({ runId: "run-codex", normalizedSessionId: "session-codex", workingDirectory: TEST_WORKING_DIRECTORY }); - try { - expect(session.turnControlCapabilities?.()).toEqual({ steering: true, queuedFollowUp: false }); - const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); - await expect(session.steer?.({ turnId, mode: "follow_up", message: { role: "user", text: "Queue" } })).rejects.toThrow("not negotiated"); - expect(transport.calls.filter(call => call.method === "turn/steer")).toHaveLength(0); - } finally { await session.close({ reason: "verified" }); } - }); - - it("steers and interrupts an active turn without replacing the session", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-controls", - normalizedSessionId: "normalized-controls", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Start." }, - }); - await session.steer?.({ - turnId, - message: { role: "user", text: "Use a shorter answer." }, - }); - await session.interrupt?.({ turnId, reason: "operator requested" }); - expect(transport.calls.map((call) => call.method)).toEqual([ - "initialize", - "thread/start", - "thread/goal/get", - "turn/start", - "turn/steer", - "turn/interrupt", - ]); - expect(session.ids()).toEqual({ - driverSessionId: "thread-1", - providerSessionId: "provider-session-1", - displayId: "thread-1", - }); - }); - - it("loads the deterministic Live console wire fixture", async () => { - const fixture = await loadLiveConsoleConformanceFixture( - liveConsoleFixturePath, - ); - expect( - fixture.runtimeRequests.map(({ requestKind }) => requestKind), - ).toEqual([ - "command_approval", - "file_approval", - "permission_approval", - "user_input", - "elicitation", - ]); - expect(fixture.goals.map(({ action }) => action)).toEqual([ - "get", - "set", - "pause", - "resume", - "clear", - ]); - }); - - it("holds browser-resolved upstream requests and returns the exact fixture responses", async () => { - const fixture = await loadLiveConsoleConformanceFixture( - liveConsoleFixturePath, - ); - for (const scenario of fixture.runtimeRequests) { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: `run-${scenario.id}`, - normalizedSessionId: `normalized-${scenario.id}`, - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Exercise request." }, - }); - const response = transport.invoke({ - id: scenario.id, - method: scenario.method, - params: { - threadId: "thread-1", - turnId, - itemId: `item-${scenario.id}`, - reason: `fixture ${scenario.id}`, - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(session.pendingRuntimeRequests?.()).toEqual([ - expect.objectContaining({ - requestId: scenario.id, - requestKind: scenario.requestKind, - method: scenario.method, - turnId, - }), - ]); - await expect( - session.resolveRuntimeRequest?.({ - requestId: scenario.id, - turnId, - resolution: { - action: "forged", - } as unknown as HarnessRuntimeRequestResolution, - }), - ).rejects.toThrow("unsupported action"); - expect(session.pendingRuntimeRequests?.()).toHaveLength(1); - await session.resolveRuntimeRequest?.({ - requestId: scenario.id, - turnId, - resolution: scenario.resolution as HarnessRuntimeRequestResolution, - }); - expect(await response).toEqual(scenario.expectedResponse); - expect(session.pendingRuntimeRequests?.()).toEqual([]); - await expect( - session.resolveRuntimeRequest?.({ - requestId: scenario.id, - turnId, - resolution: { action: "cancel" }, - }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - await session.close({ reason: "fixture complete" }); - } - }); - - it("acknowledges same-turn steering and rejects stale or child steering", async () => { - const fixture = await loadLiveConsoleConformanceFixture( - liveConsoleFixturePath, - ); - const transport = new FakeCodexTransport("thread-root"); - const session = await makeDriver([transport]).openSession({ - runId: "run-live-console-controls", - normalizedSessionId: "normalized-live-console-controls", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Start." }, - }); - await session.steer?.({ - turnId, - message: { role: "user", text: "Stay concise." }, - correlationId: "queued-comment-1", - }); - transport.push("item/completed", { - kind: "steering_acknowledgement", - status: "acknowledged", - commandId: "runnerd-steer-command", - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - await session.steer?.({ - turnId, - message: { role: "user", text: "Stay concise." }, - correlationId: "queued-comment-1", - }); - expect( - transport.calls.find((call) => call.method === "turn/steer")?.params, - ).toMatchObject({ correlationId: "queued-comment-1" }); - expect( - transport.calls.filter((call) => call.method === "turn/steer"), - ).toHaveLength(1); - transport.rejectMethods.set( - "turn/steer", - new Error("provider rejected steering"), - ); - const rejectedSteering = await session - .steer?.({ - turnId, - message: { role: "user", text: "Try this again later." }, - correlationId: "queued-comment-2", - }) - .then( - () => null, - (error: unknown) => error, - ); - expect(rejectedSteering).toBeInstanceOf(Error); - expect(rejectedSteering).not.toBeInstanceOf( - HarnessCapabilityUnavailableError, - ); - expect(String(rejectedSteering)).toContain("provider rejected steering"); - transport.rejectMethods.delete("turn/steer"); - await expect( - session.steer?.({ - turnId: fixture.controls.staleTurnSteer.turnId!, - message: { role: "user", text: "Stale." }, - }), - ).rejects.toBeInstanceOf(HarnessStaleTurnError); - transport.push("thread/started", { thread: fixture.lineage.childThread }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(session.lineage?.()).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - threadId: "thread-root", - parentThreadId: null, - depth: 0, - }), - expect.objectContaining({ - threadId: "thread-child", - parentThreadId: "thread-root", - depth: 1, - nickname: "Scout", - role: "researcher", - }), - ]), - ); - await expect( - session.steer?.({ - turnId: "thread-child", - message: { role: "user", text: "Do not emulate child steering." }, - }), - ).rejects.toBeInstanceOf(HarnessStaleTurnError); - const events = session.events()[Symbol.asyncIterator](); - const observed: PrpEvent[] = []; - // PRP v2 adds a capability and authoritative goal snapshot immediately - // after session start, so include those two events in this bounded read. - for (let index = 0; index < 11; index += 1) { - const next = await events.next(); - if (next.done) break; - observed.push(next.value); - } - expect(observed).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - eventType: "item.completed", - itemId: `${turnId}:steer:queued-comment-1`, - payload: expect.objectContaining({ - kind: "steering_acknowledgement", - status: "acknowledged", - }), - }), - expect.objectContaining({ - eventType: "harness.diagnostic", - payload: expect.objectContaining({ code: "stale_turn_rejected" }), - }), - expect.objectContaining({ - eventType: "item.started", - payload: expect.objectContaining({ kind: "thread_lineage" }), - }), - ]), - ); - expect(observed.some((event) => event.eventType === "session.failed")) - .toBe(false); - await session.close({ reason: "fixture complete" }); - }); - - it("queues an interrupt before turn identity and reports a terminal race precisely", async () => { - const transport = new FakeCodexTransport(); - let releaseStart!: (value: Record) => void; - transport.turnStartResponse = new Promise((resolve) => { - releaseStart = resolve; - }); - const session = await makeDriver([transport]).openSession({ - runId: "run-interrupt-races", - normalizedSessionId: "normalized-interrupt-races", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const starting = session.startTurn({ - message: { role: "user", text: "Start slowly." }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - await session.interrupt?.({ reason: "operator" }); - releaseStart({ turn: { id: "turn-1", status: "inProgress", items: [] } }); - await expect(starting).resolves.toEqual({ - turnId: "turn-1", - effectiveCollaborationMode: "default", - }); - expect(transport.calls.map(({ method }) => method)).toEqual( - expect.arrayContaining(["turn/start", "turn/interrupt"]), - ); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "interrupted", items: [] }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - await expect( - session.interrupt?.({ turnId: "turn-1" }), - ).rejects.toBeInstanceOf(HarnessOperationAlreadyTerminalError); - await session.close({ reason: "fixture complete" }); - }); - - it("maps all goal operations and advertises an exact unsupported state", async () => { - const fixture = await loadLiveConsoleConformanceFixture( - liveConsoleFixturePath, - ); - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-goals", - normalizedSessionId: "normalized-goals", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.goal?.({ action: "get" }); - await session.goal?.({ - action: "set", - objective: "Ship the Live console tracer", - tokenBudget: 4096, - }); - await session.goal?.({ action: "pause" }); - await session.goal?.({ action: "resume" }); - await session.goal?.({ action: "clear" }); - const goalCalls = transport.calls.filter(({ method }) => - method.startsWith("thread/goal/"), - ); - expect( - goalCalls.slice(1).map(({ method, params }) => ({ - method, - params: Object.fromEntries( - Object.entries(params).filter(([key]) => key !== "threadId"), - ), - })), - ).toEqual(fixture.goals.map(({ method, params }) => ({ method, params }))); - - expect( - (await makeDriver([new FakeCodexTransport()]).descriptor()).capabilities, - ).toMatchObject({ goals: true }); - - // Both denials a real app-server sends: the method is absent, and the - // build has the feature switched off. - for (const { denial, availability } of [ - { - denial: new CodexRpcError( - '{"code":-32601,"message":"method not found"}', - CODEX_METHOD_NOT_FOUND, - ), - availability: "unsupported", - }, - { - denial: new CodexRpcError( - '{"code":-32004,"message":"goals feature is disabled by policy"}', - -32_004, - ), - availability: "policy_disabled", - }, - ]) { - const unsupportedTransport = new FakeCodexTransport(); - unsupportedTransport.rejectMethods.set("thread/goal/get", denial); - const unsupportedDriver = makeDriver([unsupportedTransport]); - const unsupported = await unsupportedDriver.openSession({ - runId: "run-goals-unsupported", - normalizedSessionId: "normalized-goals-unsupported", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - expect((await unsupportedDriver.descriptor()).capabilities).toMatchObject( - { goals: false }, - ); - const unsupportedEvents = unsupported.events()[Symbol.asyncIterator](); - await unsupportedEvents.next(); - await expect(unsupportedEvents.next()).resolves.toMatchObject({ - value: { - eventType: "session.capabilities.updated", - payload: { sessionGoals: { availability } }, - }, - }); - await expect( - unsupported.goal?.({ action: "get" }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - await unsupported.close({ reason: "fixture complete" }); - } - await session.close({ reason: "fixture complete" }); - }); - - it("keeps goal support advertised when the probe fails transiently", async () => { - // A transport or protocol failure is evidence about this call, not about - // what the provider implements, so it must not retire the capability. - for (const failure of [ - new Error("codex app-server transport closed"), - new CodexRpcError('{"code":-32603,"message":"internal error"}', -32_603), - ]) { - const transport = new FakeCodexTransport(); - transport.rejectMethods.set("thread/goal/get", failure); - const driver = makeDriver([transport]); - const session = await driver.openSession({ - runId: "run-goals-transient", - normalizedSessionId: "normalized-goals-transient", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - expect((await driver.descriptor()).capabilities).toMatchObject({ - goals: true, - }); - // The capability survives, so the operation is still offered and the - // next call reaches the provider instead of failing closed locally. - transport.rejectMethods.delete("thread/goal/get"); - await expect(session.goal?.({ action: "get" })).resolves.toBeNull(); - expect( - transport.calls.filter(({ method }) => method === "thread/goal/get"), - ).toHaveLength(2); - await session.close({ reason: "fixture complete" }); - } - }); - - it("preserves an inactive goal status while editing its objective", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-goal-inactive-edit", - normalizedSessionId: "normalized-goal-inactive-edit", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - - await session.goal?.({ - action: "set", - objective: "Updated while paused", - status: "paused", - }); - - expect( - transport.calls.filter(({ method }) => method === "thread/goal/set"), - ).toContainEqual({ - method: "thread/goal/set", - params: { - threadId: "thread-1", - objective: "Updated while paused", - status: "paused", - }, - }); - await session.close({ reason: "fixture complete" }); - }); - - it("rolls back only a definitely rejected idle goal autostart", async () => { - const definiteTransport = new FakeCodexTransport(); - const definiteSession = await makeDriver([definiteTransport]).openSession({ - runId: "run-goal-autostart-definite-rejection", - normalizedSessionId: "normalized-goal-autostart-definite-rejection", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - definiteTransport.rejectMethods.set( - "thread/goal/set", - new CodexRpcError('{"code":-32603,"message":"internal error"}', -32_603), - ); - await expect( - definiteSession.goal?.({ - action: "set", - objective: "Rejected before a turn starts", - }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - definiteTransport.rejectMethods.delete("thread/goal/set"); - await expect( - definiteSession.startTurn({ - message: { role: "user", text: "A normal turn may still start." }, - }), - ).resolves.toMatchObject({ turnId: "turn-1" }); - await definiteSession.close({ reason: "fixture complete" }); - - const ambiguousTransport = new FakeCodexTransport(); - const ambiguousSession = await makeDriver([ambiguousTransport]).openSession({ - runId: "run-goal-autostart-ambiguous", - normalizedSessionId: "normalized-goal-autostart-ambiguous", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - ambiguousTransport.rejectMethods.set( - "thread/goal/set", - new Error("codex app-server transport closed"), - ); - await expect( - ambiguousSession.goal?.({ - action: "set", - objective: "The provider may have started this goal", - }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - ambiguousTransport.rejectMethods.delete("thread/goal/set"); - await expect( - ambiguousSession.startTurn({ - message: { role: "user", text: "Do not create competing work." }, - }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - await ambiguousSession.close({ reason: "fixture complete" }); - }); - - it("preserves a provider-neutral goal action subset through the Codex transport facade", async () => { - const transport = new FakeCodexTransport(); - const driver = makeDriver([transport], { - goalCapability: { - actions: ["set", "clear"], - autonomousUpdates: true, - persistentAcrossResume: true, - maxObjectiveChars: 4_000, - tokenBudgetControl: false, - usageReporting: true, - }, - }); - const session = await driver.openSession({ - runId: "run-goals-action-subset", - normalizedSessionId: "normalized-goals-action-subset", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const events = session.events()[Symbol.asyncIterator](); - await events.next(); - await expect(events.next()).resolves.toMatchObject({ - value: { - eventType: "session.capabilities.updated", - payload: { - sessionGoals: { - availability: "available", - actions: ["set", "clear"], - tokenBudgetControl: false, - }, - }, - }, - }); - - await expect( - session.goal?.({ action: "pause" }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - expect( - transport.calls.filter(({ method }) => method === "thread/goal/set"), - ).toHaveLength(0); - await expect( - session.goal?.({ action: "set", objective: "Finish the task" }), - ).resolves.toMatchObject({ objective: "Finish the task" }); - await expect(session.goal?.({ action: "clear" })).resolves.toBeNull(); - await session.close({ reason: "fixture complete" }); - }); - - it("validates runtime request resolutions against the kind of request they answer", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-resolution-shapes", - normalizedSessionId: "normalized-resolution-shapes", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask me things." }, - }); - - // Not `async`: an async function would flatten and await the pending - // provider response, which only settles once the request is resolved. - function open( - id: string, - method: string, - ): Promise> { - return transport.invoke({ - id, - method, - params: { - threadId: "thread-1", - turnId, - itemId: `item-${id}`, - reason: `fixture ${id}`, - }, - }); - } - const settled = () => new Promise((resolve) => setTimeout(resolve, 0)); - - const userInput = open("ask-input", "item/tool/requestUserInput"); - await settled(); - for (const resolution of [ - { action: "submit" }, - { action: "submit", answers: {} }, - { action: "submit", answers: { field: {} } }, - { action: "submit", answers: { field: { answers: [7] } } }, - { action: "submit", content: { answer: "wrong shape" } }, - { action: "accept" }, - ]) { - await expect( - session.resolveRuntimeRequest?.({ - requestId: "ask-input", - turnId, - resolution: resolution as unknown as HarnessRuntimeRequestResolution, - }), - ).rejects.toThrow(/user_input rejected its resolution/); - // A rejected resolution must leave the request answerable. - expect(session.pendingRuntimeRequests?.()).toHaveLength(1); - } - await session.resolveRuntimeRequest?.({ - requestId: "ask-input", - turnId, - resolution: { - action: "submit", - answers: { field: { answers: ["staging"] } }, - }, - }); - expect(await userInput).toEqual({ - answers: { field: { answers: ["staging"] } }, - }); - - const elicitation = open( - "ask-elicitation", - "mcpServer/elicitation/request", - ); - await settled(); - for (const resolution of [ - { action: "submit" }, - { action: "submit", content: {} }, - { action: "submit", answers: { field: { answers: ["wrong shape"] } } }, - { action: "accept_for_session" }, - ]) { - await expect( - session.resolveRuntimeRequest?.({ - requestId: "ask-elicitation", - turnId, - resolution: resolution as unknown as HarnessRuntimeRequestResolution, - }), - ).rejects.toThrow(/elicitation rejected its resolution/); - expect(session.pendingRuntimeRequests?.()).toHaveLength(1); - } - await session.resolveRuntimeRequest?.({ - requestId: "ask-elicitation", - turnId, - resolution: { action: "submit", content: { answer: "green" } }, - }); - expect(await elicitation).toEqual({ - action: "accept", - content: { answer: "green" }, - _meta: null, - }); - - const approval = open( - "ask-approval", - "item/commandExecution/requestApproval", - ); - await settled(); - await expect( - session.resolveRuntimeRequest?.({ - requestId: "ask-approval", - turnId, - resolution: { - action: "submit", - answers: { field: { answers: ["nope"] } }, - } as unknown as HarnessRuntimeRequestResolution, - }), - ).rejects.toThrow(/command_approval rejected its resolution/); - await session.resolveRuntimeRequest?.({ - requestId: "ask-approval", - turnId, - resolution: { action: "accept" }, - }); - expect(await approval).toEqual({ decision: "accept" }); - await session.close({ reason: "fixture complete" }); - }); - - it("replays the DOT-185 requestUserInput shape as one canonical three-question request", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-dot-185", - normalizedSessionId: "normalized-dot-185", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask the deployment questions." }, - }); - const resolvedEvent = (async () => { - for await (const event of session.events()) { - if (event.eventType === "runtime_request.resolved" - && event.payload.requestId === "dot-185-request") return event; - } - return null; - })(); - const nativeResponse = transport.invoke({ - id: "dot-185-request", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "dot-185-item", - questions: [ - { - id: "environment", - header: "Environment", - question: "Where should we deploy?", - options: [ - { label: "Staging", description: "Deploy to staging first." }, - { label: "Production", description: "Deploy directly to production." }, - ], - isOther: true, - }, - { - id: "regions", - header: "Regions", - question: "Which regions should receive the release?", - options: [{ label: "US" }, { label: "EU" }], - multiSelect: true, - }, - { - id: "notes", - header: "Notes", - question: "Anything else we should know?", - required: false, - }, - ], - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - const [pending] = session.pendingRuntimeRequests?.() ?? []; - expect(pending?.input).toMatchObject({ - schema: "paperclip.question_set.v1", - questions: [ - { - id: "environment", - required: false, - answerMode: "single_select", - customAnswer: { enabled: true }, - options: [ - { id: "option-1", label: "Staging", description: "Deploy to staging first." }, - { id: "option-2", label: "Production", description: "Deploy directly to production." }, - ], - }, - { id: "regions", answerMode: "multi_select", required: false }, - { id: "notes", answerMode: "text", required: false }, - ], - }); - await session.resolveRuntimeRequest?.({ - requestId: "dot-185-request", - turnId, - resolution: { - action: "submit", - response: { - schema: "paperclip.question_response.v1", - answers: { - environment: { selectedOptionIds: ["option-1"] }, - regions: { selectedOptionIds: ["option-1", "option-2"] }, - notes: { text: "Ship during the maintenance window." }, - }, - }, - }, - }); - expect(await nativeResponse).toEqual({ - answers: { - environment: { answers: ["Staging"] }, - regions: { answers: ["US", "EU"] }, - notes: { answers: ["Ship during the maintenance window."] }, - }, - }); - expect((await resolvedEvent)?.payload).toMatchObject({ - action: "submit", - response: { - schema: "paperclip.question_response.v1", - answers: { - environment: { selectedOptionIds: ["option-1"] }, - regions: { selectedOptionIds: ["option-1", "option-2"] }, - notes: { text: "Ship during the maintenance window." }, - }, - }, - }); - await session.close({ reason: "fixture complete" }); - }); - - it("rejects an explicit malformed Codex form without falling back to v1", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-malformed-input", - normalizedSessionId: "normalized-malformed-input", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask a malformed question." }, - }); - const diagnostic = (async () => { - for await (const event of session.events()) { - if (event.eventType === "harness.diagnostic" && event.payload.code === "runtime_input_rejected") { - return event; - } - } - return null; - })(); - await expect(transport.invoke({ - id: "malformed-input", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "malformed-item", - questions: [ - { id: "duplicate", question: "First?" }, - { id: "duplicate", question: "Second?" }, - ], - }, - })).resolves.toEqual({ answers: {} }); - expect(session.pendingRuntimeRequests?.()).toEqual([]); - expect(await diagnostic).toEqual(expect.objectContaining({ - eventType: "harness.diagnostic", - payload: expect.objectContaining({ - code: "runtime_input_rejected", - adapter: "codex-app-server", - }), - })); - await session.close({ reason: "fixture complete" }); - }); - - it("emits one canonical outcome payload for every terminal runtime request fact", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-outcome", - normalizedSessionId: "normalized-outcome", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const events: PrpEvent[] = []; - void (async () => { - for await (const event of session.events()) events.push(event); - })(); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Approve this." }, - }); - const resolved = transport.invoke({ - id: "outcome-resolved", - method: "item/commandExecution/requestApproval", - params: { - threadId: "thread-1", - turnId, - itemId: "item-resolved", - reason: "approve", - }, - }); - const cancelled = transport.invoke({ - id: "outcome-cancelled", - method: "item/commandExecution/requestApproval", - params: { - threadId: "thread-1", - turnId, - itemId: "item-cancelled", - reason: "approve", - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - await session.resolveRuntimeRequest?.({ - requestId: "outcome-resolved", - turnId, - resolution: { action: "accept_for_session" }, - }); - await resolved; - await session.close({ reason: "operator_closed" }); - await cancelled; - await new Promise((resolve) => setTimeout(resolve, 0)); - - const terminal = events.filter( - ({ eventType }) => - eventType === "runtime_request.resolved" || - eventType === "runtime_request.cancelled", - ); - expect( - terminal.map(({ eventType, turnId, itemId, payload }) => ({ - eventType, - turnId, - itemId, - payload, - })), - ).toEqual([ - { - eventType: "runtime_request.resolved", - turnId, - itemId: "item-resolved", - payload: { - requestId: "outcome-resolved", - requestKind: "command_approval", - turnId, - itemId: "item-resolved", - action: "accept_for_session", - }, - }, - { - eventType: "runtime_request.cancelled", - turnId, - itemId: "item-cancelled", - payload: { - requestId: "outcome-cancelled", - requestKind: "command_approval", - turnId, - itemId: "item-cancelled", - reason: "session_closed", - }, - }, - ]); - for (const event of terminal) expect(validatePrpEvent(event).ok).toBe(true); - }); - - it("expires a canonical input with its full question set when the provider transport is lost", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-provider-loss", - normalizedSessionId: "normalized-provider-loss", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const events: PrpEvent[] = []; - const consume = (async () => { - for await (const event of session.events()) events.push(event); - })(); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask before continuing." }, - }); - const pending = transport.invoke({ - id: "lost-input", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "lost-input-item", - questions: [{ - id: "environment", - header: "Environment", - question: "Where should we deploy?", - options: [{ label: "Staging" }, { label: "Production" }], - }], - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - transport.queue.fail(new Error("provider exited")); - expect(await pending).toEqual({ answers: {} }); - await consume; - - const expired = events.find((event) => event.eventType === "runtime_request.expired"); - expect(expired).toMatchObject({ - turnId, - itemId: "lost-input-item", - payload: { - requestId: "lost-input", - reason: "provider_process_lost", - replayAllowed: false, - requestType: "input", - request: { - schema: "paperclip.runtime_request.v2", - requestKind: "runtime", - requestId: "lost-input", - type: "input", - input: { - schema: "paperclip.question_set.v1", - questions: [expect.objectContaining({ id: "environment" })], - }, - }, - }, - }); - expect(events.filter((event) => event.eventType === "runtime_request.cancelled")).toHaveLength(0); - expect(validatePrpEvent(expired!)).toMatchObject({ ok: true }); - }); - - it("hands a live canonical input off exactly once when the durable window expires", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-durable-handoff", - normalizedSessionId: "normalized-durable-handoff", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const iterator = session.events()[Symbol.asyncIterator](); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask before continuing." }, - }); - const pending = transport.invoke({ - id: "handoff-input", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "handoff-input-item", - questions: [{ - id: "environment", - header: "Environment", - question: "Where should we deploy?", - options: [{ label: "Staging" }, { label: "Production" }], - }], - }, - }); - let created: PrpEvent | null = null; - for (let count = 0; count < 20; count += 1) { - const event = await iterator.next(); - if (event.done) break; - if (event.value.eventType === "runtime_request.created") { - created = event.value; - break; - } - } - expect(created).not.toBeNull(); - - const firstHandoff = session.handoffRuntimeRequest!({ - requestId: "handoff-input", - turnId, - reason: "durable_handoff", - signal: new AbortController().signal, - }); - expect(firstHandoff.result).toBe("handed_off"); - await expect(firstHandoff.cleanup).resolves.toBeUndefined(); - const repeatedHandoff = session.handoffRuntimeRequest!({ - requestId: "handoff-input", - turnId, - reason: "durable_handoff", - signal: new AbortController().signal, - }); - expect(repeatedHandoff.result).toBe("already_settled"); - await expect(repeatedHandoff.cleanup).resolves.toBeUndefined(); - expect(await pending).toEqual({ answers: {} }); - - let expired: PrpEvent | null = null; - for (let count = 0; count < 20; count += 1) { - const event = await iterator.next(); - if (event.done) break; - if (event.value.eventType === "runtime_request.expired") { - expired = event.value; - break; - } - } - expect(expired).toMatchObject({ - turnId, - itemId: "handoff-input-item", - payload: { - requestId: "handoff-input", - reason: "durable_handoff", - replayAllowed: false, - requestType: "input", - request: { - schema: "paperclip.runtime_request.v2", - requestId: "handoff-input", - type: "input", - }, - }, - }); - await session.close({ reason: "test complete" }); - }); - - it("exposes and dispatches only explicit chat tools across fresh and resumed direct chat", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const registerDeliverable = { - name: "register_deliverable", - description: "Prepare one requested file.", - inputSchema: { type: "object", properties: {} }, - }; - const readCurrentWakeComments = { - name: "read_current_wake_comments", - description: "Read only comments bound into the current wake.", - inputSchema: { type: "object", properties: {} }, - }; - const requestHumanInput = { - name: "request_human_input", - description: "Ask one structured question through Paperclip.", - inputSchema: { type: "object", properties: {} }, - }; - const listChatAttachments = { - name: "list_chat_attachments", - description: "List same-conversation attachment metadata.", - inputSchema: { type: "object", properties: {} }, - }; - const reuseChatAttachment = { - name: "reuse_chat_attachment", - description: "Prepare one same-conversation attachment again.", - inputSchema: { type: "object", properties: {} }, - }; - const readChatAttachment = { - name: "read_chat_attachment", - description: "Read one same-conversation file without resending it.", - inputSchema: { type: "object", properties: {} }, - }; - const handler = vi.fn(async (call) => ({ - interaction: { id: "interaction-direct-question", status: "pending" }, - callId: call.callId, - })); - const driver = makeDriver([first, second], { - conversationMode: "direct", - dynamicTools: [ - registerDeliverable, - readCurrentWakeComments, - requestHumanInput, - listChatAttachments, - reuseChatAttachment, - readChatAttachment, - { - name: "report_progress", - description: "Must remain unavailable in direct chat.", - inputSchema: { type: "object", properties: {} }, - }, - ], - dynamicToolHandler: handler, - }); - const original = await driver.openSession({ - runId: "run-direct-file", - normalizedSessionId: "normalized-direct-file", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ - message: { role: "user", text: "Please return one file." }, - }); - const snapshot = await original.snapshot(); - await original.close({ reason: "transport lost" }); - - expect( - first.calls.find((call) => call.method === "thread/start")?.params - .dynamicTools, - ).toEqual([ - registerDeliverable, - readCurrentWakeComments, - requestHumanInput, - listChatAttachments, - reuseChatAttachment, - readChatAttachment, - ]); - - const freshQuestion = await first.invoke({ - id: "rpc-direct-question-fresh", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-direct-question-fresh", - tool: "request_human_input", - arguments: { interactionKind: "questions" }, - }, - }); - expect(freshQuestion).toMatchObject({ success: true }); - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ - tool: "request_human_input", - callId: "call-direct-question-fresh", - arguments: { interactionKind: "questions" }, - }), - ); - - const recovery = await driver.recoverSession?.(snapshot); - expect(recovery).toMatchObject({ recovered: true }); - expect( - second.calls.find((call) => call.method === "thread/resume")?.params - .dynamicTools, - ).toEqual([ - registerDeliverable, - readCurrentWakeComments, - requestHumanInput, - listChatAttachments, - reuseChatAttachment, - readChatAttachment, - ]); - const resumedQuestion = await second.invoke({ - id: "rpc-direct-question-resumed", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-direct-question-resumed", - tool: "request_human_input", - arguments: { interactionKind: "confirmation" }, - }, - }); - expect(resumedQuestion).toMatchObject({ success: true }); - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ - tool: "request_human_input", - callId: "call-direct-question-resumed", - arguments: { interactionKind: "confirmation" }, - }), - ); - await recovery?.session?.close({ reason: "test complete" }); - }); - - it("lets an answer claimed before expiry win the terminal-event race", async () => { - const transport = new FakeCodexTransport(); - let releaseResolution!: () => void; - transport.runtimeRequestResolver = () => new Promise((resolve) => { - releaseResolution = resolve; - }); - const session = await makeDriver([transport]).openSession({ - runId: "run-answer-handoff-race", - normalizedSessionId: "normalized-answer-handoff-race", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const iterator = session.events()[Symbol.asyncIterator](); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Ask before continuing." }, - }); - const providerRequest = transport.invoke({ - id: "race-input", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "race-input-item", - questions: [{ - id: "environment", - header: "Environment", - question: "Where should we deploy?", - options: [{ label: "Staging" }, { label: "Production" }], - }], - }, - }); - for (let count = 0; count < 20; count += 1) { - const event = await iterator.next(); - if (event.done || event.value.eventType === "runtime_request.created") break; - } - const resolution = session.resolveRuntimeRequest!({ - requestId: "race-input", - turnId, - resolution: { - action: "submit", - response: { - schema: "paperclip.question_response.v1", - answers: { environment: { selectedOptionIds: ["option-1"] } }, - }, - }, - }); - await Promise.resolve(); - const handoff = session.handoffRuntimeRequest!({ - requestId: "race-input", - turnId, - reason: "durable_handoff", - signal: new AbortController().signal, - }); - expect(handoff.result).toBe("already_settled"); - await expect(handoff.cleanup).resolves.toBeUndefined(); - releaseResolution(); - await resolution; - await providerRequest; - - let terminalEvent: PrpEvent | null = null; - for (let count = 0; count < 20; count += 1) { - const event = await iterator.next(); - if (event.done) break; - if ( - event.value.payload.requestId === "race-input" - && ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(event.value.eventType) - ) { - terminalEvent = event.value; - break; - } - } - expect(terminalEvent?.eventType).toBe("runtime_request.resolved"); - expect(session.pendingRuntimeRequests?.()).toEqual([]); - await session.close({ reason: "test complete" }); - }); - - it("redacts browser-visible request details and diagnostics from the fixture markers", async () => { - const fixture = await loadLiveConsoleConformanceFixture( - liveConsoleFixturePath, - ); - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-redaction", - normalizedSessionId: "normalized-redaction", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Request input." }, - }); - const pending = transport.invoke({ - id: "redacted-request", - method: "item/tool/requestUserInput", - params: { - threadId: "thread-1", - turnId, - itemId: "redacted-item", - reason: fixture.redactionMarkers.join(" "), - authorization: "Bearer browser-secret", - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - const serialized = JSON.stringify(session.pendingRuntimeRequests?.()); - for (const marker of fixture.redactionMarkers) - expect(serialized).not.toContain(marker); - expect(serialized).toContain("[REDACTED]"); - await session.resolveRuntimeRequest?.({ - requestId: "redacted-request", - turnId, - resolution: { action: "cancel" }, - }); - await pending; - await session.close({ reason: "fixture complete" }); - }); - - it("makes duplicate semantic completion idempotent and rejects changed payloads", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-result", - normalizedSessionId: "normalized-result", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - const request = { - id: 1, - method: "item/tool/call", - paperclipTrace: { - sourceEventId: "event_runner_000101", - sourceEventType: "semantic_tool.input", - }, - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-1", - tool: "paperclip_finish", - arguments: result, - }, - }; - expect(await transport.invoke(request)).toMatchObject({ success: true }); - const resultMapping = transport.traceInterpretations.find( - (entry) => - entry.sourceEventId === "event_runner_000101" && - entry.providerMethod === "item/tool/call", - ); - expect(resultMapping).toMatchObject({ - sourceEventType: "semantic_tool.input", - disposition: "mapped", - }); - expect(resultMapping?.emittedEventIds).toHaveLength(2); - expect(resultMapping?.emittedEventIds).toEqual( - expect.arrayContaining([ - expect.stringContaining(":run-result:"), - ]), - ); - expect( - await transport.invoke({ - ...request, - id: 2, - params: { - ...request.params, - callId: "call-2", - arguments: reverseObjectKeys(result), - }, - }), - ).toMatchObject({ success: true }); - expect((await session.snapshot()).semanticResult?.callId).toBe("call-1"); - const changed = structuredClone(result); - changed.summary = "Changed after commit."; - expect( - await transport.invoke({ - ...request, - id: 3, - params: { ...request.params, arguments: changed }, - }), - ).toMatchObject({ success: false }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.filter((event) => event.eventType === "turn.completed"), - ).toHaveLength(1); - }); - - it("accepts a normalized runnerd echo of a tool-committed semantic result", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-result-normalized-echo", - normalizedSessionId: "normalized-result-normalized-echo", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - const toolShaped = structuredClone(result) as unknown as Record; - toolShaped.verification = [{ - commandOrCheck: "read hello.txt", - status: "passed", - result: "hello", - }]; - delete toolShaped.attentionRequests; - expect( - await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: toolShaped, - }, - }), - ).toMatchObject({ success: true }); - transport.push("paperclip/runResult", { - threadId: "thread-1", - turnId: "turn-1", - itemId: "tool-result", - result: { - ...result, - verification: [{ - commandOrCheck: "read hello.txt", - status: "passed", - detail: "hello", - }], - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect(events.some((event) => event.eventType === "session.failed")).toBe( - false, - ); - }); - - it("advertises and dispatches run-authorized control-plane tools", async () => { - const transport = new FakeCodexTransport(); - const handler = vi.fn(async (call) => ({ - task: { id: "issue-1" }, - callId: call.callId, - })); - const session = await makeDriver([transport], { - dynamicTools: [ - { - name: "get_task_context", - description: "Read the assigned task.", - inputSchema: { type: "object", additionalProperties: false }, - }, - ], - dynamicToolHandler: handler, - }).openSession({ - runId: "run-tools", - normalizedSessionId: "normalized-tools", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ - message: { role: "user", text: "Inspect the task." }, - }); - - expect( - transport.calls.find((call) => call.method === "thread/start")?.params - .dynamicTools, - ).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: "get_task_context" }), - ]), - ); - const response = await transport.invoke({ - id: "rpc-tool", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-context", - tool: "get_task_context", - arguments: {}, - }, - }); - expect(response).toMatchObject({ success: true }); - expect( - JSON.parse( - String( - (response.contentItems as Array>)[0]?.text, - ), - ), - ).toEqual({ task: { id: "issue-1" }, callId: "call-context" }); - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ - tool: "get_task_context", - callId: "call-context", - arguments: {}, - }), - ); - }); - - it.each(["unknown", "validation"])("preserves the dynamic tool outcome boundary (%s)", async (outcome) => { - const transport = new FakeCodexTransport(); - const error = outcome === "unknown" - ? new SemanticToolOutcomeUnknownError("write may have committed") - : new Error("invalid instruction input"); - const handler = vi.fn(async () => { throw error; }); - const session = await makeDriver([transport], { - dynamicTools: [{ name: "update_agent_instructions", description: "Write instructions.", inputSchema: { type: "object" } }], - dynamicToolHandler: handler, - }).openSession({ runId: "run-uncertain-write", normalizedSessionId: "session-uncertain-write", workingDirectory: TEST_WORKING_DIRECTORY }); - await session.startTurn({ message: { role: "user", text: "Update instructions." } }); - const response = transport.invoke({ id: "rpc-write", method: "item/tool/call", params: { - threadId: "thread-1", turnId: "turn-1", callId: "uncertain-write", tool: "update_agent_instructions", arguments: {}, - } }); - if (outcome === "unknown") await expect(response).rejects.toBe(error); - else await expect(response).resolves.toMatchObject({ success: false }); - transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } }); - const events = await collectUntilTerminal(session.events()); - const completions = events.filter((event) => event.eventType === "item.completed" && event.itemId === "uncertain-write"); - expect(completions).toHaveLength(outcome === "unknown" ? 0 : 1); - expect(handler).toHaveBeenCalledTimes(1); - }); - - it("fails closed when an agent message changes a tool-committed result", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-tool-then-message-conflict", - normalizedSessionId: "normalized-tool-then-message-conflict", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - const changed = structuredClone(result); - changed.summary = "Conflicting agent-message result."; - transport.push("item/completed", { - threadId: "thread-1", - turnId: "turn-1", - item: { - id: "message-result", - type: "agentMessage", - text: JSON.stringify(changed), - }, - }); - - const events: PrpEvent[] = []; - for await (const event of session.events()) events.push(event); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.find((event) => event.eventType === "session.failed")?.payload, - ).toMatchObject({ - code: "conflicting_semantic_result", - recoverable: false, - }); - expect((await session.snapshot()).semanticResult?.result).toEqual(result); - }); - - it("rejects a tool result that changes an agent-message commitment", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-message-then-tool-conflict", - normalizedSessionId: "normalized-message-then-tool-conflict", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: "turn-1", - item: { - id: "message-result", - type: "agentMessage", - text: JSON.stringify(result), - }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - const changed = structuredClone(result); - changed.summary = "Conflicting tool result."; - expect( - await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: changed, - }, - }), - ).toMatchObject({ success: false }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect(events.some((event) => event.eventType === "session.failed")).toBe( - false, - ); - expect((await session.snapshot()).semanticResult).toMatchObject({ - callId: "message-result", - result, - }); - }); - - it("treats a canonically identical cross-channel result as a no-op", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-identical-cross-channel", - normalizedSessionId: "normalized-identical-cross-channel", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - transport.push("item/completed", { - threadId: "thread-1", - turnId: "turn-1", - item: { - id: "message-result", - type: "agentMessage", - text: JSON.stringify(reverseObjectKeys(result)), - }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect((await session.snapshot()).semanticResult?.callId).toBe( - "tool-result", - ); - }); - - it("fails closed when a live terminal embeds a changed semantic result", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-terminal-result-conflict", - normalizedSessionId: "normalized-terminal-result-conflict", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await transport.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - const changed = structuredClone(result); - changed.summary = "Conflicting terminal result."; - transport.push("turn/completed", { - threadId: "thread-1", - turn: { - id: "turn-1", - status: "completed", - items: [ - { - id: "terminal-result", - type: "agentMessage", - text: JSON.stringify(changed), - }, - ], - }, - }); - - const events: PrpEvent[] = []; - for await (const event of session.events()) events.push(event); - expect( - events.find((event) => event.eventType === "session.failed")?.payload, - ).toMatchObject({ - code: "conflicting_semantic_result", - recoverable: false, - }); - expect(events.some((event) => event.eventType === "turn.completed")).toBe( - false, - ); - expect((await session.snapshot()).semanticResult?.result).toEqual(result); - }); - - it.each([ - { threadId: "other-thread", turnId: "turn-1", label: "another thread" }, - { threadId: "thread-1", turnId: "other-turn", label: "another turn" }, - ])( - "rejects a semantic result for $label without committing it", - async ({ threadId, turnId }) => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-hostile-tool", - normalizedSessionId: "normalized-hostile-tool", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await transport.invoke({ - id: "hostile-call", - method: "item/tool/call", - params: { - threadId, - turnId, - callId: "hostile-call", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: false }); - const events = await collectUntilTerminal(session.events()); - expect( - events.some((event) => event.eventType === "run.result.proposed"), - ).toBe(false); - expect( - events.find((event) => event.eventType === "session.failed")?.payload, - ).toMatchObject({ code: "tool_binding_mismatch", recoverable: false }); - expect( - events.find((event) => event.eventType === "turn.failed")?.turnId, - ).toBe("turn-1"); - }, - ); - - it("rejects pre-turn, cross-thread, and post-terminal notifications", async () => { - const preTurnTransport = new FakeCodexTransport(); - const preTurn = await makeDriver([preTurnTransport]).openSession({ - runId: "run-pre-turn", - normalizedSessionId: "normalized-pre-turn", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - preTurnTransport.push("item/completed", { - threadId: "thread-1", - turnId: "turn-1", - item: { - id: "answer-pre", - type: "agentMessage", - text: JSON.stringify(result), - }, - }); - const preTurnEvents: PrpEvent[] = []; - for await (const event of preTurn.events()) preTurnEvents.push(event); - expect( - preTurnEvents.some((event) => event.eventType === "run.result.proposed"), - ).toBe(false); - expect( - preTurnEvents.find((event) => event.eventType === "session.failed") - ?.payload, - ).toMatchObject({ code: "turn_binding_mismatch" }); - - const crossThreadTransport = new FakeCodexTransport(); - const crossThread = await makeDriver([crossThreadTransport]).openSession({ - runId: "run-cross-thread", - normalizedSessionId: "normalized-cross-thread", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await crossThread.startTurn({ - message: { role: "user", text: "Complete." }, - }); - crossThreadTransport.push("item/completed", { - threadId: "other-thread", - turnId: "turn-1", - item: { - id: "answer-other", - type: "agentMessage", - text: JSON.stringify(result), - }, - }); - const crossThreadEvents = await collectUntilTerminal(crossThread.events()); - expect( - crossThreadEvents.some( - (event) => event.eventType === "run.result.proposed", - ), - ).toBe(false); - expect( - crossThreadEvents.find((event) => event.eventType === "session.failed") - ?.payload, - ).toMatchObject({ code: "thread_binding_mismatch" }); - - const postTerminalTransport = new FakeCodexTransport(); - const postTerminal = await makeDriver([postTerminalTransport]).openSession({ - runId: "run-post-terminal", - normalizedSessionId: "normalized-post-terminal", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await postTerminal.startTurn({ - message: { role: "user", text: "Complete." }, - }); - postTerminalTransport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - await collectUntilTerminal(postTerminal.events()); - expect( - await postTerminalTransport.invoke({ - id: "late-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "late-call", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: false }); - }); - - it("rejects duplicate and conflicting terminal facts", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-terminal-replay", - normalizedSessionId: "normalized-terminal-replay", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { - id: "turn-1", - status: "failed", - error: { message: "changed" }, - items: [], - }, - }); - const events: PrpEvent[] = []; - for await (const event of session.events()) events.push(event); - expect( - events.filter((event) => event.eventType === "turn.completed"), - ).toHaveLength(1); - expect( - events.filter((event) => event.eventType === "turn.failed"), - ).toHaveLength(0); - expect( - events.find((event) => event.eventType === "session.failed")?.payload, - ).toMatchObject({ code: "conflicting_turn_terminal", recoverable: false }); - }); - - it("rejects oversized semantic results without retaining provider payloads", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-large-result", - normalizedSessionId: "normalized-large-result", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await transport.invoke({ - id: "large-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "large-call", - tool: "paperclip_finish", - arguments: { ...result, summary: "x".repeat(70 * 1024) }, - }, - }), - ).toEqual({ - success: false, - contentItems: [ - { - type: "inputText", - text: "Semantic result exceeded the retained payload limit.", - }, - ], - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - const events = await collectUntilTerminal(session.events()); - expect( - events.some((event) => JSON.stringify(event).includes("x".repeat(1024))), - ).toBe(false); - expect( - events.some((event) => event.eventType === "run.result.proposed"), - ).toBe(false); - }); - - it("normalizes finish and block tools into one canonical result contract", async () => { - const transport = new FakeCodexTransport(); - const session = await makeDriver([transport]).openSession({ - runId: "run-tools", - normalizedSessionId: "normalized-tools", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await session.startTurn({ - message: { role: "user", text: "Complete or block." }, - }); - const blocked: PrpStructuredRunResult = { - ...structuredClone(result), - reportedWorkDisposition: "blocked", - summary: "Waiting on a fixture owner.", - completionClaim: { - ...structuredClone(result.completionClaim), - objectiveSatisfied: false, - criteria: [ - { criterionId: "file", status: "not_satisfied", evidenceRefs: [] }, - ], - remainingWork: [ - { - description: "Fixture owner must provide input.", - blocksCompletion: true, - }, - ], - }, - blocker: { - reasonCode: "fixture_input_missing", - owner: { kind: "external", name: "fixture owner" }, - unblockAction: "Provide the fixture input.", - scope: "task_wide", - }, - artifacts: [], - }; - const request = { - id: 10, - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-block", - tool: "paperclip_block", - arguments: blocked, - }, - }; - expect( - await transport.invoke({ - ...request, - id: 9, - params: { ...request.params, tool: "paperclip_finish" }, - }), - ).toMatchObject({ success: false }); - expect(await transport.invoke(request)).toMatchObject({ success: true }); - expect(await transport.invoke({ ...request, id: 11 })).toMatchObject({ - success: true, - }); - transport.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - const events = await collectUntilTerminal(session.events()); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.find((event) => event.eventType === "run.result.proposed") - ?.payload, - ).toMatchObject({ reportedWorkDisposition: "blocked" }); - }); - - it("resumes and reconciles the exact provider thread after transport loss", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "run-recover", - normalizedSessionId: "normalized-recover", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ message: { role: "user", text: "Work." } }); - const snapshot = await original.snapshot(); - await original.close({ reason: "transport lost" }); - const recovery = await driver.recoverSession?.(snapshot); - expect(recovery).toMatchObject({ recovered: true }); - expect(recovery?.session?.ids()).toEqual(original.ids()); - expect(await recovery?.session?.reconcile?.()).toMatchObject({ - thread: { id: "thread-1" }, - }); - expect(second.calls.map((call) => call.method)).toEqual([ - "initialize", - "thread/read", - "thread/turns/list", - "thread/resume", - "thread/goal/get", - "thread/read", - "thread/turns/list", - "thread/read", - "thread/turns/list", - ]); - expect((await recovery?.session?.snapshot())?.activeTurnId).toBe("turn-1"); - }); - - it("ignores an old terminal turn when the persisted active turn is still running", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - second.readResponse = { - thread: { - id: "thread-1", - sessionId: "provider-session-1", - cwd: TEST_WORKING_DIRECTORY, - turns: [ - { id: "turn-old", status: "completed", items: [] }, - { id: "turn-1", status: "inProgress", items: [] }, - ], - }, - }; - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "run-history", - normalizedSessionId: "normalized-history", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ - message: { role: "user", text: "Keep working." }, - }); - const snapshot = await original.snapshot(); - await original.close({ reason: "transport lost" }); - - const recovery = await driver.recoverSession?.(snapshot); - const recovered = recovery?.session; - expect(recovered).toBeDefined(); - await recovered!.reconcile?.(); - expect(await recovered!.snapshot()).toMatchObject({ - activeTurnId: "turn-1", - }); - await recovered!.close({ reason: "test complete" }); - const events: PrpEvent[] = []; - for await (const event of recovered!.events()) events.push(event); - expect(events.some((event) => event.eventType === "turn.completed")).toBe( - false, - ); - }); - - it("preserves proposal idempotency and rejects a changed result after transport loss", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "run-proposal-loss", - normalizedSessionId: "normalized-proposal-loss", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await first.invoke({ - id: "call-before-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-before-loss", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - const snapshot = await original.snapshot(); - expect(snapshot.semanticResult).toMatchObject({ - callId: "call-before-loss", - turnId: "turn-1", - result, - }); - await original.close({ reason: "transport lost after proposal" }); - const originalEvents: PrpEvent[] = []; - for await (const event of original.events()) originalEvents.push(event); - expect( - originalEvents.filter( - (event) => event.eventType === "run.result.proposed", - ), - ).toHaveLength(1); - - const recovery = await driver.recoverSession?.(snapshot); - const recovered = recovery?.session; - expect(recovered).toBeDefined(); - const collecting = collectUntilTerminal(recovered!.events()); - await recovered!.reconcile?.(); - expect( - await second.invoke({ - id: "call-after-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "call-after-loss", - tool: "paperclip_finish", - arguments: reverseObjectKeys(result), - }, - }), - ).toMatchObject({ success: true }); - const changed = structuredClone(result); - changed.summary = "Changed after transport loss."; - expect( - await second.invoke({ - id: "changed-after-loss", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "changed-after-loss", - tool: "paperclip_finish", - arguments: changed, - }, - }), - ).toMatchObject({ success: false }); - second.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - const events = await collecting; - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(0); - expect( - events.filter((event) => event.eventType === "turn.completed"), - ).toHaveLength(1); - }); - - it("rejects changed terminal result content discovered during recovery", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - const changed = structuredClone(result); - changed.summary = "Conflicting recovered terminal result."; - second.readResponse = { - thread: { - id: "thread-1", - sessionId: "provider-session-1", - cwd: TEST_WORKING_DIRECTORY, - turns: [ - { - id: "turn-1", - status: "completed", - items: [ - { - id: "terminal-result", - type: "agentMessage", - text: JSON.stringify(changed), - }, - ], - }, - ], - }, - }; - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "run-recovered-result-conflict", - normalizedSessionId: "normalized-recovered-result-conflict", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await first.invoke({ - id: "tool-result", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "tool-result", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - first.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - await collectUntilTerminal(original.events()); - const snapshot = await original.snapshot(); - expect(snapshot.terminalTurns).toHaveLength(1); - await original.close({ reason: "transport lost after terminal" }); - - const recovery = await driver.recoverSession?.(snapshot); - expect(recovery).toMatchObject({ recovered: true }); - await expect( - recovery!.session!.reconcile!(), - ).rejects.toMatchObject({ - name: "HarnessReconciliationError", - recoverable: true, - message: expect.stringContaining( - "contains a conflicting semantic result", - ), - }); - expect( - (await recovery!.session!.snapshot()).semanticResult?.result, - ).toEqual(result); - await recovery!.session!.close({ reason: "test complete" }); - }); - - it("does not re-emit an already observed terminal after recovery", async () => { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - second.readResponse = { - thread: { - id: "thread-1", - sessionId: "provider-session-1", - cwd: TEST_WORKING_DIRECTORY, - turns: [{ id: "turn-1", status: "completed", items: [] }], - }, - }; - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "run-terminal-loss", - normalizedSessionId: "normalized-terminal-loss", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ message: { role: "user", text: "Complete." } }); - expect( - await first.invoke({ - id: "terminal-call", - method: "item/tool/call", - params: { - threadId: "thread-1", - turnId: "turn-1", - callId: "terminal-call", - tool: "paperclip_finish", - arguments: result, - }, - }), - ).toMatchObject({ success: true }); - first.push("turn/completed", { - threadId: "thread-1", - turn: { id: "turn-1", status: "completed", items: [] }, - }); - await collectUntilTerminal(original.events()); - const snapshot = await original.snapshot(); - expect(snapshot.terminalTurns).toHaveLength(1); - await original.close({ reason: "transport lost after terminal" }); - - const recovery = await driver.recoverSession?.(snapshot); - const recovered = recovery?.session; - expect(recovered).toBeDefined(); - await recovered!.reconcile?.(); - await recovered!.close({ reason: "test complete" }); - const events: PrpEvent[] = []; - for await (const event of recovered!.events()) events.push(event); - expect(events.some((event) => event.eventType.startsWith("turn."))).toBe( - false, - ); - expect( - events.some((event) => event.eventType === "run.result.proposed"), - ).toBe(false); - }); - - it("fails recovery explicitly when the persisted active turn is missing or replaced", async () => { - for (const testCase of [ - { - label: "missing", - turns: [{ id: "turn-old", status: "completed", items: [] }], - message: "persisted active turn turn-1 is missing", - }, - { - label: "replaced", - turns: [{ id: "turn-2", status: "inProgress", items: [] }], - message: "active turn turn-2 instead of persisted active turn turn-1", - }, - ]) { - const first = new FakeCodexTransport(); - const second = new FakeCodexTransport(); - second.readResponse = { - thread: { - id: "thread-1", - sessionId: "provider-session-1", - cwd: TEST_WORKING_DIRECTORY, - turns: testCase.turns, - }, - }; - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: `run-${testCase.label}-turn`, - normalizedSessionId: `normalized-${testCase.label}-turn`, - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ message: { role: "user", text: "Work." } }); - const snapshot = await original.snapshot(); - await original.close({ reason: "transport lost" }); - const recovery = await driver.recoverSession?.(snapshot); - expect(recovery).toMatchObject({ - recovered: false, - reason: expect.stringContaining(testCase.message), - }); - expect(recovery?.session).toBeUndefined(); - } - }); - - it("reconciles a turn completed during transport loss without replacing either session identity", async () => { - const first = new FakeCodexTransport( - "driver-thread-loss", - "provider-session-loss", - ); - const second = new FakeCodexTransport( - "driver-thread-loss", - "provider-session-loss", - ); - second.readResponse = { - thread: { - id: "driver-thread-loss", - sessionId: "provider-session-loss", - cwd: TEST_WORKING_DIRECTORY, - tokenUsage: { total: { inputTokens: 21, outputTokens: 8 } }, - turns: [ - { - id: "turn-1", - status: "completed", - items: [ - { - id: "answer-loss", - type: "agentMessage", - text: JSON.stringify(result), - }, - ], - }, - ], - }, - }; - const driver = makeDriver([first, second]); - const original = await driver.openSession({ - runId: "runtime-run-loss", - normalizedSessionId: "controller-session-loss", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await original.startTurn({ - message: { role: "user", text: "Work through loss." }, - }); - const snapshot = await original.snapshot(); - await original.close({ reason: "transport lost before terminal delivery" }); - - const recovery = await driver.recoverSession?.(snapshot); - expect(recovery).toMatchObject({ recovered: true }); - const recovered = recovery?.session; - expect(recovered).toBeDefined(); - const collecting = collectUntilTerminal(recovered!.events()); - await recovered!.reconcile?.(); - const events = await collecting; - - expect(recovered!.ids()).toEqual({ - driverSessionId: "driver-thread-loss", - providerSessionId: "provider-session-loss", - displayId: "driver-thread-loss", - }); - expect(await recovered!.snapshot()).toMatchObject({ - runId: "runtime-run-loss", - normalizedSessionId: "controller-session-loss", - driverSessionId: "driver-thread-loss", - providerSessionId: "provider-session-loss", - }); - expect( - events.filter((event) => event.eventType === "turn.completed"), - ).toHaveLength(1); - expect( - events.filter((event) => event.eventType === "run.result.proposed"), - ).toHaveLength(1); - expect( - events.every( - (event, index) => - event.sourceSeq === snapshot.lastSourceSequence! + index + 1, - ), - ).toBe(true); - expect(await recovered!.usage?.()).toEqual({ - total: { inputTokens: 21, outputTokens: 8 }, - }); - }); - - it("degrades unsupported operations with explicit redacted diagnostics", async () => { - const transport = new FakeCodexTransport(); - transport.rejectMethods.set( - "turn/steer", - new Error("method not found Bearer super-secret api_key=also-secret"), - ); - const session = await makeDriver([transport]).openSession({ - runId: "run-degrade", - normalizedSessionId: "normalized-degrade", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - const { turnId } = await session.startTurn({ - message: { role: "user", text: "Start." }, - }); - await expect( - session.steer?.({ turnId, message: { role: "user", text: "Steer." } }), - ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); - const iterator = session.events()[Symbol.asyncIterator](); - const events: PrpEvent[] = []; - // PRP v2 emits capability and goal snapshots before turn events. - for (let index = 0; index < 8; index += 1) { - const next = await iterator.next(); - if (next.value) events.push(next.value); - } - const diagnostic = events.find( - (event) => event.eventType === "harness.diagnostic", - ); - expect(diagnostic?.payload).toMatchObject({ - code: "unsupported_operation", - operation: "steering", - }); - expect(JSON.stringify(diagnostic)).not.toContain("super-secret"); - expect(JSON.stringify(diagnostic)).not.toContain("also-secret"); - }); - - it("rejects proposals that do not match the exact task envelope", () => { - const wrongRevision = structuredClone(result); - wrongRevision.completionClaim.contractRevision = "codex-demo-v0"; - expect(validateCodexResultProposal(wrongRevision, envelope)).toMatchObject({ - status: "rejected", - issues: [{ code: "contract_revision_mismatch" }], - }); - - const unknownCriterion = structuredClone(result); - unknownCriterion.completionClaim.criteria = [ - { - criterionId: "not-in-envelope", - status: "satisfied", - evidenceRefs: [], - }, - ]; - const criterionDecision = validateCodexResultProposal( - unknownCriterion, - envelope, - ); - expect(criterionDecision).toMatchObject({ status: "rejected" }); - if (criterionDecision.status === "rejected") { - expect(criterionDecision.issues.map((issue) => issue.code)).toEqual( - expect.arrayContaining(["unknown_criterion", "missing_criterion"]), - ); - } - - const invalidDone = structuredClone(result); - invalidDone.completionClaim.objectiveSatisfied = false; - expect(validateCodexResultProposal(invalidDone, envelope)).toMatchObject({ - status: "rejected", - issues: [{ code: "invalid_disposition" }], - }); - }); - - it.each(["needs_review", "blocked"] as const)( - "keeps a completed runtime successful for an accepted %s advisory proposal", - async (disposition) => { - const proposal: PrpStructuredRunResult = - disposition === "needs_review" - ? { - ...structuredClone(result), - reportedWorkDisposition: "needs_review", - attentionRequests: [ - { kind: "review", summary: "Review the completed artifact." }, - ], - } - : { - ...structuredClone(result), - reportedWorkDisposition: "blocked", - summary: "Waiting on fixture input.", - completionClaim: { - ...structuredClone(result.completionClaim), - objectiveSatisfied: false, - criteria: [ - { - criterionId: "file", - status: "not_satisfied", - evidenceRefs: [], - }, - ], - remainingWork: [ - { - description: "Provide fixture input.", - blocksCompletion: true, - }, - ], - }, - blocker: { - reasonCode: "fixture_input_missing", - owner: { kind: "external", name: "fixture owner" }, - unblockAction: "Provide fixture input.", - scope: "task_wide", - }, - artifacts: [], - }; - const { trace } = await traceCompletedProposal(proposal); - expect(trace.resultDecision.status).toBe("accepted"); - expect( - trace.events.find((event) => event.eventType === "run.terminal") - ?.payload, - ).toMatchObject({ - turnTerminalState: "completed", - runTerminalState: "succeeded", - reportedWorkDisposition: disposition, - }); - }, - ); - - it("records rejected and missing proposals as recovery evidence instead of review status", async () => { - const wrongRevision = structuredClone(result); - wrongRevision.completionClaim.contractRevision = "wrong-revision"; - const rejected = await traceCompletedProposal(wrongRevision); - expect(rejected.trace.result).toBeNull(); - expect(rejected.trace.proposedResult).toMatchObject({ - reportedWorkDisposition: "done", - }); - expect( - rejected.trace.events.find( - (event) => event.eventType === "run.result.rejected", - )?.payload, - ).toMatchObject({ recovery: { required: true, recoverable: true } }); - expect( - rejected.trace.events.find((event) => event.eventType === "run.terminal") - ?.payload, - ).toMatchObject({ - runTerminalState: "failed", - reportedWorkDisposition: "yielded", - }); - - const missing = await traceCompletedProposal(null); - expect(missing.trace.resultDecision).toMatchObject({ status: "rejected" }); - expect( - missing.trace.events.some( - (event) => - event.eventType === "run.result.proposed" && - event.payload.reportedWorkDisposition === "needs_review", - ), - ).toBe(false); - }); - - it("preserves four independent stable identities under controller ownership", async () => { - const { trace } = await traceCompletedProposal(result, { - runId: "runtime-run-identity", - normalizedSessionId: "controller-session-identity", - }); - expect(trace.assertions.stableIdentity).toBe(true); - expect(trace.metadata.identity).toMatchObject({ - runId: "runtime-run-identity", - normalizedSessionId: "controller-session-identity", - driverSessionId: "thread-1", - providerSessionId: "provider-session-1", - }); - expect( - trace.events.every( - (event) => - event.runId === "runtime-run-identity" && - event.normalizedSessionId === "controller-session-identity", - ), - ).toBe(true); - }); - - it("validates persisted identity, uniqueness, terminals, and line bounds before replay", async () => { - const { trace } = await traceCompletedProposal(result, { - runId: "runtime-run-persisted", - normalizedSessionId: "controller-session-persisted", - }); - const serialize = (events: PrpEvent[]) => - `${events.map((event) => JSON.stringify(event)).join("\n")}\n`; - expect( - replayPersistedCodexEvents(serialize(trace.events), trace.metadata), - ).toEqual(trace.replaySnapshot); - - const mismatched = structuredClone(trace.events); - mismatched[0]!.normalizedSessionId = "another-session"; - expect(() => - replayPersistedCodexEvents(serialize(mismatched), trace.metadata), - ).toThrow("identity did not match"); - - const terminalIndex = trace.events.findIndex( - (event) => event.eventType === "run.terminal", - ); - const duplicated = trace.events.toSpliced( - terminalIndex, - 0, - structuredClone(trace.events[0]!), - ); - expect(() => - replayPersistedCodexEvents(serialize(duplicated), trace.metadata), - ).toThrow("event id was duplicated"); - - const terminal = structuredClone(trace.events[terminalIndex]!); - terminal.sourceEventId = `${terminal.sourceEventId}:conflict`; - terminal.sourceSeq += 1; - expect(() => - replayPersistedCodexEvents( - serialize([...trace.events, terminal]), - trace.metadata, - ), - ).toThrow("after the run terminal"); - expect(() => - replayPersistedCodexEvents( - `{\"payload\":\"${"x".repeat(300 * 1024)}\"}\n`, - trace.metadata, - ), - ).toThrow("line was empty or oversized"); - expect(() => - replayPersistedCodexEvents("{not-json}\n", trace.metadata), - ).toThrow("malformed JSON"); - }); - - it("degrades declared unsupported capabilities without Codex-specific core branches", async () => { - const capabilities = { - resume: false, - read: false, - steering: false, - interruption: false, - usage: false, - reconciliation: false, - dynamicTools: false, - }; - const { trace, transport } = await traceCompletedProposal(result, { - capabilities, - steer: "Do not call this unsupported path.", - interrupt: true, - }); - expect(trace.resultDecision.status).toBe("accepted"); - expect(trace.assertions.contextIsSkillless).toBe(true); - expect(trace.diagnostics).toEqual( - expect.arrayContaining([ - expect.stringContaining("steering is unavailable"), - expect.stringContaining("interruption is unavailable"), - ]), - ); - expect(transport.calls.map((call) => call.method)).not.toEqual( - expect.arrayContaining(["turn/steer", "turn/interrupt", "thread/read"]), - ); - expect( - transport.calls.find((call) => call.method === "thread/start")?.params - .dynamicTools, - ).toEqual([]); - - const directTransport = new FakeCodexTransport(); - const driver = makeDriver([directTransport], { capabilities }); - const session = await driver.openSession({ - runId: "run-no-capabilities", - normalizedSessionId: "normalized-no-capabilities", - workingDirectory: TEST_WORKING_DIRECTORY, - }); - await expect(session.read?.()).rejects.toBeInstanceOf( - HarnessCapabilityUnavailableError, - ); - await expect(session.usage?.()).rejects.toBeInstanceOf( - HarnessCapabilityUnavailableError, - ); - const snapshot = await session.snapshot(); - expect(await driver.recoverSession?.(snapshot)).toMatchObject({ - recovered: false, - reason: "resume capability is unavailable", - }); - }); -}); diff --git a/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts b/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts index 78330ddc3f8..8526e7c12db 100644 --- a/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts +++ b/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts @@ -3909,41 +3909,7 @@ describe("Daytona sandbox provider plugin", () => { expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1); }); - it("rejects a late execute after the release tombstone is set", async () => { - process.env.DAYTONA_API_KEY = "host-key"; - const sandbox = createMockSandbox({ id: "lease-a" }); - let stopStarted = false; - let resolveRelease!: () => void; - const releaseGate = new Promise((resolve) => { - resolveRelease = resolve; - }); - sandbox.stop.mockImplementation(() => { - stopStarted = true; - return releaseGate; - }); - mockGet.mockResolvedValue(sandbox); - await plugin.definition.onEnvironmentExecute?.(execParams("lease-a")); - const releasePromise = plugin.definition.onEnvironmentReleaseLease?.({ - driverKey: "daytona", - companyId: "company-1", - environmentId: "env-1", - providerLeaseId: "lease-a", - config: { timeoutMs: 300000, reuseLease: true }, - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(stopStarted).toBe(true); - - const overlappingExec = plugin.definition.onEnvironmentExecute?.(execParams("lease-a")); - await expect(overlappingExec).rejects.toThrow(/no longer active/); - - resolveRelease(); - await releasePromise; - - expect(mockGet).toHaveBeenCalledTimes(1); - expect(sandbox.stop).toHaveBeenCalledTimes(1); - expect(sandbox.process.executeCommand).toHaveBeenCalledTimes(1); - }); it("cancels active work before waiting for a stalled execute to drain", async () => { process.env.DAYTONA_API_KEY = "host-key"; diff --git a/packages/shared/src/slack-app-manifest.test.ts b/packages/shared/src/slack-app-manifest.test.ts index dc8b5721cff..780a0306deb 100644 --- a/packages/shared/src/slack-app-manifest.test.ts +++ b/packages/shared/src/slack-app-manifest.test.ts @@ -30,6 +30,12 @@ describe("Slack app manifest", () => { expect(manual.settings.event_subscriptions.request_url).toBe(input.webhookUrl); expect(manual.settings.interactivity).toEqual({ is_enabled: true, request_url: input.webhookUrl }); expect(manual.settings.socket_mode_enabled).toBe(false); + expect(manual.features).toHaveProperty("agent_view"); + expect(manual.features).not.toHaveProperty("assistant_view"); + expect(manual.features.bot_user).not.toHaveProperty("always_online"); + expect(manual.features.app_home).toEqual({ + home_tab_enabled: false, messages_tab_enabled: true, messages_tab_read_only_enabled: false, + }); // The reviewed provider permissions/events stay pinned across both creation paths. expect(manual.oauth_config.scopes.bot).toMatchSnapshot(); expect(manual.settings.event_subscriptions.bot_events).toMatchSnapshot(); diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index a7d9fad74fa..8b8b0f29641 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -1220,6 +1220,96 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(nextPrefix).toHaveBeenCalledOnce(); }); + // Keep repeated fixture reads at one query owner. These return the real query + // builder, preserving each caller's await, ordering, limits and assertions. + function readChatConversationsByEndpointId(value: (typeof chatConversations.$inferSelect)["endpointId"]) { + return db.select().from(chatConversations).where(eq(chatConversations.endpointId, value)); + } + + function readChatEndpointsById(value: (typeof chatEndpoints.$inferSelect)["id"]) { + return db.select().from(chatEndpoints).where(eq(chatEndpoints.id, value)); + } + + function readIssueCommentsByIssueId(value: (typeof issueComments.$inferSelect)["issueId"]) { + return db.select().from(issueComments).where(eq(issueComments.issueId, value)); + } + + function readChatDeliveriesByEndpointId(value: (typeof chatDeliveries.$inferSelect)["endpointId"]) { + return db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, value)); + } + + function readChatDeliveriesById(value: (typeof chatDeliveries.$inferSelect)["id"]) { + return db.select().from(chatDeliveries).where(eq(chatDeliveries.id, value)); + } + + function readHeartbeatRunsById(value: (typeof heartbeatRuns.$inferSelect)["id"]) { + return db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, value)); + } + + function readIssuesById(value: (typeof issues.$inferSelect)["id"]) { + return db.select().from(issues).where(eq(issues.id, value)); + } + + function readChatActionsById(value: (typeof chatActions.$inferSelect)["id"]) { + return db.select().from(chatActions).where(eq(chatActions.id, value)); + } + + function readChatGitHubReviewsByEndpointId(value: (typeof chatGitHubReviews.$inferSelect)["endpointId"]) { + return db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, value)); + } + + function readChatPublicationsByEndpointId(value: (typeof chatPublications.$inferSelect)["endpointId"]) { + return db.select().from(chatPublications).where(eq(chatPublications.endpointId, value)); + } + + function readChatPublicationsById(value: (typeof chatPublications.$inferSelect)["id"]) { + return db.select().from(chatPublications).where(eq(chatPublications.id, value)); + } + + function readChatGitHubRegistrationsByEndpointId(value: (typeof chatGitHubRegistrations.$inferSelect)["endpointId"]) { + return db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, value)); + } + + function readToolConnectionsById(value: (typeof toolConnections.$inferSelect)["id"]) { + return db.select().from(toolConnections).where(eq(toolConnections.id, value)); + } + + function readChatConversationsById(value: (typeof chatConversations.$inferSelect)["id"]) { + return db.select().from(chatConversations).where(eq(chatConversations.id, value)); + } + + function readIssuesByCompanyId(value: (typeof issues.$inferSelect)["companyId"]) { + return db.select().from(issues).where(eq(issues.companyId, value)); + } + + function readChatPublicationsByCommentId(value: (typeof chatPublications.$inferSelect)["commentId"]) { + return db.select().from(chatPublications).where(eq(chatPublications.commentId, value)); + } + + function readChatActionsByProviderActionId(value: (typeof chatActions.$inferSelect)["providerActionId"]) { + return db.select().from(chatActions).where(eq(chatActions.providerActionId, value)); + } + + function readChatPublicationsByIdempotencyKey(value: (typeof chatPublications.$inferSelect)["idempotencyKey"]) { + return db.select().from(chatPublications).where(eq(chatPublications.idempotencyKey, value)); + } + + function readHeartbeatRunsByCompanyId(value: (typeof heartbeatRuns.$inferSelect)["companyId"]) { + return db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, value)); + } + + function readAgentWakeupRequestsById(value: (typeof agentWakeupRequests.$inferSelect)["id"]) { + return db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, value)); + } + + function readIssueCommentsByCompanyId(value: (typeof issueComments.$inferSelect)["companyId"]) { + return db.select().from(issueComments).where(eq(issueComments.companyId, value)); + } + + function readChatTeamsFileTransfersById(value: (typeof chatTeamsFileTransfers.$inferSelect)["id"]) { + return db.select().from(chatTeamsFileTransfers).where(eq(chatTeamsFileTransfers.id, value)); + } + // A truthy return is not a durable scheduler receipt. These transport tests // record the same exact receipt identity; real scheduling/coalescing is // separately exercised by durable-chat-wakeup.test.ts against heartbeat. @@ -1584,10 +1674,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const endpoint = await service.get(endpointId); let conversation: typeof chatConversations.$inferSelect | undefined; await vi.waitFor(async () => { - conversation = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpointId)) + conversation = await readChatConversationsByEndpointId(endpointId) .then((rows) => rows.at(-1)); expect(conversation).toBeDefined(); }); @@ -2191,7 +2278,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "owner-user", ); if (!useVerifiedAppId) { - const [configured] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, endpoint.id)); + const [configured] = await readChatEndpointsById(endpoint.id); // Legacy transport fixtures already have explicit tool authority. Do not // save a review configuration: that would switch their admission model. await db.transaction((tx) => syncGitHubBotTools(tx, configured!, "owner-user", true)); @@ -2304,7 +2391,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const thread = makeThread({ channelId: "paperclipai/paperclip", id: threadId }).thread; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread, trigger: "mention", message: makeMessage({ id: "41901", text: `@${f.endpoint.botUsername} start this thread`, userId: "42", userName: "octocat", mentioned: true }) }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); return { ...f, thread, conversation }; } async function addPeerGitHubBot(f: Awaited>) { @@ -2336,8 +2423,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const thread = makeThread({ channelId: "paperclipai/paperclip", id: "github:paperclipai/paperclip:issue:419" }).thread; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread, trigger: "mention", message: makeMessage({ id: "41901", text: `@${f.endpoint.botUsername} ${ref(injected)}`, userId: "42", userName: "octocat", mentioned: true }) }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); - const [comment] = await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId!)); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); + const [comment] = await readIssueCommentsByIssueId(conversation.issueId!); const args = { db, companyId: f.companyId, issueId: conversation.issueId!, commentIds: [comment.id] }; expect(await resolveRunScopedMentionedSkillKeys(args)).toEqual([common.key, mention.key]); expect(comment.body).toContain(ref(common)); @@ -2352,7 +2439,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } }, "owner-user"); await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread, trigger: "subscribed_message", message: makeMessage({ id: "41902", text: ref(injected), userId: "42", userName: "octocat" }) }); - const all = await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId!)).orderBy(asc(issueComments.createdAt)); + const all = await readIssueCommentsByIssueId(conversation.issueId!).orderBy(asc(issueComments.createdAt)); expect(all).toHaveLength(2); expect(await resolveRunScopedMentionedSkillKeys({ ...args, commentIds: [all[1].id] })).toEqual([next.key]); // A later event must not change the snapshot for a queued older wake. @@ -2376,7 +2463,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(rows[0]).toMatchObject({ state: "filtered", principalId: null, redactedError: "GitHub message explicitly mentions a different connected bot" }); expect(rows[0].normalizedEvent).toHaveProperty("filtering.contentRetained", false); expect(JSON.stringify(rows[0].normalizedEvent)).not.toContain("tell me a joke"); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, f.conversation.issueId!))).toHaveLength(1); + expect(await readIssueCommentsByIssueId(f.conversation.issueId!)).toHaveLength(1); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount); expect(f.runtime.endpoints.get(f.endpoint.id)?.reactions).toEqual([]); expect(f.responseWrites).toHaveLength(1); @@ -2391,7 +2478,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const wakeCount = f.wakeup.mock.calls.length; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread: f.thread, trigger: "subscribed_message", message: makeMessage({ id: "41902", text, userId: "42", userName: "octocat" }) }); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, f.conversation.issueId!))).toHaveLength(2); + expect(await readIssueCommentsByIssueId(f.conversation.issueId!)).toHaveLength(2); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount + 1); }, ); @@ -2401,7 +2488,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const wakeCount = f.wakeup.mock.calls.length; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread: f.thread, trigger: "mention", message: makeMessage({ id: "41902", text: `@gonzo @${f.endpoint.botUsername} both help`, userId: "42", userName: "octocat", mentioned: true }) }); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, f.conversation.issueId!))).toHaveLength(2); + expect(await readIssueCommentsByIssueId(f.conversation.issueId!)).toHaveLength(2); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount + 1); }); it.each(["other_company", "other_provider", "archived"])("does not route GitHub mentions using %s identities", async kind => { @@ -2412,13 +2499,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const wakeCount = f.wakeup.mock.calls.length; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread: f.thread, trigger: "subscribed_message", message: makeMessage({ id: "41902", text: "@gonzo help", userId: "42", userName: "octocat" }) }); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, f.conversation.issueId!))).toHaveLength(2); + expect(await readIssueCommentsByIssueId(f.conversation.issueId!)).toHaveLength(2); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount + 1); }); it("rechecks another GitHub bot's mention when recovering a durable subscribed delivery", async () => { const f = await subscribedGitHubFixture(); await addPeerGitHubBot(f); - const [root] = await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpoint.id)); + const [root] = await readChatDeliveriesByEndpointId(f.endpoint.id); const providerEventId = `${f.thread.id}:41902`; const [pending] = await db.insert(chatDeliveries).values({ companyId: f.companyId, endpointId: f.endpoint.id, providerEventId, deduplicationKey: randomUUID(), eventKind: "message", state: "received", @@ -2428,18 +2515,18 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }).returning(); const wakeCount = f.wakeup.mock.calls.length; await f.service.processPendingDeliveries(1, pending.id); - const [recovered] = await db.select().from(chatDeliveries).where(eq(chatDeliveries.id, pending.id)); + const [recovered] = await readChatDeliveriesById(pending.id); expect(recovered.state).toBe("filtered"); expect(recovered.normalizedEvent).toHaveProperty("filtering.contentRetained", false); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, f.conversation.issueId!))).toHaveLength(1); + expect(await readIssueCommentsByIssueId(f.conversation.issueId!)).toHaveLength(1); }); async function toolReplyFixture(threadId = "github:paperclipai/paperclip:issue:418") { const f = await reviewBotFixture(); const thread = makeThread({ channelId: "paperclipai/paperclip", id: threadId }).thread; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread, trigger: "mention", message: makeMessage({ id: "41801", text: "Are you there?", userId: "42", userName: "octocat", mentioned: true }) }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); const [run] = await db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.assignedAgentId, status: "running", runtimeMode: "native", contextSnapshot: await chatWakeContext({ endpointId: f.endpoint.id, issueId: conversation.issueId, provider: "github", providerMessageId: "41801", @@ -2504,9 +2591,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await githubChatReviewService(db, f.providerFetch).execute({ companyId: f.companyId, agentId: f.assignedAgentId, issueId: job.issueId!, runId: run.id }, "comment", { body: "The border styling is fixed.", idempotencyKey: "fast-response-final" }); expect(f.responseWrites).toEqual([expect.objectContaining({ body: expect.stringContaining("The border styling is fixed.") })]); - expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id)))[0].status).toBe("running"); + expect((await readHeartbeatRunsById(run.id))[0].status).toBe("running"); } - expect((await db.select().from(issues).where(eq(issues.id, job.issueId!)))[0].status).not.toBe("done"); + expect((await readIssuesById(job.issueId!))[0].status).not.toBe("done"); }); it("acknowledges a GitHub request after a durable scheduler retry succeeds", async () => { const f = await reviewBotFixture(); @@ -2519,7 +2606,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(f.responseWrites).toEqual([]); await db.update(chatActions).set({ result: { ...action.result, retryAt: new Date(0).toISOString() } }).where(eq(chatActions.id, action.id)); await f.service.processPendingDeliveries(1, action.deliveryId!); - const [accepted] = await db.select().from(chatActions).where(eq(chatActions.id, action.id)); + const [accepted] = await readChatActionsById(action.id); expect(accepted.status).toBe("processed"); expect(f.responseWrites.map(write => write.method)).toEqual(["POST"]); expect([...f.responseComments.values()][0].body).toContain("Working on this"); @@ -2621,7 +2708,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(new Set(f.responseWrites.map(write => write.id)).size).toBe(1); expect(f.responseComments.size).toBe(1); await expect(githubRunReplyState(db, { ...f.session, endpointId: f.endpoint.id })).resolves.toBe("confirmed"); - expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.run.id)))[0].status).toBe("running"); + expect((await readHeartbeatRunsById(f.run.id))[0].status).toBe("running"); const authorizationReason = await resolveChatRunPresentationAuthorizationReason(db, f.session); expect(authorizationReason).toBe("internal_agent_write"); await db.update(heartbeatRuns).set({ status: "succeeded", resultJson: { presentationDecision: { authorizationReason } } }).where(eq(heartbeatRuns.id, f.run.id)); @@ -2783,7 +2870,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(publication.result?.replyWriteStarted).toBe(true); await expect(githubRunReplyState(db, { ...f.session, endpointId: f.endpoint.id })).resolves.toBe(phase === "after_summary" ? "confirmed" : "unsettled"); if (phase === "after_summary") { - const [review] = await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id)); + const [review] = await readChatGitHubReviewsByEndpointId(f.endpoint.id); expect(review.publicationReceipts.summary).toMatchObject({ id: String([...f.responseComments.keys()][0]) }); } }); @@ -2835,20 +2922,20 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const [lease] = await db.insert(chatEndpointLeases).values({ companyId: f.companyId, endpointId: f.endpoint.id, leaseKey: "credentials", token: randomUUID(), expiresAt: new Date(Date.now() + 60_000) }).returning(); const processing = f.service.processPendingPublications(); - await expect.poll(async () => (await db.select().from(chatPublications).where(eq(chatPublications.endpointId, f.endpoint.id)))[0]?.state).toBe("streaming"); + await expect.poll(async () => (await readChatPublicationsByEndpointId(f.endpoint.id))[0]?.state).toBe("streaming"); const [action] = await db.insert(chatActions).values({ companyId: f.companyId, endpointId: f.endpoint.id, conversationId: f.conversation.id, kind: "github_review_publication", providerActionId: `github_publication:${randomUUID()}`, payload: { operation: "comment", session: f.session }, status: "processing" }).returning(); await db.delete(chatEndpointLeases).where(eq(chatEndpointLeases.id, lease.id)); await processing; expect(f.runtime.endpoints.get(f.endpoint.id)?.posts).toEqual([]); - const [fallback] = await db.select().from(chatPublications).where(eq(chatPublications.endpointId, f.endpoint.id)); + const [fallback] = await readChatPublicationsByEndpointId(f.endpoint.id); expect(fallback).toMatchObject({ state: "cancelled", attempts: 1 }); await db.update(chatActions).set({ status: "processed", result: { id: "41802", url: "https://github.com/test/41802" } }).where(eq(chatActions.id, action.id)); await db.update(chatPublications).set({ nextAttemptAt: null }).where(eq(chatPublications.id, fallback.id)); await f.service.processPendingPublications(); expect(f.runtime.endpoints.get(f.endpoint.id)?.posts).toEqual([]); - expect((await db.select().from(chatPublications).where(eq(chatPublications.id, fallback.id)))[0].state).toBe("cancelled"); + expect((await readChatPublicationsById(fallback.id))[0].state).toBe("cancelled"); }); it.each(["personal", "organization"] as const)( "wizard persists a %s owner and resumes the same bound registration after a lost Cloud response and enrolled origin change", @@ -2911,10 +2998,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { browserOrigin = "http://localhost:3104"; const resumed = await wizard.start(bot.id, "owner-user", input); expect(resumed.state).toBe("create"); - const sessions = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, bot.id)); + const sessions = await readChatGitHubRegistrationsByEndpointId(bot.id); expect(sessions).toHaveLength(1); expect(sessions[0].ownerType).toBe(ownerType); expect(cloudRecord).toMatchObject({ @@ -2982,7 +3066,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect(wizard.restartRegistration(bot.id, "owner-user", scenario === "wrong-registration" ? randomUUID() : prior.id)) .rejects.toThrow("cannot be restarted"); expect(githubApp).not.toHaveBeenCalled(); - expect(await db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, bot.id))).toHaveLength(1); + expect(await readChatGitHubRegistrationsByEndpointId(bot.id)).toHaveLength(1); return; } expect(await wizard.advance(bot.id, "owner-user")).toMatchObject({ state: "recovery", restartableRegistrationId: prior.id }); @@ -2991,7 +3075,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const result = await wizard.restartRegistration(bot.id, "owner-user", prior.id); expect(result).toMatchObject({ endpointId: bot.id, state: "create", registration: { manifest: { name: "My Reviewer", public: false } } }); expect(result.registration!.registrationUrl).toContain("/organizations/acme/settings/apps/new"); - const sessions = await db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, bot.id)); + const sessions = await readChatGitHubRegistrationsByEndpointId(bot.id); expect(sessions).toHaveLength(2); expect(sessions.find(s => s.id === prior.id)).toMatchObject({ status: "failed", consumedAt: expect.any(Date) }); const next = sessions.find(s => s.id !== prior.id)!; @@ -3002,7 +3086,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(unchanged.connectionId).toBe(bot.connectionId); expect(unchanged.assignedAgentId).toBe(bot.assignedAgentId); expect((await wizard.restartRegistration(bot.id, "owner-user", prior.id)).registration).toEqual(result.registration); - expect(await db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, bot.id))).toHaveLength(2); + expect(await readChatGitHubRegistrationsByEndpointId(bot.id)).toHaveLength(2); }, ); it("stack exchanges a callback code only once when vault storage is interrupted", async () => { @@ -3086,19 +3170,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(exchange).toHaveBeenCalledTimes(1); expect(storeApp).toHaveBeenCalledTimes(1); expect(claimGitHubApp).toHaveBeenCalledTimes(1); - const [session] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, bot.id)); + const [session] = await readChatGitHubRegistrationsByEndpointId(bot.id); expect(session.status).toBe("failed"); }); it("wizard retains partial manifest recovery until explicit existing-App recovery succeeds", async () => { const f = await reviewBotFixture(); const returnState = randomUUID(); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, f.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(f.endpoint.connectionId); expect(connection.credentialSecretRefs.map((ref) => ref.configPath)).toEqual( expect.arrayContaining([ "credentials.appId", @@ -3162,10 +3240,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); expect(claimGitHubApp).not.toHaveBeenCalled(); expect(finish).not.toHaveBeenCalled(); - const [session] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, f.endpoint.id)); + const [session] = await readChatGitHubRegistrationsByEndpointId(f.endpoint.id); expect(session.status).toBe("failed"); const resourcesBefore = await f.service.listResources(f.endpoint.id); await f.service.storeGitHubApp(f.endpoint.id, "owner-user", { @@ -3261,10 +3336,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect( await wizard.cloudCallback("bound-state", "origin-recovery", "claim"), ).toContain("https://new.example.test/"); - const [session] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, bot.id)); + const [session] = await readChatGitHubRegistrationsByEndpointId(bot.id); expect(session!.trustedOrigin).toBe(enrolledOrigin); expect(session!.handoff?.cloudId).toBe("origin-recovery"); }); @@ -3303,10 +3375,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { appName: "My Reviewer", }); expect( - await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, bot.id)), + await readChatGitHubRegistrationsByEndpointId(bot.id), ).toHaveLength(0); }); it("wizard repairs Cloud after manual credential recovery and secret rotation without changing App identity", async () => { @@ -3389,10 +3458,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(current.setup.github?.cloudRegistrationId).toBe(cloudId); expect(current.setup.webhookVerifiedAt).toBeNull(); expect(await context.service.listResources(bot.id)).toEqual(before); - const [session] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, bot.id)); + const [session] = await readChatGitHubRegistrationsByEndpointId(bot.id); expect(session.status).toBe("completed"); expect(session.handoff?.webhookSecretHash).toMatch(/^[a-f0-9]{64}$/); await context.service.generateSetupSecret(bot.id, "owner-user"); @@ -3729,10 +3795,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const connectedAppId = (await context.service.get(context.endpoint.id)) .botExternalId; const vault = secretService(db); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, context.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(context.endpoint.connectionId); const refs = [...connection!.credentialSecretRefs]; for (const key of ["clientId", "clientSecret"]) { const secret = await vault.create(f.companyId, { @@ -3885,10 +3948,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).rejects.toThrow("in progress"); callbackRelease(); await callback; - const [session] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, context.endpoint.id)); + const [session] = await readChatGitHubRegistrationsByEndpointId(context.endpoint.id); expect(session!.handoff).toMatchObject({ returnState: "preserve-return", redemptionId: "preserve-receipt", @@ -3904,7 +3964,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await configuredGitHubEndpoint(f, {}, true, method === "manifest", method === "existing"); const current = await context.service.get(context.endpoint.id); expect(current.setup.github?.initialRepositoryImportPending).toBe(true); - const registrations = await db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, current.id)); + const registrations = await readChatGitHubRegistrationsByEndpointId(current.id); expect(registrations).toHaveLength(method === "manifest" ? 1 : 0); await db.update(chatGitHubRegistrations).set({ status: "completed" }) .where(eq(chatGitHubRegistrations.endpointId, current.id)); @@ -4037,7 +4097,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, }, }; - const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, f.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(f.endpoint.connectionId); await db.update(toolConnections).set({credentialSecretRefs: connection.credentialSecretRefs.filter(ref => ref.configPath !== "credentials.webhookSecret")}).where(eq(toolConnections.id, connection.id)); await expect(dispatchGitHubBotCloudEvent(db, event)).rejects.toThrow("credentials are not ready"); await db.update(toolConnections).set({credentialSecretRefs: connection.credentialSecretRefs}).where(eq(toolConnections.id, connection.id)); @@ -4098,10 +4158,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect( moved.service.completeGitHubRegistration(state, "code"), ).rejects.toThrow("expired or was already used"); - const [pending] = await db - .select() - .from(chatGitHubRegistrations) - .where(eq(chatGitHubRegistrations.endpointId, endpoint.id)); + const [pending] = await readChatGitHubRegistrationsByEndpointId(endpoint.id); expect(pending.status).toBe("pending"); await db .update(chatGitHubRegistrations) @@ -4424,14 +4481,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ); expect(link).toBeDefined(); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, link.conversationId)); - const [task] = await db - .select() - .from(issues) - .where(eq(issues.id, conversation.issueId)); + const [conversation] = await readChatConversationsById(link.conversationId); + const [task] = await readIssuesById(conversation.issueId); expect(task.sourceTrust?.preset).toBe("low_trust_review"); expect(task.executionPolicy?.authorizationPolicy).toMatchObject({ trustBoundary: { @@ -4494,15 +4545,15 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const send = () => f.service.handleWebhook(f.endpoint.publicId, "github", signedGitHubWebhookRequest({ event, delivery, payload, webhookSecret: f.webhookSecret })); expect((await send()).status).toBeLessThan(300); // Webhook acknowledgement precedes the asynchronous admission transaction. - await expect.poll(async () => (await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).length, { timeout: 10_000 }).toBe(1); + await expect.poll(async () => (await readChatConversationsByEndpointId(f.endpoint.id)).length, { timeout: 10_000 }).toBe(1); await send(); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); expect(conversation.externalThreadId).toBe(`github:PaperclipAI/Paperclip:${event === "issues" || event === "issue_comment" ? "issue:" : ""}83${event === "pull_request_review_comment" ? ":rc:83000" : ""}`); expect(conversation.providerUrl).not.toContain("mention-event"); if (event === "issues") expect(conversation.providerUrl).toBe("https://github.com/paperclipai/paperclip/issues/83"); - const [task] = await db.select().from(issues).where(eq(issues.id, conversation.issueId!)); + const [task] = await readIssuesById(conversation.issueId!); expect(task).toMatchObject({ assigneeAgentId: f.assignedAgentId, responsibleUserId: "owner-user" }); - const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, task.id)); + const comments = await readIssueCommentsByIssueId(task.id); expect(comments).toHaveLength(1); expect(comments[0].body).toContain("tell me a joke"); expect(comments[0].body).toContain("You were mentioned on GitHub. Your task is to respond to the authorized person (octocat)"); @@ -4516,7 +4567,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(request.normalizedEvent).not.toHaveProperty("githubAutomatic"); expect(request.normalizedEvent).not.toHaveProperty("githubIssue"); if (event.includes("comment")) expect(request.normalizedEvent).toHaveProperty("message.sourceProviderMessageId", "83001"); - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(0); expect(await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "github_review_check")))).toHaveLength(0); expect((await f.management.configuration(f.endpoint.id, "owner-user")).configuration.defaults.invocation).toBe("mentions_only"); }, @@ -4549,19 +4600,19 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }; const send = (content = payload) => f.service.handleWebhook(f.endpoint.publicId, "github", signedGitHubWebhookRequest({ event: "issues", delivery: randomUUID(), payload: content, webhookSecret: f.webhookSecret })); await send(); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatConversationsByEndpointId(f.endpoint.id)).toHaveLength(0); expect(f.wakeup).not.toHaveBeenCalled(); const allowed = { ...payload, sender: { id: 42, login: "octocat", type: "User" }, issue: { ...payload.issue, user: payload.sender } }; await send(allowed); - await expect.poll(async () => (await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).length).toBe(1); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); - const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId!)); + await expect.poll(async () => (await readChatConversationsByEndpointId(f.endpoint.id)).length).toBe(1); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); + const comments = await readIssueCommentsByIssueId(conversation.issueId!); expect(comments).toHaveLength(1); await send({ ...allowed, changes: { body: { from: body } } }); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId!))).toHaveLength(1); + expect(await readIssueCommentsByIssueId(conversation.issueId!)).toHaveLength(1); await db.update(chatIdentityLinks).set({ status: "revoked" }).where(eq(chatIdentityLinks.principalId, f.principal.id)); await send({ ...allowed, issue: { ...allowed.issue, number: 85 } }); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).toHaveLength(1); + expect(await readChatConversationsByEndpointId(f.endpoint.id)).toHaveLength(1); expect(await f.service.listActivity(f.endpoint.id)).toEqual(expect.arrayContaining([expect.objectContaining({ status: "filtered" })])); }); it.each(["issue_comment", "pull_request_review_comment"])("admits a newly mentioned %s edit while preserving its lifecycle record", async (event) => { @@ -4575,8 +4626,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { changes: { body: { from: "No mention" } }, }; await f.service.handleWebhook(f.endpoint.publicId, "github", signedGitHubWebhookRequest({ event, delivery: randomUUID(), payload, webhookSecret: f.webhookSecret })); - await expect.poll(async () => (await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).length).toBe(1); - const deliveries = await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpoint.id)); + await expect.poll(async () => (await readChatConversationsByEndpointId(f.endpoint.id)).length).toBe(1); + const deliveries = await readChatDeliveriesByEndpointId(f.endpoint.id); expect(deliveries).toEqual(expect.arrayContaining([ expect.objectContaining({ eventKind: "message_updated" }), expect.objectContaining({ eventKind: "mention", normalizedEvent: expect.objectContaining({ githubManual: expect.objectContaining({ event: "mention" }) }) }), @@ -4604,12 +4655,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const activity = await f.service.listActivity(f.endpoint.id); expect(activity.filter(row => row.status === "filtered")).toHaveLength(1); expect(activity).toEqual(expect.arrayContaining([expect.objectContaining({ detail: expect.stringContaining("runs only when mentioned") })])); - const [ignored] = await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, f.endpoint.id)); + const [ignored] = await readChatDeliveriesByEndpointId(f.endpoint.id); expect(JSON.stringify(ignored.normalizedEvent)).not.toContain("Private"); expect(ignored.normalizedEvent).toHaveProperty("filtering.contentRetained", false); await db.update(chatEndpointResources).set({ enabled: false }).where(eq(chatEndpointResources.endpointId, f.endpoint.id)); await send({ ...payload, issue: { ...payload.issue, body: `@${f.endpoint.botUsername!} help` } }, randomUUID()); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatConversationsByEndpointId(f.endpoint.id)).toHaveLength(0); expect(f.wakeup).not.toHaveBeenCalled(); }); it("admits opt-in signed issue events without PR checks, deduplicates them and rechecks revoked policy", async () => { @@ -4630,7 +4681,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { event: "issues", delivery, payload: content, webhookSecret: f.webhookSecret, })); expect((await send()).status).toBeLessThan(300); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatConversationsByEndpointId(f.endpoint.id)).toHaveLength(0); const saved = await f.management.configuration(f.endpoint.id, "owner-user"); await f.management.saveConfiguration(f.endpoint.id, { expectedRevision: saved.revision, @@ -4638,20 +4689,20 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, "owner-user"); const delivery = randomUUID(); expect((await send(delivery)).status).toBeLessThan(300); - await expect.poll(async () => (await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).length, { timeout: 10000 }).toBe(1); + await expect.poll(async () => (await readChatConversationsByEndpointId(f.endpoint.id)).length, { timeout: 10000 }).toBe(1); await send(delivery); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); - const [task] = await db.select().from(issues).where(eq(issues.id, conversation.issueId!)); + const [conversation] = await readChatConversationsByEndpointId(f.endpoint.id); + const [task] = await readIssuesById(conversation.issueId!); expect(task).toMatchObject({ title: "GitHub issue #83: Say hello", assigneeAgentId: f.assignedAgentId, responsibleUserId: "owner-user", originKind: "chat_channel" }); expect(conversation.providerUrl).toBe("https://github.com/paperclipai/paperclip/issues/83"); - const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, task.id)); + const comments = await readIssueCommentsByIssueId(task.id); expect(comments).toHaveLength(1); expect(comments[0].body).toContain(instruction); expect(comments[0].body).toContain("Read the SKILL.md"); expect(await resolveRunScopedMentionedSkillKeys({ db, companyId: f.companyId, issueId: task.id, commentIds: [comments[0].id] })).toEqual([skill.key]); expect(comments[0].body).toContain("Reply with an ASCII animal."); expect(comments[0].body).toContain("Untrusted GitHub issue context"); - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(0); expect(await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "github_review_check")))).toHaveLength(0); const [run] = await db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.assignedAgentId, invocationSource: "assignment", status: "running", @@ -4682,8 +4733,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { issue: { number: 84, title: "No authority", body: "", user: { id: 999, login: "unknown", type: "User" }, labels: [] }, }, }))).status).toBeLessThan(300); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id))).toHaveLength(0); - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatConversationsByEndpointId(f.endpoint.id)).toHaveLength(0); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(0); }); it("admits signed PR events into an ordinary assigned task and deduplicates delivery", async () => { const f = await reviewBotFixture(); @@ -4735,24 +4786,15 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .poll( async () => ( - await db - .select() - .from(chatGitHubReviews) - .where(eq(chatGitHubReviews.endpointId, f.endpoint.id)) + await readChatGitHubReviewsByEndpointId(f.endpoint.id) ).length, { timeout: 10000 }, ) .toBe(1); await send(); - const reviews = await db - .select() - .from(chatGitHubReviews) - .where(eq(chatGitHubReviews.endpointId, f.endpoint.id)); + const reviews = await readChatGitHubReviewsByEndpointId(f.endpoint.id); expect(reviews).toHaveLength(1); - const [task] = await db - .select() - .from(issues) - .where(eq(issues.id, reviews[0].issueId)); + const [task] = await readIssuesById(reviews[0].issueId); expect(task).toMatchObject({ title: "PR #81: Check review routing", assigneeAgentId: f.assignedAgentId, @@ -4764,10 +4806,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(reviews[0].event.sender.id).toBe("77"); expect(reviews[0].event.author.id).toBe("42"); expect(reviews[0].policySnapshot.ratingThreshold).toBe(5); - const comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, task.id)); + const comments = await readIssueCommentsByIssueId(task.id); expect(comments[0].body).toContain(instruction); expect(comments[0].body).toContain("Read the SKILL.md"); expect(await resolveRunScopedMentionedSkillKeys({ db, companyId: f.companyId, issueId: task.id, commentIds: [comments[0].id] })).toEqual([skill.key]); @@ -4809,7 +4848,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ? Response.json({ ...payload.pull_request, head: { sha: "c".repeat(40) } }) : undefined); const checks = githubReviewCheckService(db, f.providerFetch); - const [activeEndpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpoint.id)); + const [activeEndpoint] = await readChatEndpointsById(f.endpoint.id); await checks.enqueue(activeEndpoint, githubAutomaticReviewEvent(payload, deliveryId)!, true, "authorized"); await checks.processPending(); const [action] = await db.select().from(chatActions).where(and( @@ -4819,9 +4858,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect((await f.service.handleWebhook(f.endpoint.publicId, "github", signedGitHubWebhookRequest({ event: "pull_request", delivery: deliveryId, payload, webhookSecret: f.webhookSecret, }))).status).toBeLessThan(300); - await expect.poll(async () => (await db.select().from(chatGitHubReviews) - .where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).length, { timeout: 10000 }).toBe(1); - const [late] = await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id)); + await expect.poll(async () => (await readChatGitHubReviewsByEndpointId(f.endpoint.id)).length, { timeout: 10000 }).toBe(1); + const [late] = await readChatGitHubReviewsByEndpointId(f.endpoint.id); const [unrelated] = await db.insert(chatGitHubReviews).values({ companyId: late.companyId, endpointId: late.endpointId, issueId: late.issueId, repositoryId: late.repositoryId, repository: late.repository, pullNumber: late.pullNumber, @@ -4860,14 +4898,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { return undefined; }); const checks = githubReviewCheckService(db, f.providerFetch); - const [activeEndpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpoint.id)); + const [activeEndpoint] = await readChatEndpointsById(f.endpoint.id); await checks.enqueue(activeEndpoint, event, false, "manual_required"); await checks.processPending(); expect(writes).toEqual([expect.objectContaining({ status: "completed", conclusion: "action_required", head_sha: head, details_url: `https://current-vanity.example/${company.issuePrefix}/apps/chat/${f.endpoint.id}/reviews`, })]); - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(0); }); it("uses task-bound bot tools and deterministic checks, then denies revoked people", async () => { const publicOrigin = vi.spyOn(cloudRuntimeIdentity, "runtimePublicOrigin").mockReturnValue("https://current-vanity.example"); @@ -4902,10 +4940,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { eq(chatMessageLinks.direction, "inbound"), ), ); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, link.conversationId)); + const [conversation] = await readChatConversationsById(link.conversationId); const [run] = await db .insert(heartbeatRuns) .values({ @@ -5021,11 +5056,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { section: "metadata", }); // Merely discussing or inspecting a PR must not invalidate its rating. - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(0); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(0); expect(mutations).toHaveLength(0); await expect(service.execute(session, "begin_review", { reviewedCommit: "f".repeat(40) })).rejects.toThrow("head changed"); await service.execute(session, "begin_review", { reviewedCommit: head }); - expect(await db.select().from(chatGitHubReviews).where(eq(chatGitHubReviews.endpointId, f.endpoint.id))).toHaveLength(1); + expect(await readChatGitHubReviewsByEndpointId(f.endpoint.id)).toHaveLength(1); await githubReviewCheckService(db, f.providerFetch).processPending(); expect(mutations.at(-1)).toMatchObject({ url: "https://api.github.com/repos/paperclipai/paperclip/check-runs", @@ -5110,14 +5145,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); expect(retried).toMatchObject({ status: "processed", score: 2 }); expect(mutations).toHaveLength(publishedCount); - const boundThreads = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, f.endpoint.id)); + const boundThreads = await readChatConversationsByEndpointId(f.endpoint.id); const findingThread = boundThreads.find((row) => row.externalThreadId.includes(":rc:"))!; expect(findingThread).toMatchObject({ issueId: conversation.issueId, resourceId: conversation.resourceId, externalConversationId: conversation.externalConversationId, }); - const taskCount = (await db.select().from(issues).where(eq(issues.companyId, f.companyId))).length; + const taskCount = (await readIssuesByCompanyId(f.companyId)).length; await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, @@ -5131,7 +5166,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { eq(chatMessageLinks.providerMessageId, "inline-finding-followup"), )); expect(replyLink.conversationId).toBe(findingThread.id); - expect((await db.select().from(issues).where(eq(issues.companyId, f.companyId))).length).toBe(taskCount); + expect((await readIssuesByCompanyId(f.companyId)).length).toBe(taskCount); await expect( service.execute(session, "submit_review", { ...assessment, score: 5 }), ).rejects.toThrow("already submitted its assessment"); @@ -5308,10 +5343,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "direct_message", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); const configured = await service.get(endpoint.id); const expectedUrl = `https://t.me/${configured!.botUsername}`; @@ -6173,10 +6205,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { setup: { command: "/maya", slackApp: { appName: "maya-paperclip", botName: "maya", command: "/maya" } }, }); const createdCommand = createResponse.body.setup.command as string; - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, createResponse.body.connectionId)); + const [connection] = await readToolConnectionsById(createResponse.body.connectionId); expect(connection).toMatchObject({ connectionPurpose: "channel", transport: "chat_sdk", @@ -6629,7 +6658,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (interruption === "removed") await f.service.configure(f.endpoint.id, { action: "remove" }, actor.userId); else if (interruption === "revoked-link") await db.update(chatIdentityLinks).set({ status: "revoked" }).where(eq(chatIdentityLinks.endpointId, f.endpoint.id)); else { - const [saved] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpoint.id)); + const [saved] = await readChatEndpointsById(f.endpoint.id); await db.update(chatEndpoints).set({ setup: { ...saved.setup, slackAccount: { ...saved.setup.slackAccount!, verificationStatus: "sending" } } }).where(eq(chatEndpoints.id, f.endpoint.id)); } await f.service.slackRegistration.notifyVerified(f.endpoint.id); @@ -6856,7 +6885,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(saved.providerAccountId).toBeNull(); expect(saved.botExternalId).toBeNull(); expect(saved.setup.slackRegistration?.status).toBe("credentials_saved"); - const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, f.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(f.endpoint.connectionId); expect(connection.credentialSecretRefs).toEqual([]); await expect(f.service.slackRegistration.resume(f.endpoint.id, actor)).rejects.toThrow(); }); @@ -6874,7 +6903,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(saved).toMatchObject({ status: "verifying", providerAccountId: "TAUTO", botExternalId: f.botId }); expect(saved.setup.slackRegistration).toMatchObject({ status: "configured", appId: f.appId, errorCode: null }); expect(saved.setup.slackAccount).toEqual(account); - const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, f.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(f.endpoint.connectionId); expect(connection).toMatchObject({ status: "active", enabled: true }); expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1); expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("chat.postMessage"))).toHaveLength(1); @@ -6927,7 +6956,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const slackApp = { appName: "Research Ops", botName: "research-ops", command: "/research" }; await request(app).patch(`/api/chat-endpoints/${endpoint.id}`).send({ slackApp }).expect(200); expect((await service.get(endpoint.id)).setup).toMatchObject({ slackApp, command: "/research" }); - const [stored] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, endpoint.id)); + const [stored] = await readChatEndpointsById(endpoint.id); expect(stored.setup.command).toBe("/research"); await request(app).patch(`/api/chat-endpoints/${endpoint.id}`) .send({ slackApp: { ...slackApp, command: "not-a-command" } }).expect(400); @@ -6994,7 +7023,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(posts).toContainEqual({ threadId: channel.thread.id, text: "I’ll check the border styling." }); const [receipt] = await db.select().from(issueComments).where(and(eq(issueComments.issueId, job.issueId!), eq(issueComments.origin, "fast_response"))); expect(receipt).toMatchObject({ authorAgentId: fixture.assignedAgentId, createdByRunId: null }); - expect((await db.select().from(issues).where(eq(issues.id, job.issueId!)))[0].status).not.toBe("done"); + expect((await readIssuesById(job.issueId!))[0].status).not.toBe("done"); expect(provider).toHaveBeenCalledTimes(1); }); @@ -7047,7 +7076,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(wakeup).toHaveBeenCalledWith(fixture.assignedAgentId, expect.objectContaining({ requestedByActorId: "owner-user", contextSnapshot: expect.objectContaining({ source: "issue.comment", taskKey: (await issueService(db).getById(conversation.issueId))!.identifier, wakeCommentId: expect.any(String) }) })); expect(runtime.endpoints.get(endpoint.id)!.posts.filter(post => post.text.includes("Now assign the follow-ups"))).toHaveLength(1); const ordinary = await issueService(db).addComment(conversation.issueId, "Private system note", { agentId: fixture.assignedAgentId }, { authorType: "agent" }); - expect(await db.select().from(chatPublications).where(eq(chatPublications.commentId, ordinary.id))).toEqual([]); + expect(await readChatPublicationsByCommentId(ordinary.id)).toEqual([]); // A scheduler outage preserves the Board request for the delivery worker. wakeup.mockRejectedValueOnce(new Error("temporary scheduler failure")); await service.publishBoardMessage(endpoint.id, conversation.id, "Retry this work", randomUUID(), "owner-user"); @@ -7091,7 +7120,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await db.insert(heartbeatRuns).values({ id: runId, companyId: fixture.companyId, agentId: fixture.assignedAgentId, status: "running", responsibleUserId: "owner-user", contextSnapshot: { issueId: conversation.issueId, source: "issue.comment", wakeCommentId: comment.id } }); await initializeRunIdentity(db, { companyId: fixture.companyId, runId, issueId: conversation.issueId, responsibleUserId: "owner-user", messageIds: [comment.id], cause: "user_message" }); await addSelectedChatFinal({ companyId: fixture.companyId, issueId: conversation.issueId, agentId: fixture.assignedAgentId, runId, body: "Queued final before access changed" }); - expect(await db.select().from(chatPublications).where(eq(chatPublications.endpointId, endpoint.id))).toHaveLength(2); + expect(await readChatPublicationsByEndpointId(endpoint.id)).toHaveLength(2); if (change === "later-cancelled") { const later = await issueService(db).addComment(conversation.issueId, "A different, cancelled request", { userId: "owner-user" }, { authorType: "user", mirrorToSlack: true }); await db.update(chatActions).set({ status: "cancelled" }).where(and(eq(chatActions.endpointId, endpoint.id), eq(sql`${chatActions.payload}->>'commentId'`, later.id))); @@ -7113,7 +7142,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); } else { expect(runtime.endpoints.get(endpoint.id)!.posts).toEqual([]); - expect((await db.select().from(chatPublications).where(eq(chatPublications.endpointId, endpoint.id))).every(row => row.state === "cancelled")).toBe(true); + expect((await readChatPublicationsByEndpointId(endpoint.id)).every(row => row.state === "cancelled")).toBe(true); } }); @@ -7145,15 +7174,15 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const [workspace] = await db.insert(executionWorkspaces).values({ companyId: fixture.companyId, projectId: project.id, mode: "isolated_workspace", strategyType: "git_worktree", name: "Closed", status: "archived", closedAt: new Date() }).returning(); await db.update(issues).set({ executionWorkspaceId: workspace.id }).where(eq(issues.id, conversation.issueId)); } - const before = await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId)); + const before = await readIssueCommentsByIssueId(conversation.issueId); await expect(service.publishBoardMessage(endpoint.id, conversation.id, "Must not bypass guard", randomUUID(), "owner-user")).rejects.toMatchObject({ status: 409 }); - expect(await db.select().from(issueComments).where(eq(issueComments.issueId, conversation.issueId))).toHaveLength(before.length); + expect(await readIssueCommentsByIssueId(conversation.issueId)).toHaveLength(before.length); wakeup.mockClear(); await service.processPendingDeliveries(); expect(wakeup).not.toHaveBeenCalled(); await service.processPendingPublications(); expect(runtime.endpoints.get(endpoint.id)!.posts).toEqual([]); - expect((await db.select().from(chatPublications).where(eq(chatPublications.endpointId, endpoint.id))).every(row => row.state === "cancelled")).toBe(true); + expect((await readChatPublicationsByEndpointId(endpoint.id)).every(row => row.state === "cancelled")).toBe(true); expect(await db.select().from(chatActions).where(and(eq(chatActions.endpointId, endpoint.id), eq(chatActions.kind, "slack_board_message")))).toEqual([expect.objectContaining({ status: "cancelled" })]); }); @@ -7314,7 +7343,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await db.insert(chatIdentityLinks).values({ companyId: fixture.companyId, endpointId: endpoint.id, principalId: principal.id, paperclipUserId: "owner-user", status: "linked", confirmedAt: new Date() }); const thread = makeThread({ channelId: "slack:CTOOLS", id: "slack:CTOOLS:7100.1", name: "tools" }); await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, message: makeMessage({ id: "7100.1", text: "@maya read this channel", mentioned: true, userId: "UTOOLS" }), trigger: "mention" }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); const [action] = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, endpoint.id), eq(chatActions.kind, "inbound_wakeup"))); expect(action.payload.requestedByActorId).toBe("owner-user"); @@ -7507,12 +7536,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { conversation.issueId, "Recorded approved operation", { agentId: fixture.assignedAgentId, runId: continuationRunId }, { authorType: "agent", authorizationReason: "paperclip_runner_protocol" }, ); - expect(await db.select().from(chatPublications).where(eq(chatPublications.commentId, approvalBookkeeping.id))).toHaveLength(0); + expect(await readChatPublicationsByCommentId(approvalBookkeeping.id)).toHaveLength(0); const approvalFinal = await issueService(db).addComment( conversation.issueId, "Channel created", { agentId: fixture.assignedAgentId, runId: continuationRunId }, { authorType: "agent", authorizationReason: CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON }, ); - expect(await db.select().from(chatPublications).where(eq(chatPublications.commentId, approvalFinal.id))).toHaveLength(1); + expect(await readChatPublicationsByCommentId(approvalFinal.id)).toHaveLength(1); await expect(resolveChatRunPresentationAuthorizationReason(db, { ...presentation, companyId: randomUUID() })).resolves.toBe("internal_agent_write"); await db.update(agentWakeupRequests).set({ payload: { ...approvalWake.mock.calls[0][1].payload, sourceRunId: randomUUID() } }).where(eq(agentWakeupRequests.runId, continuationRunId)); await expect(resolveChatRunPresentationAuthorizationReason(db, presentation)).resolves.toBe("internal_agent_write"); @@ -7601,7 +7630,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const thread = makeThread({ channelId: "D-RUNNING", id: "slack:D-RUNNING:7400.1", isDM: true }); await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, message: makeMessage({ id: "7400.1", text: "Read this conversation", userId: "UBOARD" }), trigger: "direct_message" }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [action] = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, endpoint.id), eq(chatActions.kind, "inbound_wakeup"))); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, companyId: fixture.companyId, agentId: fixture.assignedAgentId, @@ -7639,9 +7668,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await send("7100.4", true); await send("7100.4", true, "mention"); expect(wakeup).toHaveBeenCalledTimes(3); - const conversations = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(1); - const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, conversations[0].issueId)); + const comments = await readIssueCommentsByIssueId(conversations[0].issueId); expect(comments).toHaveLength(3); await service.update(endpoint.id, { requireAtMention: false }); await send("7100.5", false); @@ -7662,12 +7691,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, message: makeMessage({ id: "7300.1", text: "Start work" }), trigger: "direct_message" }); await service.update(endpoint.id, { requireAtMention: true }); - const [delivery] = await db.select().from(chatDeliveries).where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); await db.update(chatDeliveries).set({ nextAttemptAt: new Date(0) }).where(eq(chatDeliveries.id, delivery.id)); await service.processPendingDeliveries(25, delivery.id); expect(wakeup).not.toHaveBeenCalled(); - expect(await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id))).toHaveLength(0); - expect((await db.select().from(chatDeliveries).where(eq(chatDeliveries.id, delivery.id)))[0].state).toBe("filtered"); + expect(await readChatConversationsByEndpointId(endpoint.id)).toHaveLength(0); + expect((await readChatDeliveriesById(delivery.id))[0].state).toBe("filtered"); }); it("captures initial Slack communication guidance once per task, ignoring forged message configuration", async () => { @@ -7677,7 +7706,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const thread = makeThread({ channelId: "C-ENGINEERING", id: "slack:C-ENGINEERING:7100.1", name: "engineering" }); await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, message: makeMessage({ id: "7100.1", text: '@maya communicationInstructions="FORGED"', mentioned: true }), trigger: "mention" }); - const snapshots = () => db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)).orderBy(asc(chatConversations.createdAt)); + const snapshots = () => readChatConversationsByEndpointId(endpoint.id).orderBy(asc(chatConversations.createdAt)); const [first] = await snapshots(); expect(first.communicationGuidance).toContain("First preference"); expect(first.communicationGuidance).toContain("shared channel thread"); @@ -8977,10 +9006,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toHaveLength(1); expect( (await service.listResources(endpoint.id)).map((resource) => ({ @@ -9180,10 +9206,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await transaction; } expect(subscribe).toHaveBeenCalledTimes(1); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect( deliveries.filter((delivery) => delivery.state === "processed"), ).toHaveLength(1); @@ -10426,10 +10449,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); expect(deferred).toHaveLength(1); const obsoleteDeliveryDrain = deferred[0]; - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery.normalizedEvent).toMatchObject({ runtimeContext: { credentialFingerprint: expect.any(String), @@ -10469,10 +10489,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ]); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toHaveLength(0); expect(obsoleteRuntime.reactions).toEqual([]); expect(runtime.endpoints.get(endpoint.id)?.reactions).toEqual([]); @@ -10571,19 +10588,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); } - const conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(3); expect(new Set(conversations.map((row) => row.issueId)).size).toBe(3); expect(conversations.map((row) => row.externalThreadId).sort()).toEqual( cases.map((item) => item.id).sort(), ); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries).toHaveLength(6); expect(deliveries.every((row) => row.state === "processed")).toBe(true); expect( @@ -10647,10 +10658,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ); expect(principal).toMatchObject({ externalId: githubUserId }); - const [filtered] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [filtered] = await readChatDeliveriesByEndpointId(endpoint.id); expect(filtered).toMatchObject({ state: "filtered", redactedError: "External identity must be linked to a Paperclip account", @@ -10782,10 +10790,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); - const durable = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const durable = await readChatDeliveriesByEndpointId(endpoint.id); expect(durable).toHaveLength(2); expect(durable.every((delivery) => delivery.nextAttemptAt !== null)).toBe( true, @@ -10801,16 +10806,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // drain to finish on loaded CI instead of sharing the default one-second budget. deferred.shift()?.(); await vi.waitFor(async () => { - const rows = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const rows = await readChatConversationsByEndpointId(endpoint.id); expect(rows).toHaveLength(1); }, { timeout: 10_000 }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await vi.waitFor(async () => { const rows = await db .select({ body: issueComments.body }) @@ -10852,10 +10851,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: laterReply, trigger: "unaddressed_message", }); - const [replyDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [replyDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); await db .update(chatDeliveries) .set({ nextAttemptAt: new Date() }) @@ -10864,10 +10860,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingDeliveries(25, replyDelivery!.id); await expect( - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, replyDelivery!.id)), + readChatDeliveriesById(replyDelivery!.id), ).resolves.toEqual([ expect.objectContaining({ state: "retry", @@ -10876,10 +10869,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ]); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toHaveLength(0); expect(wakeup).not.toHaveBeenCalled(); @@ -10913,10 +10903,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, mentionDelivery!.id)); await service.processPendingDeliveries(25, mentionDelivery!.id); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); await db .update(chatDeliveries) @@ -10962,20 +10949,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "unaddressed_message", }); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); for (let expectedAttempt = 1; expectedAttempt <= 13; expectedAttempt += 1) { await db .update(chatDeliveries) .set({ nextAttemptAt: new Date() }) .where(eq(chatDeliveries.id, delivery!.id)); await service.processPendingDeliveries(25, delivery!.id); - const current = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery!.id)) + const current = await readChatDeliveriesById(delivery!.id) .then((rows) => rows[0]); expect(current).toMatchObject({ state: expectedAttempt <= 12 ? "retry" : "filtered", @@ -10983,10 +10964,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); } await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toHaveLength(0); expect(wakeup).not.toHaveBeenCalled(); expect(deferred).toHaveLength(1); @@ -11016,10 +10994,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const credentialRefsBeforeDeletion = await db .select({ refs: toolConnections.credentialSecretRefs }) .from(toolConnections) @@ -11130,10 +11105,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { credentials: { installationId: 8642 }, }); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, publication!.id)), + readChatPublicationsById(publication!.id), ).resolves.toEqual([expect.objectContaining({ state: "published" })]); }); @@ -11160,10 +11132,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await service.handleWebhook( endpoint.publicId, @@ -11459,10 +11428,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: "attention", setup: { step: "provider_setup", webhookVerifiedAt: null }, }); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [connection] = await readToolConnectionsById(endpoint.connectionId); expect(connection).toMatchObject({ status: "disabled", enabled: false, @@ -11666,10 +11632,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { setup: { step: "complete" }, }); await expect( - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)), + readChatDeliveriesByEndpointId(endpoint.id), ).resolves.toEqual([ expect.objectContaining({ providerEventId: "lifecycle:foreign-installation-deleted", @@ -12279,10 +12242,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect( restartedRuntime.endpoints.get(configured.endpoint.id)?.webhookRequest, ).not.toBeNull(); - const processed = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, staged!.id)) + const processed = await readChatActionsById(staged!.id) .then((rows) => rows[0]); expect(processed).toMatchObject({ status: "processed", @@ -12395,10 +12355,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const ingress = await context.ingress(); expect(ingress).toMatchObject({ status: "received" }); await service.processPendingGitHubWebhookIngress(1, ingress!.id); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); for (const delivery of deliveries) { // Advance only this fixture's persisted coalescing deadline; the // production batching delay is exercised by its dedicated tests. @@ -12409,10 +12366,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingDeliveries(1, delivery.id); } expect(context.wakeup).toHaveBeenCalledTimes(1); - const tasks = await db - .select() - .from(issues) - .where(eq(issues.companyId, context.fixture.companyId)); + const tasks = await readIssuesByCompanyId(context.fixture.companyId); expect(tasks).toHaveLength(1); const comments = await db .select({ body: issueComments.body }) @@ -13127,10 +13081,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .status, ).toBe(401); await expect( - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)), + readChatDeliveriesByEndpointId(endpoint.id), ).resolves.toEqual([]); const responses = await Promise.all( @@ -13141,10 +13092,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(responses.map((response) => response.status)).toEqual([ 200, 200, 200, ]); - const rows = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const rows = await readChatDeliveriesByEndpointId(endpoint.id); expect(rows).toHaveLength(1); expect(rows[0]).toMatchObject({ state: "filtered", @@ -13373,10 +13321,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(conflictingDuplicate.status).toBe(202); expect(webhookHook).toHaveBeenCalledTimes(1); await expect( - db - .select() - .from(chatActions) - .where(eq(chatActions.id, terminal!.id)) + readChatActionsById(terminal!.id) .then((rows) => rows[0]), ).resolves.toMatchObject({ status: "failed", @@ -13397,10 +13342,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(recovered.status).toBe(202); expect(webhookHook).toHaveBeenCalledTimes(2); await expect( - db - .select() - .from(chatActions) - .where(eq(chatActions.id, terminal!.id)) + readChatActionsById(terminal!.id) .then((rows) => rows[0]), ).resolves.toMatchObject({ status: "processed", @@ -13442,10 +13384,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { | undefined; const sdkDispatch = vi.fn(); try { - const [record] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, context.endpoint.id)); + const [record] = await readChatEndpointsById(context.endpoint.id); await db .update(chatEndpoints) .set({ setup: { ...record!.setup, step: "complete" } }) @@ -13631,10 +13570,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); releaseReplay(); await expect(pendingResponse).resolves.toMatchObject({ status: 202 }); - const cancelled = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, action.id)) + const cancelled = await readChatActionsById(action.id) .then((rows) => rows[0]); expect(cancelled).toMatchObject({ status: "cancelled", @@ -13669,10 +13605,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const originalConversation = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)) + const originalConversation = await readChatConversationsByEndpointId(endpoint.id) .then((rows) => rows[0]!); const renamedRepository = { @@ -13731,10 +13664,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ); - const [migratedConversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [migratedConversation] = await readChatConversationsByEndpointId(endpoint.id); expect(migratedConversation).toMatchObject({ id: originalConversation.id, issueId: originalConversation.issueId, @@ -13762,10 +13692,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toEqual([ expect.objectContaining({ id: originalConversation.id, @@ -15671,10 +15598,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Teams conversation"); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ @@ -15838,10 +15762,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { updatedAt: new Date(), }) .where(eq(chatEndpoints.id, configured.endpoint.id)); - const [endpointBefore] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, configured.endpoint.id)); + const [endpointBefore] = await readChatEndpointsById(configured.endpoint.id); if (!endpointBefore) throw new Error("Expected Telegram endpoint row"); const [connectionBefore] = await db .select({ refs: toolConnections.credentialSecretRefs }) @@ -15873,10 +15794,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(providerFetch).not.toHaveBeenCalled(); await expect( - db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, configured.endpoint.id)), + readChatEndpointsById(configured.endpoint.id), ).resolves.toEqual([endpointBefore]); await expect( db @@ -16748,10 +16666,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); expect(await service.listConversations(endpoint.id)).toHaveLength(1); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); expect(wakeup).toHaveBeenCalledTimes(1); }, @@ -16882,10 +16797,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); await expect(service.listResources(endpoint.id)).resolves.toEqual([ expect.objectContaining({ label: "#c-lifecycle" }), @@ -16915,7 +16827,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatConversations.id, conversation.id)), ).toEqual([{ state: "unavailable" }]); expect( - await db.select().from(issues).where(eq(issues.id, conversation.issueId)), + await readIssuesById(conversation.issueId), ).toHaveLength(1); await deliverMessage({ callbacks, @@ -16933,10 +16845,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); await expect(service.listResources(endpoint.id)).resolves.toEqual([ expect.objectContaining({ availability: "unavailable", enabled: true }), @@ -16955,10 +16864,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); expect( ( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + await readChatDeliveriesByEndpointId(endpoint.id) ).filter( (delivery) => delivery.providerEventId === "lifecycle:Ev-member-left", ), @@ -16973,10 +16879,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { healthMessage: "Slack app was uninstalled", }); expect(runtime.endpoints.has(endpoint.id)).toBe(false); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [connection] = await readToolConnectionsById(endpoint.connectionId); expect(connection).toMatchObject({ status: "disabled", enabled: false, @@ -17179,10 +17082,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { teams.service.listResources(teamsEndpoint.id), ).resolves.toEqual([]); await expect( - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, teamsEndpoint.id)), + readChatDeliveriesByEndpointId(teamsEndpoint.id), ).resolves.toHaveLength(0); await deliverTeamsLifecycle(teamsPayload("add")); @@ -17220,10 +17120,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, teamsEndpoint.id)), + readChatConversationsByEndpointId(teamsEndpoint.id), ).resolves.toHaveLength(1); await deliverTeamsLifecycle(teamsPayload("remove")); await expect( @@ -17452,10 +17349,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, telegramEndpoint.id)), + readChatConversationsByEndpointId(telegramEndpoint.id), ).resolves.toHaveLength(1); await expect( telegram.service.listResources(telegramEndpoint.id), @@ -18254,10 +18148,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "subscribed_message", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected setup conversation"); for (const progressState of ["queued", "working"] as const) { @@ -18348,10 +18239,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected setup conversation"); const rootContext = await chatWakeContext({ @@ -18476,10 +18364,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, externalUserId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const providerAccountId = (await service.get(endpoint.id)) .providerAccountId; const principal = await db @@ -18724,10 +18609,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, externalUserId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const providerAccountId = (await service.get(endpoint.id)) .providerAccountId; const principal = await db @@ -19249,10 +19131,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); } - const durable = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const durable = await readChatDeliveriesByEndpointId(endpoint.id); expect(durable).toHaveLength(8); expect(durable.every((delivery) => delivery.nextAttemptAt !== null)).toBe( true, @@ -19279,10 +19158,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]), ); expect( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + await readChatConversationsByEndpointId(endpoint.id), ).toHaveLength(0); expect(deferred).toHaveLength(1); @@ -19304,10 +19180,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { releaseFirstDelivery(); await draining; expect(drainSettled).toBe(true); - const conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(1); const [conversation] = conversations; const comments = await db @@ -19562,10 +19435,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); if (!delivery) throw new Error("Expected exact provider source receipt"); await service.processPendingDeliveries(25, delivery.id); - return db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery.id)) + return readChatDeliveriesById(delivery.id) .then((rows) => rows[0]!); }; await admit(100, controlSecond - 5, "Initial task before control"); @@ -19821,10 +19691,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "connection closed after provider acceptance became unknown", ); await f.service.processPendingPublications(); - const [unknown] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, publication.id)); + const [unknown] = await readChatPublicationsById(publication.id); expect(unknown).toMatchObject({ state: "delivery_unknown", providerMessageId: null, @@ -19836,15 +19703,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "mark_delivered", "owner-user", ); - const [receipt] = await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `task-control-authorization:${publication.id}`, - ), - ); + const [receipt] = await readChatActionsByProviderActionId(`task-control-authorization:${publication.id}`); expect(receipt).toMatchObject({ status: "processed", result: { code: "task_control_marked_delivered_by_operator" }, @@ -20019,10 +19878,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ state: "retry", nextAttemptAt: new Date(0) }) .where(eq(chatDeliveries.id, accepted.id)); await f.service.processPendingDeliveries(25, accepted.id); - const [retried] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, accepted.id)); + const [retried] = await readChatDeliveriesById(accepted.id); // Existing current-access recovery refuses this now-closed source. The // first-seen filter must not erase or recreate its already saved work. expect(retried).toMatchObject({ @@ -20073,15 +19929,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const f = await chronologyFixture("telegram"); try { const control = await f.control("close"); - const [originalAuthorization] = await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `task-control-authorization:${control.id}`, - ), - ); + const [originalAuthorization] = await readChatActionsByProviderActionId(`task-control-authorization:${control.id}`); await f.service.configure( f.endpoint.id, { action: "pause" }, @@ -20129,10 +19977,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { (await f.admit(201, freshSecond, "Fresh request after resumed control")) .state, ).toBe("processed"); - const [retained] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, duplicate.id)); + const [retained] = await readChatDeliveriesById(duplicate.id); expect(retained?.normalizedEvent).toEqual(duplicate.normalizedEvent); expect(control.publishedAt).not.toBeNull(); } finally { @@ -20145,12 +19990,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { try { const delivery = await f.admit(200, f.controlSecond, "/close\n"); await f.service.processPendingPublications(); - const [control] = await db - .select() - .from(chatPublications) - .where( - eq(chatPublications.idempotencyKey, `control:close:${delivery.id}`), - ); + const [control] = await readChatPublicationsByIdempotencyKey(`control:close:${delivery.id}`); expect(control?.state).toBe("published"); expect((await f.service.listConversations(f.endpoint.id))[0]?.state).toBe( "completed", @@ -20294,10 +20134,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "older control confirmation response lost", ); await f.service.processPendingPublications(); - const [unknown] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, older.id)); + const [unknown] = await readChatPublicationsById(older.id); expect(unknown?.state).toBe("delivery_unknown"); providerRuntime.postError = null; // FIFO correctly blocks later publications in this old conversation. @@ -20485,10 +20322,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "direct_message", }); - const durable = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const durable = await readChatDeliveriesByEndpointId(endpoint.id); expect(durable).toHaveLength(2); expect(durable.every((delivery) => delivery.nextAttemptAt !== null)).toBe( true, @@ -20519,10 +20353,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]), ); expect( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + await readChatConversationsByEndpointId(endpoint.id), ).toHaveLength(0); expect(wakeup).not.toHaveBeenCalled(); expect(deferred).toHaveLength(1); @@ -20531,10 +20362,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await vi.waitFor(() => expect(wakeup).toHaveBeenCalledTimes(2), { timeout: 3_000, }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const comments = await db .select({ id: issueComments.id, body: issueComments.body }) @@ -20637,10 +20465,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "direct_message", }); - const durable = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const durable = await readChatDeliveriesByEndpointId(endpoint.id); expect(durable).toHaveLength(2); expect(durable).toEqual( expect.arrayContaining([ @@ -20720,10 +20545,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "subscribed_message", }); - const [deferredReply] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [deferredReply] = await readChatDeliveriesByEndpointId(endpoint.id); expect(deferredReply).toMatchObject({ state: "retry", attempts: 1, @@ -20791,10 +20613,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: makeMessage({ id: "9120.2", text: "not for the bot" }), trigger: "subscribed_message", }); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "retry", attempts: 1 }); for (let expectedAttempt = 2; expectedAttempt <= 13; expectedAttempt += 1) { @@ -20852,10 +20671,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); expect(wakeup).not.toHaveBeenCalled(); - const [filteredGroupDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [filteredGroupDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(filteredGroupDelivery).toMatchObject({ state: "filtered", attempts: 0, @@ -22199,10 +22015,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!endpointRuntime) throw new Error("Expected Teams endpoint runtime"); const initialRouteCount = endpointRuntime.recordedMicrosoftTeamsRoutes.length; - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const commentCount = await db .select({ id: issueComments.id }) .from(issueComments) @@ -22331,10 +22144,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ); expect(principals).toEqual([{ externalId: aadObjectId }]); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await expect( db .select({ body: issueComments.body }) @@ -22398,10 +22208,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); const body = outcome === "malformed" @@ -22560,10 +22367,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (outcome === "too_large") expect(storage.storage.getObject).not.toHaveBeenCalled(); } - const [publication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, result.id)); + const [publication] = await readChatPublicationsById(result.id); if (!denied) expect(publication).toMatchObject({ state: ambiguous ? "delivery_unknown" : "published", @@ -22684,10 +22488,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "direct_message", }); - const conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); const channelConversation = conversations.find( (conversation) => !conversation.isDirectMessage, ); @@ -22981,10 +22782,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { else await deliver(); await lifecycleCompletion; if (mode === "restart" || mode === "revoked_after_receipt") { - const [received] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [received] = await readChatDeliveriesByEndpointId(endpoint.id); expect(received).toMatchObject({ state: "received", attempts: 0 }); expect(get).not.toHaveBeenCalled(); expect(storage.putFile).not.toHaveBeenCalled(); @@ -23246,10 +23044,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(personalFetch).toHaveBeenCalledTimes(1); expect(storage.putFile).toHaveBeenCalledTimes(1); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); const channelDelivery = deliveries.find( (delivery) => delivery.providerEventId === @@ -23388,10 +23183,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: parsed!, trigger: "direct_message", }); - const [received] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [received] = await readChatDeliveriesByEndpointId(endpoint.id); expect(received).toMatchObject({ state: "received", attempts: 0 }); expect(received!.normalizedEvent).toMatchObject({ message: { @@ -23434,10 +23226,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { scheduleDeferredWork: () => undefined, }); await restarted.service.processPendingDeliveries(); - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, received!.id)); + const [processed] = await readChatDeliveriesById(received!.id); expect(processed).toMatchObject({ state: "processed", redactedError: @@ -23658,10 +23447,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, delivery!.id)); await active.service.processPendingDeliveries(); } - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery!.id)); + const [processed] = await readChatDeliveriesById(delivery!.id); expect(processed).toMatchObject({ state: "processed", redactedError: @@ -23782,10 +23568,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.endpointId, endpoint.id)); await service.processPendingDeliveries(); - const [deferredReply] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [deferredReply] = await readChatDeliveriesByEndpointId(endpoint.id); expect(deferredReply).toMatchObject({ state: "retry", attempts: 1, @@ -23880,10 +23663,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { body: "@maya keep both Teams messages" }, { body: "Teams follow-up whose callback arrived first" }, ]); - const [hydratedReply] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, deferredReply.id)); + const [hydratedReply] = await readChatDeliveriesById(deferredReply.id); expect(hydratedReply).toMatchObject({ state: "processed", principalId: expect.any(String), @@ -24046,10 +23826,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ nextAttemptAt: new Date(0) }) .where(eq(chatDeliveries.endpointId, endpoint.id)); await service.processPendingDeliveries(); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "retry", attempts: 1, @@ -24267,10 +24044,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); } await restarted.service.processPendingDeliveries(); - const [storedDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery!.id)); + const [storedDelivery] = await readChatDeliveriesById(delivery!.id); expect({ state: storedDelivery!.state, error: storedDelivery!.redactedError, @@ -24560,10 +24334,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(storage.putFile).not.toHaveBeenCalled(); if (revokeAt === "storage") expect(storage.storage.deleteObject).toHaveBeenCalledTimes(1); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(JSON.stringify(deliveries)).not.toContain("privatepayload"); expect(JSON.stringify(deliveries)).not.toContain("body_html"); if (revokeAt === "ambiguous_anchor") { @@ -24681,10 +24452,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [durableDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [durableDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(durableDelivery).toMatchObject({ state: "received", attempts: 0 }); expect(JSON.stringify(durableDelivery.normalizedEvent)).not.toContain( "xoxb-restart-attachment", @@ -24723,10 +24491,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { body: attachmentBody, }), ); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const storedAttachments = await db .select({ attachmentId: issueAttachments.id, @@ -24819,10 +24584,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ).rejects.toThrow("injected provider subscription failure"); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "retry", attempts: 1 }); expect(wakeup).not.toHaveBeenCalled(); const [pendingIntent] = await db @@ -24853,10 +24615,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { details: { code: "chat_inbound_wakeup_unadmitted" }, }); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)), + await readHeartbeatRunsByCompanyId(fixture.companyId), ).toEqual([]); expect(storage.putFile).toHaveBeenCalledTimes(1); await db @@ -24901,10 +24660,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingDeliveries(25, delivery.id); expect(wakeup).toHaveBeenCalledTimes(1); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, action.id)), + await readAgentWakeupRequestsById(action.id), ).toHaveLength(1); expect( await db @@ -24991,10 +24747,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { result: { code: "inbound_wakeup_retry" }, }); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, firstAction.id)), + await readAgentWakeupRequestsById(firstAction.id), ).toEqual([]); await send(firstThread.thread, "9200.2", false); expect(wakeup).toHaveBeenCalledTimes(1); @@ -25407,10 +25160,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toBe(false); expect(timingEvents.at(-1)?.statusCode).toBe(401); expect( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)), + await readChatDeliveriesByEndpointId(endpoint.id), ).toHaveLength(0); const originalTransaction = db.transaction.bind(db); @@ -25474,10 +25224,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); expect(timingEvents.at(-1)?.statusCode).toBe(503); expect( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)), + await readChatDeliveriesByEndpointId(endpoint.id), ).toHaveLength(0); transactionSpy.mockRestore(); @@ -25489,19 +25236,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // one-second worker SLA. Keep the condition bounded under suite load. await vi.waitFor( async () => { - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries).toHaveLength(1); expect(deliveries[0].state).toBe("processed"); }, { timeout: 5_000 }, ); - const [initialDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [initialDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); const initialDuplicateCount = Number( initialDelivery.normalizedEvent.deduplication?.duplicateCount ?? 0, ); @@ -25551,18 +25292,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } expect(acceptedRedelivery.status).toBe(200); await vi.waitFor(async () => { - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect( Number(delivery.normalizedEvent.deduplication?.duplicateCount ?? 0), ).toBeGreaterThan(initialDuplicateCount); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); }); } finally { @@ -25722,10 +25457,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { providerError ? { ok: false, error: providerError } : { ok: true }, { ok: true }, ]); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ id: heldDeliveryId, state: terminalState, @@ -25954,10 +25686,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { deferred.shift()?.(); await service.shutdown(); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id) .orderBy(asc(chatDeliveries.receivedAt)); expect(deliveries).toHaveLength(1); expect(deliveries.map((delivery) => delivery.state)).toEqual(["filtered"]); @@ -25968,16 +25697,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await service.get(endpoint.id)).toMatchObject({ status: "active" }); expect(wakeup).not.toHaveBeenCalled(); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(0); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)), + await readHeartbeatRunsByCompanyId(fixture.companyId), ).toHaveLength(0); expect( await db @@ -26137,10 +25860,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } as never), ).resolves.toEqual({ action: "clear" }); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id) .orderBy(asc(chatDeliveries.receivedAt)); expect(deliveries).toHaveLength(3); expect(deliveries[0]).toMatchObject({ @@ -26163,10 +25883,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); expect(wakeup).not.toHaveBeenCalled(); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(0); const freshCallbacks = runtime.configurations.get(endpoint.id)?.callbacks; @@ -26192,10 +25909,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingDeliveries(); await vi.waitFor(() => expect(wakeup).toHaveBeenCalledTimes(1)); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); await service.shutdown(); }); @@ -26264,16 +25978,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingDeliveries(); expect(wakeup).toHaveBeenCalledTimes(1); expect(runtime.endpoints.get(endpoint.id)?.reactions).toHaveLength(1); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed" }); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)), + await readIssueCommentsByCompanyId(fixture.companyId), ).toHaveLength(1); await service.shutdown(); }); @@ -26334,22 +26042,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { timeout: 10_000 }, ); - let conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); - let endpointIssues = await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)); - let deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); - let comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversations[0].issueId)) + let conversations = await readChatConversationsByEndpointId(endpoint.id); + let endpointIssues = await readIssuesByCompanyId(fixture.companyId); + let deliveries = await readChatDeliveriesByEndpointId(endpoint.id); + let comments = await readIssueCommentsByIssueId(conversations[0].issueId) .orderBy(asc(issueComments.createdAt), asc(issueComments.id)); expect(conversations).toHaveLength(1); expect(endpointIssues).toHaveLength(1); @@ -26383,18 +26079,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: makeMessage({ id: "1000.2", text: "Here is another detail" }), trigger: "subscribed_message", }); - conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); - endpointIssues = await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)); - comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversations[0].issueId)) + conversations = await readChatConversationsByEndpointId(endpoint.id); + endpointIssues = await readIssuesByCompanyId(fixture.companyId); + comments = await readIssueCommentsByIssueId(conversations[0].issueId) .orderBy(asc(issueComments.createdAt), asc(issueComments.id)); expect(conversations).toHaveLength(1); expect(endpointIssues).toHaveLength(1); @@ -26429,19 +26116,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { label: "engineering", enabled: true }, { label: "finance", enabled: true }, ]); - deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect( deliveries.find((delivery) => delivery.providerEventId.includes("2000.1")) ?.state, ).toBe("processed"); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(2); const finance = resources.find((resource) => resource.label === "finance"); @@ -26461,16 +26142,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); expect( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + await readChatConversationsByEndpointId(endpoint.id), ).toHaveLength(2); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(2); }); @@ -26512,10 +26187,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const principal = await db .select() .from(chatExternalPrincipals) @@ -26689,31 +26361,15 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await db.update(heartbeatRuns).set({ resultJson: { presentationDecision: { authorizationReason: "internal_agent_write" } } }).where(eq(heartbeatRuns.id, runId)); await enqueueChatRunMilestones(db); await service.processPendingPublications(); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); - const [removal] = await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `receipt_reaction_remove:${delivery.id}`, - ), - ); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); + const [removal] = await readChatActionsByProviderActionId(`receipt_reaction_remove:${delivery.id}`); expect(removal).toBeDefined(); await service.processPendingReceiptReactions(1, removal!.id); await service.processPendingReceiptReactions(1, removal!.id); expect(runtime.endpoints.get(endpoint.id)?.removedReactions).toEqual([ { threadId: thread.thread.id, messageId: "5603841952", emoji: "eyes" }, ]); - const [add] = await db - .select() - .from(chatActions) - .where( - eq(chatActions.providerActionId, `receipt_reaction:${delivery.id}`), - ); + const [add] = await readChatActionsByProviderActionId(`receipt_reaction:${delivery.id}`); await db .update(chatActions) .set({ @@ -27017,19 +26673,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await deliveryWork; if (mode !== "final_before_add") await publish(); } - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); - const [removal] = await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `receipt_reaction_remove:${delivery.id}`, - ), - ); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); + const [removal] = await readChatActionsByProviderActionId(`receipt_reaction_remove:${delivery.id}`); expect(removal).toBeDefined(); if (!["final_before_add", "held_token", "held_add"].includes(mode)) expect(removal.payload.githubReceipt).toEqual({ @@ -27062,10 +26707,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { activeService = next.service; } if (mode === "rotation") { - const [current] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [current] = await readChatEndpointsById(endpoint.id); await db .update(chatEndpoints) .set({ @@ -27084,10 +26726,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ].includes(mode); await activeService.processPendingReceiptReactions(1, removal.id); if (mode === "remove_retry") { - const [failed] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, removal.id)); + const [failed] = await readChatActionsById(removal.id); expect(failed).toMatchObject({ status: "failed", result: { retryable: true, attempts: 1 }, @@ -27102,10 +26741,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // Blocked operations retain their failed audit result. Retry only the // explicit rate-limit case; successful cases also prove idempotency. if (!blocked) await activeService.processPendingReceiptReactions(1, removal.id); - const [result] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, removal.id)); + const [result] = await readChatActionsById(removal.id); expect(result.status).toBe( mode === "rotation" ? "cancelled" : blocked ? "failed" : "processed", ); @@ -27262,17 +26898,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ], ); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", attempts: 1 }); expect(wakeup).toHaveBeenCalledTimes(1); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); const activity = (await service.listActivity(endpoint.id)).find( (item) => item.id === delivery.id, @@ -27417,10 +27047,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); if (mode !== "final_before_add") await publishFinal(); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); const [removal] = await db .select() .from(chatActions) @@ -27472,12 +27099,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { emoji: "eyes", }); expect(providerRuntime.reactions).toEqual(expectedAdds); - const [add] = await db - .select() - .from(chatActions) - .where( - eq(chatActions.providerActionId, `receipt_reaction:${delivery.id}`), - ); + const [add] = await readChatActionsByProviderActionId(`receipt_reaction:${delivery.id}`); await db .update(chatActions) .set({ @@ -27985,15 +27607,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); expect( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + await readChatConversationsByEndpointId(endpoint.id), ).toHaveLength(0); - const [filtered] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [filtered] = await readChatDeliveriesByEndpointId(endpoint.id); expect(filtered.state).toBe("filtered"); const now = new Date(); @@ -28049,15 +27665,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); expect( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + await readChatConversationsByEndpointId(endpoint.id), ).toHaveLength(0); - const viewerDelivery = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + const viewerDelivery = await readChatDeliveriesByEndpointId(endpoint.id) .then((rows) => rows.find((delivery) => delivery.providerEventId.includes("3000.2")), ); @@ -28081,10 +27691,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, "U-LINK-ME"); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toBeDefined(); const [comment] = await db .select() @@ -28098,10 +27705,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(comment.authorType).toBe("user"); expect(comment.authorUserId).toBe("linked-paperclip-user"); const userCommentCountBeforeSuspend = ( - await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)) + await readIssueCommentsByIssueId(conversation.issueId) ).filter((candidate) => candidate.authorType === "user").length; await db @@ -28119,17 +27723,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "subscribed_message", }); - const allComments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)); + const allComments = await readIssueCommentsByIssueId(conversation.issueId); expect( allComments.filter((candidate) => candidate.authorType === "user"), ).toHaveLength(userCommentCountBeforeSuspend); - const allDeliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const allDeliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect( allDeliveries.find((delivery) => delivery.providerEventId.includes("3000.4"), @@ -28173,10 +27771,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } expect(await service.listConversations(endpoint.id)).toEqual([]); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "filtered", redactedError: "Endpoint sponsor can no longer authorize external guests", @@ -28240,7 +27835,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(wakeup).toHaveBeenCalledTimes(index * 2 + 1); const conversation = (await service.listConversations(endpoint.id)).find(row => row.externalThreadId === root.thread.id)!; expect(conversation).toMatchObject({ state: "active" }); - const task = await db.select().from(issues).where(eq(issues.id, conversation.issueId!)).then(rows => rows[0]); + const task = await readIssuesById(conversation.issueId!).then(rows => rows[0]); expect(task).toMatchObject({ assigneeAgentId: fixture.assignedAgentId, responsibleUserId: "owner-user" }); await deliverMessage({ callbacks, endpointId: endpoint.id, thread: root.thread, @@ -28248,7 +27843,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "subscribed_message", }); expect(wakeup).toHaveBeenCalledTimes(index * 2 + 2); - const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, task!.id)).orderBy(asc(issueComments.createdAt), asc(issueComments.id)); + const comments = await readIssueCommentsByIssueId(task!.id).orderBy(asc(issueComments.createdAt), asc(issueComments.id)); expect(comments.map(comment => comment.body)).toEqual([text.trim() || "Started a conversation by mentioning the agent.", "u there?"]); expect(runtime.endpoints.get(endpoint.id)?.reactions).toContainEqual({ threadId: root.thread.id, @@ -28260,16 +27855,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await service.listConversations(endpoint.id)).toHaveLength(variants.length); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(variants.length); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)), + await readHeartbeatRunsByCompanyId(fixture.companyId), ).toHaveLength(0); expect( await db @@ -28277,10 +27866,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .from(chatPublications) .where(eq(chatPublications.companyId, fixture.companyId)), ).toHaveLength(0); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries).toHaveLength(variants.length * 2); expect( deliveries.every( @@ -28305,7 +27891,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: makeMessage({ id: "3300.1", text: "", mentioned: true, userId: restriction === "unlinked" ? "U-UNLINKED" : "UBOARD" }), }); expect(await service.listConversations(endpoint.id)).toEqual([]); - expect(await db.select().from(issues).where(eq(issues.companyId, fixture.companyId))).toHaveLength(0); + expect(await readIssuesByCompanyId(fixture.companyId)).toHaveLength(0); expect(root.subscribe).not.toHaveBeenCalled(); expect(wakeup).not.toHaveBeenCalled(); }); @@ -28575,10 +28161,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Visible answer. never expose this reasoning", @@ -28605,10 +28188,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(providerRuntime?.posts).toEqual([ { threadId: channel.thread.id, text: "Visible answer." }, ]); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)); + const publications = await readChatPublicationsByEndpointId(endpoint.id); const links = await db .select() .from(chatMessageLinks) @@ -28645,10 +28225,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { conversationId: conversation.id, }, }); - const [boundIssue] = await db - .select() - .from(issues) - .where(eq(issues.id, conversation.issueId)); + const [boundIssue] = await readIssuesById(conversation.issueId); expect(boundIssue.assigneeAgentId).toBe(fixture.assignedAgentId); await service.configure(endpoint.id, { action: "remove" }, "owner-user"); @@ -28742,10 +28319,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { body: "Answer received for the interaction. Closing issue." }, ]); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, lifecycleComment.id)), + readChatPublicationsByCommentId(lifecycleComment.id), ).resolves.toHaveLength(0); // A continuation agent may write its final prose before the heartbeat @@ -28760,10 +28334,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { authorType: "agent", authorizationReason: "allow_self" }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, provisionalFinalComment.id)), + readChatPublicationsByCommentId(provisionalFinalComment.id), ).resolves.toHaveLength(0); const authorizationReason = @@ -28887,10 +28458,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, bookkeepingComment.id)), + readChatPublicationsByCommentId(bookkeepingComment.id), ).resolves.toHaveLength(0); const authorizationReason = @@ -29203,10 +28771,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), }); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, firstFinal[0]!.id)), + readChatPublicationsByCommentId(firstFinal[0]!.id), ).resolves.toHaveLength(1); await service.processPendingPublications(1_000); @@ -29296,7 +28861,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: makeMessage({ id: "41701", text: "@maya produce one quiet response", mentioned: true }), trigger: "mention" }); const config = await githubChatManagementService(db).configuration(endpoint.id, "owner-user"); await db.insert(chatGitHubConfigurations).values({ companyId: fixture.companyId, endpointId: endpoint.id, configuration: config.configuration, updatedByUserId: "owner-user" }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [run] = await db.insert(heartbeatRuns).values({ companyId: fixture.companyId, agentId: fixture.assignedAgentId, status: "succeeded", contextSnapshot: await chatWakeContext({ endpointId: endpoint.id, issueId: conversation.issueId, provider: "github", providerMessageId: "41701" }) }).returning(); @@ -29330,7 +28895,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const thread = makeThread({ channelId: "github:paperclipai/paperclip", id: "github:paperclipai/paperclip:issue:417" }); await deliverMessage({ callbacks, endpointId: endpoint.id, provider: "github", thread: thread.thread, message: makeMessage({ id: "41701", text: "@maya answer", mentioned: true }), trigger: "mention" }); - const [conversation] = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [run] = await db.insert(heartbeatRuns).values({ companyId: fixture.companyId, agentId: fixture.assignedAgentId, status: "succeeded", contextSnapshot: await chatWakeContext({ endpointId: endpoint.id, issueId: conversation.issueId, provider: "github", providerMessageId: "41701" }) }).returning(); await expect(resolveChatRunPresentationAuthorizationReason(db, { companyId: fixture.companyId, issueId: conversation.issueId, runId: run.id })).resolves.toBe("allow_chat_run_presentation"); @@ -29388,10 +28953,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { enabled: true, }); if (linked) { - const [stored] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [stored] = await readChatEndpointsById(endpoint.id); const [principal] = await db .insert(chatExternalPrincipals) .values({ @@ -29539,10 +29101,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ); const deliveries = () => - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + readChatDeliveriesByEndpointId(endpoint.id); let resumed: ReturnType | undefined; return { ...context, @@ -29662,10 +29221,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toHaveLength(0); expect(context.wakeup).not.toHaveBeenCalled(); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)), + await readChatPublicationsByEndpointId(context.endpoint.id), ).toEqual([]); } finally { await context.close(); @@ -29743,10 +29299,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { before.principalId!, ); } else if (change === "generation") { - const [endpoint] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, context.endpoint.id)); + const [endpoint] = await readChatEndpointsById(context.endpoint.id); await db .update(chatEndpoints) .set({ @@ -29989,10 +29542,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "direct_message", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await db .update(issues) .set({ status: "in_progress", updatedAt: new Date() }) @@ -30058,10 +29608,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "Maya is working…", ]); const statusPublication = ( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)) + await readChatPublicationsByEndpointId(endpoint.id) ).find((publication) => publication.idempotencyKey.startsWith("control:status:"), ); @@ -30126,10 +29673,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect([...renderedTelegramMessages.values()]).toEqual([ "telegram-status-race-final", ]); - const publishedStatus = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, statusPublication!.id)) + const publishedStatus = await readChatPublicationsById(statusPublication!.id) .then((rows) => rows[0]); expect(publishedStatus).toMatchObject({ state: "published", @@ -30302,10 +29846,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "new-generation-final", ]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(2); }); @@ -30879,10 +30420,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { issueId: f.conversation.issueId, runId: f.runId, }); - const [publication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, final.id)); + const [publication] = await readChatPublicationsByCommentId(final.id); // A later final can be unconfirmed while the earlier close is the // FIFO head. The old progress link is not proof of remote absence. await db @@ -30974,15 +30512,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, { agentId: f.assignedAgentId, runId: f.runId }, ); - const [prompt] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${interaction.id}:${f.endpoint.id}`, - ), - ); + const [prompt] = await readChatPublicationsByIdempotencyKey(`interaction:${interaction.id}:${f.endpoint.id}`); expect(prompt).toMatchObject({ commentId: null, payload: { interactionId: interaction.id }, @@ -31004,26 +30534,17 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(f.providerRuntime.removedReactions).toHaveLength(beforeRemovals); expect( ( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, close.id)) + await readChatPublicationsById(close.id) )[0], ).toMatchObject({ state: "published" }); expect( ( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, prompt.id)) + await readChatPublicationsById(prompt.id) )[0]?.state, ).toBe(state === "delivery_unknown" ? "delivery_unknown" : "cancelled"); expect( ( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, f.runId)) + await readHeartbeatRunsById(f.runId) )[0]?.status, ).toBe("running"); } finally { @@ -31056,15 +30577,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, { agentId: f.assignedAgentId, runId: otherRunId }, ); - const [prompt] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${interaction.id}:${f.endpoint.id}`, - ), - ); + const [prompt] = await readChatPublicationsByIdempotencyKey(`interaction:${interaction.id}:${f.endpoint.id}`); expect(prompt).toBeDefined(); await db .update(chatPublications) @@ -31083,10 +30596,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); expect( ( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, prompt.id)) + await readChatPublicationsById(prompt.id) )[0]?.state, ).toBe("delivery_unknown"); } finally { @@ -31161,10 +30671,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(f.providerRuntime.edits).toHaveLength(1); expect(f.providerRuntime.edits[0]?.messageId).toBe(f.progressMessageId); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, consumerId)), + await readChatPublicationsById(consumerId), ).toEqual([ expect.objectContaining({ state: "delivery_unknown", attempts: 1 }), ]); @@ -31179,10 +30686,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "owner-user", ); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, consumerId)), + await readChatPublicationsById(consumerId), ).toEqual([ expect.objectContaining({ state: "cancelled", @@ -31212,16 +30716,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(f.providerRuntime.posts).toHaveLength(beforePosts + 1); expect(f.providerRuntime.posts.at(-1)?.text).toBe(close.payload.text); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, close.id)), + await readChatPublicationsById(close.id), ).toEqual([expect.objectContaining({ state: "published" })]); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, f.runId)), + await readHeartbeatRunsById(f.runId), ).toEqual([expect.objectContaining({ status: "running" })]); } finally { threadSpy.mockRestore(); @@ -31258,10 +30756,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { beforePosts + (mayEdit ? 0 : 1), ); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, close.id)), + await readChatPublicationsById(close.id), ).toEqual([expect.objectContaining({ state: "published" })]); } finally { await retirePublicationFixture(f.service, f.endpoint.id); @@ -31548,10 +31043,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (provider === "github") { // This case exercises tool-owned replies; legacy coverage below keeps // the generic final publication behavior without a saved configuration. @@ -31729,10 +31221,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const firstConversation = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, first.endpoint.id)) + const firstConversation = await readChatConversationsByEndpointId(first.endpoint.id) .then((rows) => rows[0]!); await db.insert(chatPublications).values({ companyId: firstFixture.companyId, @@ -31783,10 +31272,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const secondConversation = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, second.endpoint.id)) + const secondConversation = await readChatConversationsByEndpointId(second.endpoint.id) .then((rows) => rows[0]!); await db.insert(chatPublications).values({ companyId: secondFixture.companyId, @@ -31879,10 +31365,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const conversation = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)) + const conversation = await readChatConversationsByEndpointId(endpoint.id) .then((rows) => rows[0]!); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ @@ -32096,7 +31579,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, message: makeMessage({ id: providerMessageId, text, mentioned: index === 0 }), trigger: index === 0 ? "mention" : "subscribed_message" }); - const conversations = await db.select().from(chatConversations).where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(1); const conversation = conversations[0]!; expect(conversation.state).toBe("active"); @@ -32151,10 +31634,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -32323,10 +31803,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -32398,10 +31875,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram continuation conversation"); const [inboundLink] = await db @@ -32705,10 +32179,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [inboundLink] = await db .select({ commentId: chatMessageLinks.commentId }) .from(chatMessageLinks) @@ -32889,10 +32360,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const providerRuntime = runtime.endpoints.get(endpoint.id); if (!conversation || !providerRuntime) throw new Error("Expected publication lane conversation"); @@ -33055,7 +32523,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const [stale] = await db.insert(chatActions).values(action("receipt_reaction")).returning(); await registerChatActionWork(coordinator, f.service, () => true).ready; await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBeNull(), { timeout: 10_000 }); - expect((await db.select().from(chatActions).where(eq(chatActions.id, stale!.id)))[0]?.status).toBe("failed"); + expect((await readChatActionsById(stale!.id))[0]?.status).toBe("failed"); const scans = vi.spyOn(db, "select"); try { await new Promise(resolve => setTimeout(resolve, 1200)); @@ -33192,7 +32660,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await vi.waitFor(() => expect(coordinator.nextWakeAt()).toBeNull(), { timeout: 10_000 }); } await vi.waitFor(async () => { - const [row] = await db.select().from(chatPublications).where(eq(chatPublications.id, publication.id)); + const [row] = await readChatPublicationsById(publication.id); expect(row!.state).toBe("cancelled"); expect(coordinator.nextWakeAt()).toBeNull(); }, { timeout: 10_000 }); @@ -33808,10 +33276,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const createdAt = new Date("2026-09-05T15:00:00.000Z"); const firstId = "00000000-0000-4000-8000-000000000001"; const secondId = "00000000-0000-4000-8000-000000000002"; @@ -33887,10 +33352,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); } - const conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); const blockedConversation = conversations.find( (conversation) => conversation.externalThreadId === blockedThread.thread.id, @@ -34269,10 +33731,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [storedEndpoint] = await db .select({ setup: chatEndpoints.setup }) .from(chatEndpoints) @@ -35382,10 +34841,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [publication] = await db .insert(chatPublications) .values({ @@ -35499,10 +34955,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, "U-CONTROL-CLAIM"); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const [principal] = await db .select() .from(chatExternalPrincipals) @@ -35675,10 +35128,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const resource = (await service.listResources(endpoint.id)).find( (candidate) => candidate.providerResourceId === "C-RESOURCE-OUTBOX", ); @@ -35881,10 +35331,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const publicParagraph = "External-safe result. ".repeat(220).trim(); const comment = await issueService(db).addComment( conversation.issueId, @@ -35894,10 +35341,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.publishComment(endpoint.id, conversation.id, comment.id); - const [publication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [publication] = await readChatPublicationsByCommentId(comment.id); expect(publication).toMatchObject({ state: "published", payload: { text: publicParagraph }, @@ -35972,10 +35416,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(imageFetch).toHaveBeenCalledTimes(1); expect(rejectedFetch).not.toHaveBeenCalled(); expect(storage.putFile).toHaveBeenCalledTimes(2); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", redactedError: "1 external attachment was omitted (unsupported type: 1)", @@ -36041,10 +35482,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!callbacks.onReaction) { throw new Error("Discord reaction callback was not registered"); } - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Discord conversation"); const commentCountBefore = await db .select({ id: issueComments.id }) @@ -36332,12 +35770,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!delivery) throw new Error("Expected Discord root delivery"); await service.processPendingDeliveries(25, delivery.id); expect(providerRuntime.reactions).toHaveLength(0); - const [addAction] = await db - .select() - .from(chatActions) - .where( - eq(chatActions.providerActionId, `receipt_reaction:${delivery.id}`), - ); + const [addAction] = await readChatActionsByProviderActionId(`receipt_reaction:${delivery.id}`); expect(addAction).toMatchObject({ status: "failed", result: { @@ -36377,15 +35810,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).resolves.toBe(1); await service.processPendingPublications(1_000); - const [failedPublication] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${runId}:failed:${endpoint.id}`, - ), - ); + const [failedPublication] = await readChatPublicationsByIdempotencyKey(`run:${runId}:failed:${endpoint.id}`); expect(failedPublication).toMatchObject({ state: "published", payload: { @@ -36418,15 +35843,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { for (const task of deferred.splice(0)) task(); let removal: typeof chatActions.$inferSelect | undefined; await vi.waitFor(async () => { - removal = await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `receipt_reaction_remove:${delivery.id}`, - ), - ) + removal = await readChatActionsByProviderActionId(`receipt_reaction_remove:${delivery.id}`) .then((rows) => rows[0]); expect(removal?.status).toBe("failed"); }); @@ -36533,10 +35950,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .returning(); return ( created ?? - db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, continuationRunId)) + readHeartbeatRunsById(continuationRunId) .then((rows) => rows[0]) ); }, @@ -36883,10 +36297,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.processPendingPublications(1_000); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, continuationComment.id)), + readChatPublicationsByCommentId(continuationComment.id), ).resolves.toEqual([ expect.objectContaining({ endpointId: endpoint.id, @@ -37561,22 +36972,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const { endpoint } = configured; let { runtime, service } = configured; try { - const [current] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [current] = await readChatEndpointsById(endpoint.id); await db .update(chatEndpoints) .set({ capabilities: { ...current!.capabilities, modals: false } }) .where(eq(chatEndpoints.id, endpoint.id)); - const [before] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); - const [connectionBefore] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, before!.connectionId)); + const [before] = await readChatEndpointsById(endpoint.id); + const [connectionBefore] = await readToolConnectionsById(before!.connectionId); const resourcesBefore = await db .select() .from(chatEndpointResources) @@ -37630,10 +37032,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.reconcileProviderRuntimes(); if (mode !== "warm" && mode !== "cold") expect(targetInitializations).toBe(1); - const [after] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [after] = await readChatEndpointsById(endpoint.id); expect(after!.capabilities.modals).toBe( mode === "warm" || mode === "cold", ); @@ -37643,10 +37042,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { capabilities: before!.capabilities, ...(mode === "stale generation" ? { setup: before!.setup } : {}), }).toEqual(before); - const [connectionAfter] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, before!.connectionId)); + const [connectionAfter] = await readToolConnectionsById(before!.connectionId); expect({ ...connectionAfter, updatedAt: connectionBefore!.updatedAt, @@ -37707,10 +37103,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }; signal.addEventListener("abort", abort, { once: true }); try { - const [before] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [before] = await readChatEndpointsById(endpoint.id); await db .update(chatEndpoints) .set({ capabilities: { ...before!.capabilities, modals: false } }) @@ -37818,10 +37211,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } }); await reconciliation; - const [after] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [after] = await readChatEndpointsById(endpoint.id); expect(after!.capabilities.modals).toBe(false); expect({ ...after, @@ -38056,10 +37446,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { comment.id, ); expect(blocked).toMatchObject({ state: "retry" }); - const [afterFailure] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [afterFailure] = await readChatPublicationsByCommentId(comment.id); expect(afterFailure).toMatchObject({ state: "retry", attempts: 1, @@ -38081,10 +37468,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatPublications.id, afterFailure.id)); await service.processPendingPublications(); - const [completed] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, afterFailure.id)); + const [completed] = await readChatPublicationsById(afterFailure.id); expect(completed).toMatchObject({ state: "published", attempts: 2 }); expect(providerRuntime.posts).toHaveLength(1); const delivered = providerRuntime.posts[0]!; @@ -38613,10 +37997,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { comment.id, ); expect(publication).toMatchObject({ state: "published", attempts: 1 }); - const rows = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const rows = await readChatPublicationsByCommentId(comment.id); expect(rows).toHaveLength(1); expect(rows[0]?.payload.transportPart).toBeUndefined(); expect(providerRuntime.posts).toHaveLength(1); @@ -38684,10 +38065,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { comment.id, ); expect(blocked).toMatchObject({ state: "retry" }); - const afterFailure = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)) + const afterFailure = await readChatPublicationsByCommentId(comment.id) .orderBy( sql`(${chatPublications.payload}->'transportPart'->>'index')::int`, ); @@ -38711,10 +38089,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatPublications.id, blocked.id)); await service.processPendingPublications(); - const completed = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)) + const completed = await readChatPublicationsByCommentId(comment.id) .orderBy( sql`(${chatPublications.payload}->'transportPart'->>'index')::int`, ); @@ -38799,10 +38174,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { comment.id, ); expect(blocked).toMatchObject({ state: "retry" }); - const [afterFailure] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [afterFailure] = await readChatPublicationsByCommentId(comment.id); expect(afterFailure).toMatchObject({ state: "retry", attempts: 1, @@ -38824,10 +38196,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatPublications.id, afterFailure.id)); await service.processPendingPublications(); - const [completed] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, afterFailure.id)); + const [completed] = await readChatPublicationsById(afterFailure.id); expect(completed).toMatchObject({ state: "published", attempts: 2 }); expect(providerRuntime.posts).toHaveLength(1); expect(providerRuntime.posts[0]?.text).toBe( @@ -39025,10 +38394,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Safe provider response", @@ -39041,10 +38407,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const beforeAttempt = Date.now(); await service.publishComment(endpoint.id, conversation.id, comment.id); - const [publication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [publication] = await readChatPublicationsByCommentId(comment.id); expect(publication).toMatchObject({ state: expectedPublicationState, attempts: 1, @@ -39060,10 +38423,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect(service.get(endpoint.id)).resolves.toMatchObject({ status: expectedEndpointStatus, }); - const [storedConversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, conversation.id)); + const [storedConversation] = await readChatConversationsById(conversation.id); expect(storedConversation.state).toBe(expectedConversationState); const [resource] = await db .select() @@ -39072,10 +38432,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(resource.availability).toBe(expectedResourceAvailability); if (expectedEndpointStatus === "attention") { expect(runtime.endpoints.has(endpoint.id)).toBe(false); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [connection] = await readToolConnectionsById(endpoint.connectionId); expect(connection).toMatchObject({ status: "disabled", enabled: false, @@ -39181,10 +38538,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: "active", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Safe Telegram provider response", @@ -39196,10 +38550,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { providerRuntime.postError = error; await service.publishComment(endpoint.id, conversation.id, comment.id); - const [publication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [publication] = await readChatPublicationsByCommentId(comment.id); expect(publication).toMatchObject({ state: expectedPublicationState, attempts: 1, @@ -39273,10 +38624,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Safe response during membership recovery", @@ -39430,10 +38778,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Safe response during credential reconnect", @@ -39557,10 +38902,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .returning(); return ( created ?? - db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, continuationRunId)) + readHeartbeatRunsById(continuationRunId) .then((rows) => rows[0]) ); }, @@ -39587,10 +38929,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const sourceRunId = randomUUID(); await db.insert(heartbeatRuns).values({ id: sourceRunId, @@ -40096,10 +39435,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.processPendingPublications(); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, continuationComment.id)), + readChatPublicationsByCommentId(continuationComment.id), ).resolves.toEqual([ expect.objectContaining({ conversationId: conversation.id, @@ -40171,10 +39507,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, unrelatedComment.id)), + readChatPublicationsByCommentId(unrelatedComment.id), ).resolves.toHaveLength(0); await db @@ -40191,10 +39524,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, inFlightNativeComment.id)), + readChatPublicationsByCommentId(inFlightNativeComment.id), ).resolves.toHaveLength(0); await db @@ -40211,10 +39541,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, resumedSourceComment.id)), + readChatPublicationsByCommentId(resumedSourceComment.id), ).resolves.toHaveLength(0); const steeredRunId = randomUUID(); @@ -40245,10 +39572,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { runId: steeredRunId, }); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, steeredComment.id)), + readChatPublicationsByCommentId(steeredComment.id), ).resolves.toEqual([ expect.objectContaining({ conversationId: conversation.id, @@ -40284,10 +39608,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, reassignedComment.id)), + readChatPublicationsByCommentId(reassignedComment.id), ).resolves.toHaveLength(0); const staleNoticeCount = channel.postEphemeral.mock.calls.filter( @@ -40999,10 +40320,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, externalUserId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const linkedUserId = `modal-paperclip-user-${randomUUID()}`; const now = new Date(); @@ -41840,15 +41158,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingPublications(); const providerRuntime = runtime.endpoints.get(endpoint.id); if (!providerRuntime) throw new Error("Expected Slack provider runtime"); - const workingPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${sourceRunId}:working:${endpoint.id}`, - ), - ) + const workingPublication = await readChatPublicationsByIdempotencyKey(`run:${sourceRunId}:working:${endpoint.id}`) .then((rows) => rows[0]); if (!workingPublication?.providerMessageId) { throw new Error("Expected Slack working publication"); @@ -41910,10 +41220,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, protocolWaitingSummary.id)), + readChatPublicationsByCommentId(protocolWaitingSummary.id), ).resolves.toHaveLength(0); const sourcePresentationAuthorization = @@ -41943,10 +41250,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, ]); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, internalPresentation.id)), + readChatPublicationsByCommentId(internalPresentation.id), ).resolves.toHaveLength(0); // External answer isolation cancels the parked source run before starting @@ -41987,15 +41291,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(heartbeatRuns.id, sourceRunId)); const postCountBeforePrompt = providerRuntime.posts.length; await service.processPendingPublications(); - const originalPrompt = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${interaction.id}:${endpoint.id}`, - ), - ) + const originalPrompt = await readChatPublicationsByIdempotencyKey(`interaction:${interaction.id}:${endpoint.id}`) .then((rows) => rows[0]); expect(originalPrompt).toMatchObject({ state: "published", @@ -42185,15 +41481,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingPublications(); const providerRuntime = runtime.endpoints.get(endpoint.id); if (!providerRuntime) throw new Error("Expected Telegram provider runtime"); - const queuedPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${sourceRunId}:queued:${endpoint.id}`, - ), - ) + const queuedPublication = await readChatPublicationsByIdempotencyKey(`run:${sourceRunId}:queued:${endpoint.id}`) .then((rows) => rows[0]); if (!queuedPublication?.providerMessageId) { throw new Error("Expected Telegram queued publication"); @@ -42237,10 +41525,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.id, internalPresentation.id)), ).resolves.toEqual([{ body: "Internal Telegram confirmation summary" }]); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, internalPresentation.id)), + readChatPublicationsByCommentId(internalPresentation.id), ).resolves.toHaveLength(0); await enqueueChatRunMilestones(db); await expect( @@ -42257,15 +41542,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const postCountBeforePrompt = providerRuntime.posts.length; await service.processPendingPublications(); - const promptPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${interaction.id}:${endpoint.id}`, - ), - ) + const promptPublication = await readChatPublicationsByIdempotencyKey(`interaction:${interaction.id}:${endpoint.id}`) .then((rows) => rows[0]); expect(promptPublication).toMatchObject({ state: "published", @@ -42649,10 +41926,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const linkedUserId = `telegram-question-user-${randomUUID()}`; const now = new Date(); @@ -43075,10 +42349,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(JSON.stringify(channel.post.mock.calls)).not.toContain( expiredCallbackData, ); - const [expiredToken] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, expired.token.id)); + const [expiredToken] = await readChatActionsById(expired.token.id); expect(expiredToken).toMatchObject({ status: "expired", result: { code: "question_action_token_expired" }, @@ -43891,15 +43162,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { agentId: fixture.assignedAgentId }, ); await service.processPendingPublications(); - const guardedPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${governed.id}:${endpoint.id}`, - ), - ) + const guardedPublication = await readChatPublicationsByIdempotencyKey(`interaction:${governed.id}:${endpoint.id}`) .then((rows) => rows[0]); expect( guardedPublication.payload.card?.actions?.some( @@ -43944,10 +43207,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const interaction = await issueThreadInteractionService(db).create( { id: conversation.issueId, companyId: fixture.companyId }, { @@ -44035,10 +43295,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const interaction = await issueThreadInteractionService(db).create( { id: conversation!.issueId, companyId: fixture.companyId }, { @@ -44136,10 +43393,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .returning(); return ( created ?? - db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, continuationRunId)) + readHeartbeatRunsById(continuationRunId) .then((rows) => rows[0]) ); }); @@ -44162,10 +43416,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected GitHub conversation"); // Exercise the upgraded reply model after admitting the legacy fixture's // root. Legacy connections deliberately continue publishing final prose. @@ -44211,15 +43462,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { agentId: fixture.assignedAgentId, runId: sourceRunId }, ); await service.processPendingPublications(); - const promptPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `interaction:${interaction.id}:${endpoint.id}`, - ), - ) + const promptPublication = await readChatPublicationsByIdempotencyKey(`interaction:${interaction.id}:${endpoint.id}`) .then((rows) => rows[0]); expect(promptPublication).toMatchObject({ state: "published", @@ -44260,15 +43503,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await enqueueChatRunMilestones(db); await service.processPendingPublications(); - const workingPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${continuationRunId}:working:${endpoint.id}`, - ), - ) + const workingPublication = await readChatPublicationsByIdempotencyKey(`run:${continuationRunId}:working:${endpoint.id}`) .then((rows) => rows[0]); expect(workingPublication).toBeUndefined(); @@ -44291,10 +43526,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.processPendingPublications(); - const finalPublication = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, finalComment.id)) + const finalPublication = await readChatPublicationsByCommentId(finalComment.id) .then((rows) => rows[0]); expect(finalPublication).toBeUndefined(); const providerRuntime = runtime.endpoints.get(endpoint.id); @@ -44340,10 +43572,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected GitHub conversation"); const sourceCommentId = await db .select({ commentId: chatMessageLinks.commentId }) @@ -44370,10 +43599,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.processPendingPublications(); wakeup.mockClear(); - const issue = await db - .select() - .from(issues) - .where(eq(issues.id, conversation.issueId)) + const issue = await readIssuesById(conversation.issueId) .then((rows) => rows[0]); if (!issue) throw new Error("Expected GitHub-backed issue"); await issueThreadInteractionService(db).acceptInteraction( @@ -44434,15 +43660,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).resolves.toBe(1); await service.processPendingPublications(); - const failedPublication = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${failedRunId}:failed:${endpoint.id}`, - ), - ) + const failedPublication = await readChatPublicationsByIdempotencyKey(`run:${failedRunId}:failed:${endpoint.id}`) .then((rows) => rows[0]); expect(failedPublication).toMatchObject({ conversationId: conversation.id, @@ -44498,10 +43716,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -44760,10 +43975,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, "owner-user", ); - const [firstConnection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [firstConnection] = await readToolConnectionsById(endpoint.connectionId); const firstIds = new Set( firstConnection.credentialSecretRefs.map((ref) => ref.secretId), ); @@ -44780,10 +43992,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, "owner-user", ); - const [rotatedConnection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [rotatedConnection] = await readToolConnectionsById(endpoint.connectionId); expect(rotatedConnection.credentialSecretRefs).toHaveLength(2); expect( rotatedConnection.credentialSecretRefs.every( @@ -44799,10 +44008,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.configure(endpoint.id, { action: "remove" }, "owner-user"); - const [removedConnection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [removedConnection] = await readToolConnectionsById(endpoint.connectionId); expect(removedConnection.credentialSecretRefs).toEqual([]); expect( await db @@ -45013,10 +44219,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { healthMessage: "Provider setup needs attention", lastError: "Telegram could not register the webhook: webhook unavailable", }); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, endpoint.connectionId)); + const [connection] = await readToolConnectionsById(endpoint.connectionId); expect(connection).toMatchObject({ healthStatus: "degraded", healthMessage: "Provider setup failed", @@ -45109,10 +44312,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comment = await issueService(db).addComment( conversation.issueId, "Batch status fixture", @@ -45228,10 +44428,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const stored = await storage.storage.putFile({ companyId: fixture.companyId, namespace: `issues/${conversation.issueId}`, @@ -45277,10 +44474,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { eq(issueComments.body, "Visible board update"), ), ); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comments[0].id)) + const publications = await readChatPublicationsByCommentId(comments[0].id) .orderBy(asc(chatPublications.createdAt)); expect(comments).toHaveLength(1); expect(publications).toHaveLength(2); @@ -45391,10 +44585,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(context.service, context.endpoint.id); await context.service.test(context.endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, context.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(context.endpoint.id); const stored = await storage.storage.putFile({ companyId: company.companyId, namespace: `issues/${conversation.issueId}`, @@ -45749,10 +44940,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); if (!conversation) throw new Error("Expected admitted active Teams conversation"); - [issue] = await db - .select() - .from(issues) - .where(eq(issues.id, conversation.issueId)); + [issue] = await readIssuesById(conversation.issueId); await expect(context.service.get(endpoint.id)).resolves.toMatchObject({ status: "active", }); @@ -45854,10 +45042,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .from(chatTeamsFileTransfers) .where(eq(chatTeamsFileTransfers.companyId, company.companyId)) .orderBy(asc(chatTeamsFileTransfers.id)), - comments: await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, company.companyId)) + comments: await readIssueCommentsByCompanyId(company.companyId) .orderBy(asc(issueComments.id)), actions: await db .select() @@ -46390,10 +45575,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const [part] = await fixture.batch([ { state: "pending", transfer: { phase: "consent_pending" } }, ]); - const [endpointBefore] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, fixture.endpoint.id)); + const [endpointBefore] = await readChatEndpointsById(fixture.endpoint.id); expect(endpointBefore?.status).toBe(status); const before = await fixture.snapshot(); await expect( @@ -46404,10 +45586,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).resolves.toBe(0); expect(await fixture.snapshot()).toEqual(before); await expect( - db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, fixture.endpoint.id)), + readChatEndpointsById(fixture.endpoint.id), ).resolves.toEqual([endpointBefore]); expect(fixture.runtime.replaceCount).toBe(0); // Also checks zero provider requests, runtime creation, wakeups, and @@ -46460,10 +45639,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: "active", setup: { step: "complete" }, }); - const [connection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, fixture.endpoint.connectionId)); + const [connection] = await readToolConnectionsById(fixture.endpoint.connectionId); expect(connection).toMatchObject({ status: "active", enabled: true, @@ -46582,10 +45758,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!blocked.commentId) throw new Error("Expected durable batch comment"); const readBatch = () => - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, blocked.commentId!)) + readChatPublicationsByCommentId(blocked.commentId!) .orderBy(asc(chatPublications.createdAt)); const initialBatch = await readBatch(); expect(initialBatch.map((row) => row.state)).toEqual([ @@ -47831,10 +47004,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); - const conversations = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(2); const firstConversation = conversations.find( (conversation) => @@ -48429,10 +47599,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ], })), ); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)); + const publications = await readChatPublicationsByEndpointId(endpoint.id); const filePublications = publications.filter( (row) => (row.payload.attachmentIds as unknown[] | undefined)?.length, ); @@ -48483,10 +47650,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -48542,10 +47706,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!endpointRuntime) throw new Error("Expected Slack endpoint runtime"); endpointRuntime.postResultIds.push("1788.301", "1788.302"); await service.processPendingPublications(); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, response.id)) + const publications = await readChatPublicationsByCommentId(response.id) .orderBy(asc(chatPublications.createdAt)); expect(publications).toEqual([ expect.objectContaining({ @@ -48662,10 +47823,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Discord conversation"); const insertRun = async (agentId: string, status = "succeeded") => { @@ -49115,10 +48273,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Discord conversation"); const runId = randomUUID(); @@ -49445,10 +48600,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); } - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const inboundLinks = await db .select() .from(chatMessageLinks) @@ -49602,15 +48754,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toBeUndefined(); } expect( - await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `attachment:${internalAttachment.id}:${endpoint.id}`, - ), - ), + await readChatPublicationsByIdempotencyKey(`attachment:${internalAttachment.id}:${endpoint.id}`), ).toHaveLength(0); await service.processPendingPublications(1_000); @@ -49631,15 +48775,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { createdByRunId: secondRunId, }); await expect( - db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `attachment:${chatAttachment.id}:${endpoint.id}`, - ), - ), + readChatPublicationsByIdempotencyKey(`attachment:${chatAttachment.id}:${endpoint.id}`), ).resolves.toEqual([ expect.objectContaining({ commentId: secondResponse.id, @@ -49686,10 +48822,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const firstComment = await issueService(db).addComment( conversation.issueId, "First safe response", @@ -49707,10 +48840,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { providerRuntime.postError = new Error("socket reset after write"); await service.publishComment(endpoint.id, conversation.id, firstComment.id); - const firstPublication = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, firstComment.id)) + const firstPublication = await readChatPublicationsByCommentId(firstComment.id) .then((rows) => rows[0]); expect(firstPublication).toMatchObject({ state: "delivery_unknown", @@ -49723,10 +48853,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { conversation.id, secondComment.id, ); - const secondPublication = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, secondComment.id)) + const secondPublication = await readChatPublicationsByCommentId(secondComment.id) .then((rows) => rows[0]); expect(secondPublication.state).toBe("pending"); const activity = await service.listActivity(endpoint.id); @@ -49753,15 +48880,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "owner-user", ); await service.processPendingPublications(); - const replayed = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, firstPublication.id)) + const replayed = await readChatPublicationsById(firstPublication.id) .then((rows) => rows[0]); - const releasedSecond = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, secondPublication.id)) + const releasedSecond = await readChatPublicationsById(secondPublication.id) .then((rows) => rows[0]); expect(replayed.state).toBe("published"); expect(releasedSecond.state).toBe("published"); @@ -49800,10 +48921,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); await db .update(chatDeliveries) .set({ state: "failed", redactedError: "temporary failure" }) @@ -49819,10 +48937,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { wakeCommentId: expect.any(String), }, }); - const [replayed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery.id)); + const [replayed] = await readChatDeliveriesById(delivery.id); expect(replayed).toMatchObject({ state: "processed", attempts: 2, @@ -50384,10 +49499,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(cold.wakeup).not.toHaveBeenCalled(); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("active"); } finally { @@ -50418,10 +49530,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect((await f.publications())[0]?.state).toBe("published"); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("completed"); } finally { @@ -50467,10 +49576,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(publication.idempotencyKey).toMatch(/^control:close:/); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("active"); expect(await f.actions()).toHaveLength(3); @@ -50481,10 +49587,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("completed"); // Fresh adapter removes only process-local ACK suppression. The real @@ -50535,10 +49638,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); const before = await f.actions(); expect(before).toHaveLength(1); - const current = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, f.endpoint.id)) + const current = await readChatEndpointsById(f.endpoint.id) .then((rows) => rows[0]!); if (reason === "missing owner") await db @@ -50816,10 +49916,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await f.publications()).toHaveLength(1); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, conversations[1]!.id)) + await readChatConversationsById(conversations[1]!.id) )[0]?.state, ).toBe("active"); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount); @@ -50840,10 +49937,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await f.publications()).toHaveLength(1); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, conversations[1]!.id)) + await readChatConversationsById(conversations[1]!.id) )[0]?.state, ).toBe("active"); expect(f.wakeup).toHaveBeenCalledTimes(wakeCount); @@ -50861,10 +49955,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { let blocker: Promise | undefined; let delivery: Promise | undefined; try { - const current = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, f.endpoint.id)) + const current = await readChatEndpointsById(f.endpoint.id) .then((rows) => rows[0]!); let locked = () => {}; const acquired = new Promise((resolve) => { @@ -50931,10 +50022,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await f.publications()).toHaveLength(0); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("active"); } finally { @@ -51149,10 +50237,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { body: "DISCORD-FRESH-AFTER-CLOSE", }); await f.service.processPendingPublications(); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const publications = await readChatPublicationsByCommentId(comment.id); expect(publications).toEqual([ expect.objectContaining({ conversationId: f.conversation.id, @@ -51287,15 +50372,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (mode !== "only_unproven") { expect( ( - await db - .select() - .from(chatActions) - .where( - eq( - chatActions.providerActionId, - `task-control-authorization:${f.control.id}`, - ), - ) + await readChatActionsByProviderActionId(`task-control-authorization:${f.control.id}`) )[0]?.status, ).toBe("processed"); } @@ -51779,10 +50856,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await f.publications()).toHaveLength(0); expect( ( - await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, f.conversation.id)) + await readChatConversationsById(f.conversation.id) )[0]?.state, ).toBe("active"); } finally { @@ -51964,10 +51038,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { externalThreadId: starterThreadId, state: "active", }); - const comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)); + const comments = await readIssueCommentsByIssueId(conversation.issueId); expect(comments.map((comment) => comment.body)).toEqual([ "investigate the command path", ]); @@ -53605,10 +52676,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); }); @@ -53766,10 +52834,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatActions.id, taskStart.id)); expect(action?.status).toBe("processed"); }); - const [stagedDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [stagedDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(stagedDelivery).toMatchObject({ state: "received" }); if (!stagedDelivery) throw new Error("Expected the durable Slack starter receipt"); @@ -53914,10 +52979,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }; const controlPostOffset = runtime.endpoints.get(endpoint.id)?.posts.length ?? 0; - const issuesBeforeStatus = await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)); + const issuesBeforeStatus = await readIssuesByCompanyId(fixture.companyId); const firstIssue = issuesBeforeStatus.find( (issue) => issue.title === "Start the first DM task", ); @@ -53934,10 +52996,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); await invokeControl("status"); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(issuesBeforeStatus.length); await service.processPendingPublications(); expect(runtime.endpoints.get(endpoint.id)?.posts.at(-1)?.text).toMatch( @@ -53946,10 +53005,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await invokeControl("new"); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(issuesBeforeStatus.length); await expect(service.listConversations(endpoint.id)).resolves.toEqual([ expect.objectContaining({ state: "active" }), @@ -53973,17 +53029,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "direct_message", }); - const issuesBeforeClose = await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)); + const issuesBeforeClose = await readIssuesByCompanyId(fixture.companyId); expect(issuesBeforeClose).toHaveLength(issuesBeforeStatus.length + 1); await invokeControl("close"); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(issuesBeforeClose.length); expect( (await service.listConversations(endpoint.id)).map( @@ -54051,10 +53101,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); expect( ( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)) + await readChatPublicationsByEndpointId(endpoint.id) .orderBy(asc(chatPublications.createdAt), asc(chatPublications.id)) ) .filter((publication) => @@ -54067,10 +53114,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // Each slash control asserted above that it added no reaction. The second // real DM task message is still free to receive its normal acknowledgement. const controlDeliveries = ( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + await readChatDeliveriesByEndpointId(endpoint.id) ).filter((delivery) => { const normalized = delivery.normalizedEvent as { message?: { text?: unknown }; @@ -54230,10 +53274,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } expect(await service.listConversations(endpoint.id)).toEqual([]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(0); // The ephemeral post is observable before its provider_effect row // settles, so under suite load the third row can still be @@ -54318,10 +53359,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(wakeup).not.toHaveBeenCalled(); expect(await service.listConversations(endpoint.id)).toEqual([]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(0); expect( await db @@ -54337,10 +53375,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect.objectContaining({ kind: "provider_effect", status: "processed" }), expect.objectContaining({ kind: "provider_effect", status: "processed" }), ]); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries).toHaveLength(2); expect(deliveries).toEqual( expect.arrayContaining([ @@ -54671,10 +53706,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, "71112233"); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected a bound conversation"); const providerRuntime = runtime.endpoints.get(endpoint.id); if (!providerRuntime) throw new Error("Expected Telegram runtime"); @@ -55124,10 +54156,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await callbacks.onSlashCommand(slashEvent); expect(runtime.endpoints.get(endpoint.id)?.posts).toEqual([]); - const publicationsBeforeDrain = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)); + const publicationsBeforeDrain = await readChatPublicationsByEndpointId(endpoint.id); expect(publicationsBeforeDrain).toEqual([ expect.objectContaining({ state: "pending", @@ -55151,16 +54180,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); expect(wakeup).toHaveBeenCalledTimes(1); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)), + await readIssueCommentsByCompanyId(fixture.companyId), ).toHaveLength(1); }); @@ -55353,10 +54376,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { apiCalls.filter(({ method }) => method === "sendMessage"), ).toHaveLength(2); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries).toHaveLength(3); for (const messageId of [601, 602]) { expect(deliveries).toContainEqual( @@ -55385,10 +54405,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); expect(await service.listConversations(endpoint.id)).toEqual([]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(0); expect(wakeup).not.toHaveBeenCalled(); } finally { @@ -55506,10 +54523,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); expect( await db @@ -55574,17 +54588,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect.objectContaining({ state: "active", sessionGeneration: 1 }), ]); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toHaveLength(1); expect( ( - await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversations[0]!.issueId)) + await readIssueCommentsByIssueId(conversations[0]!.issueId) ).map((comment) => comment.body), ).toEqual( expect.arrayContaining([ @@ -56223,10 +55231,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } as Message, }); - const deliveries = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const deliveries = await readChatDeliveriesByEndpointId(endpoint.id); expect(deliveries.map((delivery) => delivery.eventKind).sort()).toEqual([ "mention", "message", @@ -56245,14 +55250,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { redactedError: "Message edit arrived after the provider message was deleted", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); - const comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); + const comments = await readIssueCommentsByIssueId(conversation.issueId); expect( comments .filter( @@ -56300,10 +55299,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!callbacks.onMessageUpdated || !callbacks.onReaction) { throw new Error("Slack lifecycle callbacks were not registered"); } - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const commentCount = await db .select({ id: issueComments.id }) .from(issueComments) @@ -57481,10 +56477,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .from(chatDeliveries) .where(eq(chatDeliveries.id, lifecycle.id)), ).resolves.toEqual([{ attempts: 1, state: "retry" }]); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, first.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(first.endpoint.id); await expect( db .select() @@ -57602,10 +56595,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { generation: expect.any(Number), }, }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await db .delete(issueComments) .where( @@ -57802,10 +56792,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "direct_message", }); - const [originalDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [originalDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(originalDelivery).toMatchObject({ state: "received" }); const sendEdit = (updateId: number, text: string) => @@ -57909,10 +56896,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ]), ); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await expect( db .select({ body: issueComments.body }) @@ -57997,10 +56981,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ]); } await expect( - db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)), + readChatConversationsByEndpointId(endpoint.id), ).resolves.toHaveLength(0); expect(wakeup).not.toHaveBeenCalled(); }); @@ -58248,10 +57229,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ).resolves.toMatchObject({ ok: true }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const editComments = await db .select({ body: issueComments.body }) .from(issueComments) @@ -58417,10 +57395,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { timestamp: "2026-09-06T14:02:45.000Z", }); const latestLifecycle = ( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + await readChatDeliveriesByEndpointId(endpoint.id) ).filter((row) => !priorLifecycleIds.has(row.id)); expect(latestLifecycle).toHaveLength(2); expect(latestLifecycle.every((row) => row.state === "filtered")).toBe(true); @@ -58440,10 +57415,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { text: "OLDER_RESTORE_MUST_NOT_PERSIST", }); const afterDisabledDelete = ( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)) + await readChatDeliveriesByEndpointId(endpoint.id) ).filter((row) => !priorLifecycleIds.has(row.id)); expect( afterDisabledDelete.filter((row) => row.state === "processed"), @@ -58565,14 +57537,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); - const comments = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); + const comments = await readIssueCommentsByIssueId(conversation.issueId); expect( comments .filter((comment) => @@ -58652,10 +57618,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const principal = await db .select() .from(chatExternalPrincipals) @@ -58789,10 +57752,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ @@ -58835,10 +57795,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(JSON.stringify(providerRuntime?.edits)).not.toContain( "Private runner", ); - const [retainedRun] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, runId)); + const [retainedRun] = await readHeartbeatRunsById(runId); expect(retainedRun).toMatchObject({ status: "running", finishedAt: null }); }); @@ -58869,10 +57826,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ @@ -58930,10 +57884,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(JSON.stringify(providerRuntime.edits)).not.toContain( "Private diagnostic", ); - const [retained] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, stale!.id)); + const [retained] = await readChatPublicationsById(stale!.id); expect(retained).toMatchObject({ state: "cancelled", attempts: 0 }); }, ); @@ -58963,10 +57914,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const runId = randomUUID(); @@ -59001,10 +57949,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { authorType: "agent", authorizationReason: "internal_agent_write" }, ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, internalSummary.id)), + readChatPublicationsByCommentId(internalSummary.id), ).resolves.toHaveLength(0); const internalAttachmentBody = Buffer.from("internal attachment"); await issueService(db).createAttachment({ @@ -59020,10 +57965,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { createdByRunId: runId, }); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, internalSummary.id)), + readChatPublicationsByCommentId(internalSummary.id), ).resolves.toHaveLength(0); await db @@ -59102,10 +58044,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const runId = randomUUID(); @@ -59143,10 +58082,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); await service.processPendingPublications(); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)), + readChatPublicationsByCommentId(comment.id), ).resolves.toHaveLength(publishesComment ? 1 : 0); await db @@ -59210,15 +58146,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ).resolves.toHaveLength(publishesComment ? 0 : 1); await expect( - db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${laterQueuedRunId}:queued:${endpoint.id}`, - ), - ), + readChatPublicationsByIdempotencyKey(`run:${laterQueuedRunId}:queued:${endpoint.id}`), ).resolves.toHaveLength(1); }, ); @@ -59398,10 +58326,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await context.resolve().expect(200); expect(context.genericWake).not.toHaveBeenCalled(); expect(context.wakeup).toHaveBeenCalledTimes(1); - const receipts = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, retry.id)); + const receipts = await readAgentWakeupRequestsById(retry.id); expect(receipts).toHaveLength(1); expect(receipts[0]).toMatchObject({ status: "queued", @@ -59453,10 +58378,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(context.wakeup).not.toHaveBeenCalled(); expect(context.genericWake).not.toHaveBeenCalled(); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, retry!.id)), + await readAgentWakeupRequestsById(retry!.id), ).toEqual([]); expect( ( @@ -59521,10 +58443,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toEqual(context.action); expect( ( - await db - .select() - .from(issues) - .where(eq(issues.id, context.issue.id)) + await readIssuesById(context.issue.id) )[0]?.status, ).toBe("blocked"); expect( @@ -59565,10 +58484,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { githubUnavailableFile, false, ); - const [issue] = await db - .select() - .from(issues) - .where(eq(issues.id, context.conversation.issueId)); + const [issue] = await readIssuesById(context.conversation.issueId); const [action] = await db .select() .from(chatActions) @@ -59578,10 +58494,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { eq(chatActions.kind, "inbound_wakeup"), ), ); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, action.id)); + const [receipt] = await readAgentWakeupRequestsById(action.id); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -59987,10 +58900,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { initiatedByUserId: "owner-user", }), ); - const [original] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, runId)); + const [original] = await readHeartbeatRunsById(runId); if ( mode === "settled" || mode === "distinct_account" || @@ -60003,19 +58913,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect( context.service.processFailedChatRunRetry(intent.actionId), ).resolves.toMatchObject({ status: "queued" }); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, intent.actionId)); + const [receipt] = await readAgentWakeupRequestsById(intent.actionId); expect(receipt.payload).toMatchObject({ retryOfRunId: runId, wakeCommentIds: [action.payload.commentId], }); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, runId)), + await readHeartbeatRunsById(runId), ).toEqual([original]); expect( ( @@ -60307,10 +59211,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect( context.service.processFailedChatRunRetry(staged.actionId), ).resolves.toMatchObject({ status: "queued" }); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)); + const [receipt] = await readAgentWakeupRequestsById(staged.actionId); expect(receipt.payload).toMatchObject({ retryOfRunId: context.runId, wakeCommentIds: [context.action.payload.commentId], @@ -60336,10 +59237,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { async (provider) => { const context = await failedChatRetryFixture(provider); try { - const sourceBefore = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, context.action.id)); + const sourceBefore = await readChatActionsById(context.action.id); const stage = () => db.transaction((tx) => context.service.prepareFailedChatRunRetry(tx, { @@ -60355,10 +59253,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(first.actionId).not.toBe(context.action.id); await context.service.processFailedChatRunRetry(first.actionId); await context.service.processFailedChatRunRetry(first.actionId); - const [retry] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, first.actionId)); + const [retry] = await readAgentWakeupRequestsById(first.actionId); expect(retry).toMatchObject({ companyId: context.fixture.companyId, agentId: context.fixture.assignedAgentId, @@ -60372,10 +59267,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, }); expect( - await db - .select() - .from(chatActions) - .where(eq(chatActions.id, context.action.id)), + await readChatActionsById(context.action.id), ).toEqual(sourceBefore); expect( await db @@ -60416,10 +59308,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.service.processFailedChatRunRetry(staged.actionId), ).resolves.toMatchObject({ status: "failed", runId: null }); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toHaveLength(0); } finally { await context.service.shutdown(); @@ -60478,10 +59367,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { eq(issueComments.id, String(context.action.payload.commentId)), ); if (change === "deleted") { - const [original] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, context.action.deliveryId!)); + const [original] = await readChatDeliveriesById(context.action.deliveryId!); await db.insert(chatDeliveries).values({ companyId: context.fixture.companyId, endpointId: context.endpoint.id, @@ -60533,10 +59419,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.service.processFailedChatRunRetry(staged.actionId), ).resolves.toMatchObject({ status: "failed", runId: null }); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toEqual([]); } finally { await context.service.shutdown(); @@ -60567,10 +59450,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { initiatedByUserId: "owner-user", }), ); - const [original] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, context.action.deliveryId!)); + const [original] = await readChatDeliveriesById(context.action.deliveryId!); const fence = original.normalizedEvent.runtimeContext as Record< string, unknown @@ -60610,10 +59490,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { runId: null, }); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toHaveLength(denied ? 0 : 1); } finally { await context.service.shutdown(); @@ -60744,10 +59621,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(heartbeatRuns.id, context.runId)); const staged = await stage(); await context.service.processFailedChatRunRetry(staged.actionId); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)); + const [receipt] = await readAgentWakeupRequestsById(staged.actionId); expect(receipt.payload).toMatchObject({ wakeCommentIds: commentIds, wakeCommentId: commentIds[1], @@ -60778,20 +59652,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(agentWakeupRequests.id, staged.actionId)); await context.service.enqueueInboundWakeupPublications(100); await context.service.processPendingPublications(100); - const [queued] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `wake:${staged.actionId}:queued:${context.endpoint.id}:${context.conversation.id}`, - ), - ); + const [queued] = await readChatPublicationsByIdempotencyKey(`wake:${staged.actionId}:queued:${context.endpoint.id}:${context.conversation.id}`); expect(queued).toMatchObject({ state: "published", attempts: 1 }); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)); + const [receipt] = await readAgentWakeupRequestsById(staged.actionId); const retryRunId = randomUUID(); await db.insert(heartbeatRuns).values({ id: retryRunId, @@ -60835,10 +59698,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { body: "EXACT-RETRY-ANSWER", }); await context.service.processPendingPublications(100); - const [finalPublication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, final.id)); + const [finalPublication] = await readChatPublicationsByCommentId(final.id); expect(finalPublication).toMatchObject({ state: "published", attempts: 1, @@ -60874,10 +59734,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.service.processFailedChatRunRetry(staged.actionId), ).resolves.toMatchObject({ status: "queued", runId: null }); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toHaveLength(0); await db .update(chatActions) @@ -60910,16 +59767,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await context.service.processFailedChatRunRetry(staged.actionId); expect(context.wakeup).toHaveBeenCalledTimes(2); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toHaveLength(1); expect( - await db - .select() - .from(chatActions) - .where(eq(chatActions.id, staged.actionId)), + await readChatActionsById(staged.actionId), ).toEqual([ expect.objectContaining({ status: "processed", @@ -60945,10 +59796,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ); await context.service.processFailedChatRunRetry(staged.actionId); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)); + const [receipt] = await readAgentWakeupRequestsById(staged.actionId); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, @@ -60994,10 +59842,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { spy.mockRestore(); spy = undefined; expect(injected).toBe(true); - const [pending] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, final.id)); + const [pending] = await readChatPublicationsByCommentId(final.id); expect(pending.state).toBe("retry"); expect(context.providerRuntime.posts).toHaveLength(0); await db @@ -61006,10 +59851,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatPublications.id, pending.id)); await context.service.processPendingPublications(100); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, pending.id)), + await readChatPublicationsById(pending.id), ).toEqual([expect.objectContaining({ state: "published" })]); expect(context.providerRuntime.posts).toHaveLength(1); } finally { @@ -61032,10 +59874,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.service.prepareFailedChatRunRetry(tx, input), ); await context.service.processFailedChatRunRetry(first.actionId); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, first.actionId)); + const [receipt] = await readAgentWakeupRequestsById(first.actionId); const failedRetryRunId = randomUUID(); await db.insert(heartbeatRuns).values({ id: failedRetryRunId, @@ -61052,10 +59891,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .update(agentWakeupRequests) .set({ runId: failedRetryRunId, status: "failed" }) .where(eq(agentWakeupRequests.id, receipt.id)); - const [ancestor] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, first.actionId)); + const [ancestor] = await readChatActionsById(first.actionId); const cyclicContext = { ...receipt.payload, retryOfRunId: failedRetryRunId, @@ -61101,20 +59937,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); expect(second.actionId).not.toBe(first.actionId); await context.service.processFailedChatRunRetry(second.actionId); - const [secondReceipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, second.actionId)); + const [secondReceipt] = await readAgentWakeupRequestsById(second.actionId); expect(secondReceipt.payload).toMatchObject({ retryOfRunId: failedRetryRunId, taskKey: context.issue.identifier, wakeCommentIds: [context.action.payload.commentId], }); expect( - await db - .select() - .from(chatActions) - .where(eq(chatActions.id, first.actionId)), + await readChatActionsById(first.actionId), ).toEqual([ancestor]); } finally { await context.service.shutdown(); @@ -61155,14 +59985,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { it("retries an exact GitHub source denied by pre-provider reviewed attestation without rewriting its failure", async () => { const context = await failedReviewedGitHubRetryFixture(); try { - const [original] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, context.runId)); - const [originalSource] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, context.action.id)); + const [original] = await readHeartbeatRunsById(context.runId); + const [originalSource] = await readChatActionsById(context.action.id); const stage = () => db.transaction((tx) => context.service.prepareFailedChatRunRetry(tx, { @@ -61179,10 +60003,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.service.processFailedChatRunRetry(first.actionId), ).resolves.toMatchObject({ status: "queued" }); await context.service.processFailedChatRunRetry(first.actionId); - const receipts = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, first.actionId)); + const receipts = await readAgentWakeupRequestsById(first.actionId); expect(receipts).toEqual([ expect.objectContaining({ companyId: context.fixture.companyId, @@ -61198,16 +60019,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), ]); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, context.runId)), + await readHeartbeatRunsById(context.runId), ).toEqual([original]); expect( - await db - .select() - .from(chatActions) - .where(eq(chatActions.id, context.action.id)), + await readChatActionsById(context.action.id), ).toEqual([originalSource]); expect( await db @@ -61253,10 +60068,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).resolves.toMatchObject({ status: "failed", runId: null }); expect(context.wakeup.mock.calls.length).toBe(wakeCount); expect( - await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)), + await readAgentWakeupRequestsById(staged.actionId), ).toEqual([]); } finally { await context.service.shutdown(); @@ -61576,10 +60388,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await context.service.shutdown(); restarted = createService(context.runtime).service; await restarted.processFailedChatRunRetry(staged.actionId); - const [receipt] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, staged.actionId)); + const [receipt] = await readAgentWakeupRequestsById(staged.actionId); const authorize = (contextSnapshot: Record) => db.transaction((tx) => authorizeFailedChatRunRetryWake(db, tx as unknown as TestDb, { @@ -61756,10 +60565,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: sourceMessage, trigger: thread.thread.isDM ? "direct_message" : "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected safe-progress conversation"); const providerRuntime = runtime.endpoints.get(endpoint.id); if (!providerRuntime) throw new Error("Expected safe-progress runtime"); @@ -62113,15 +60919,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await safeNativeProgressFixture("telegram", "61"); try { const interrupted = await context.createRun("the original request"); - const [originalWorking] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${interrupted.runId}:working:${context.endpoint.id}`, - ), - ); + const [originalWorking] = await readChatPublicationsByIdempotencyKey(`run:${interrupted.runId}:working:${context.endpoint.id}`); expect(originalWorking?.providerMessageId).toBeTruthy(); await db .update(heartbeatRuns) @@ -62139,15 +60937,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // Model delayed milestone reconciliation after a successor has already // published its own working placeholder in the same conversation. const successor = await context.createRun("the successor request"); - const [successorWorking] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${successor.runId}:working:${context.endpoint.id}`, - ), - ); + const [successorWorking] = await readChatPublicationsByIdempotencyKey(`run:${successor.runId}:working:${context.endpoint.id}`); expect(successorWorking?.providerMessageId).toBeTruthy(); expect(successorWorking?.providerMessageId).not.toBe( originalWorking?.providerMessageId, @@ -62205,15 +60995,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { /PRIVATE|stdout|tool arguments|server_shutdown_interrupted/, ); await expect( - db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${interrupted.runId}:failed:${context.endpoint.id}`, - ), - ), + readChatPublicationsByIdempotencyKey(`run:${interrupted.runId}:failed:${context.endpoint.id}`), ).resolves.toEqual([ expect.objectContaining({ state: "published", @@ -62300,24 +61082,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { try { const original = await context.createRun("the original request"); const successor = await context.createRun("the successor request"); - const [originalWorking] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${original.runId}:working:${context.endpoint.id}`, - ), - ); - const [successorWorking] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${successor.runId}:working:${context.endpoint.id}`, - ), - ); + const [originalWorking] = await readChatPublicationsByIdempotencyKey(`run:${original.runId}:working:${context.endpoint.id}`); + const [successorWorking] = await readChatPublicationsByIdempotencyKey(`run:${successor.runId}:working:${context.endpoint.id}`); const addFinal = () => addSelectedChatFinal({ agentId: context.fixture.assignedAgentId, @@ -62345,19 +61111,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { final = await addFinal(); } await context.service.processPendingPublications(100); - const [answerPublication] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, final!.id)); - const [failurePublication] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${original.runId}:failed:${context.endpoint.id}`, - ), - ); + const [answerPublication] = await readChatPublicationsByCommentId(final!.id); + const [failurePublication] = await readChatPublicationsByIdempotencyKey(`run:${original.runId}:failed:${context.endpoint.id}`); expect(answerPublication?.state).toBe("published"); expect(failurePublication?.state).toBe("published"); expect(answerPublication?.providerMessageId).not.toBe( @@ -63085,10 +61840,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id, chatId); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected Telegram conversation"); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ @@ -63174,10 +61926,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await service.test(endpoint.id, "owner-user"); if (!callbacks.onReaction) throw new Error("Telegram reaction callback was not registered"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); await db .update(issues) .set({ status: "done", completedAt: new Date(), updatedAt: new Date() }) @@ -63206,16 +61955,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { state: "active", sessionGeneration: 2, }); - const commentCount = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)) + const commentCount = await readIssueCommentsByCompanyId(fixture.companyId) .then((rows) => rows.length); const wakeupCount = wakeup.mock.calls.length; - const runCount = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)) + const runCount = await readHeartbeatRunsByCompanyId(fixture.companyId) .then((rows) => rows.length); const emoji = { name: "thumbs_up", @@ -63283,18 +62026,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ).resolves.toHaveLength(0); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)) + await readIssueCommentsByCompanyId(fixture.companyId) .then((rows) => rows.length), ).toBe(commentCount); expect(wakeup).toHaveBeenCalledTimes(wakeupCount); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)) + await readHeartbeatRunsByCompanyId(fixture.companyId) .then((rows) => rows.length), ).toBe(runCount); }); @@ -63325,14 +62062,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!callbacks.onReaction) throw new Error("Slack reaction callback was not registered"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); - const commentCountBefore = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)) + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); + const commentCountBefore = await readIssueCommentsByIssueId(conversation.issueId) .then((rows) => rows.length); const wakeupCountBefore = wakeup.mock.calls.length; const emoji = { @@ -63377,10 +62108,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { reaction: { emoji: "thumbs_up", rawEmoji: "+1" }, }); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation.issueId)) + await readIssueCommentsByIssueId(conversation.issueId) .then((rows) => rows.length), ).toBe(commentCountBefore); expect(wakeup.mock.calls).toHaveLength(wakeupCountBefore); @@ -63635,10 +62363,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, stagedId)); const restarted = createService(); await restarted.service.processPendingDeliveries(1, stagedId); - const [replayed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, stagedId)); + const [replayed] = await readChatDeliveriesById(stagedId); expect(replayed).toMatchObject({ conversationId: conversation.id, state: "processed", @@ -63826,7 +62551,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { try { await restarted.service.processPendingDeliveries(1, stagedId); await expect( - db.select().from(chatDeliveries).where(eq(chatDeliveries.id, stagedId)), + readChatDeliveriesById(stagedId), ).resolves.toEqual([ expect.objectContaining({ conversationId: conversation.id, @@ -64170,10 +62895,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(outcome.error).toBe(sentinel); expect(await actionState()).toBe("processed"); await expect( - db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, reaction.id)), + readChatDeliveriesById(reaction.id), ).resolves.toEqual([ expect.objectContaining({ conversationId: conversation.id, @@ -64362,10 +63084,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await conversationLockReached; await Promise.all([settlement, replay]); - const [waitingForLink] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, staged.id)); + const [waitingForLink] = await readChatDeliveriesById(staged.id); expect(waitingForLink).toMatchObject({ conversationId: null, state: "retry", @@ -64377,10 +63096,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ nextAttemptAt: new Date(0) }) .where(eq(chatDeliveries.id, staged.id)); await service.processPendingDeliveries(1, staged.id); - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, staged.id)); + const [processed] = await readChatDeliveriesById(staged.id); expect(processed).toMatchObject({ conversationId: conversation.id, state: "processed", @@ -64473,10 +63189,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, stagedId)); await callbacks.onReaction(reaction); - const [expired] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, stagedId)); + const [expired] = await readChatDeliveriesById(stagedId); expect(expired).toMatchObject({ conversationId: null, state: "filtered", @@ -64584,10 +63297,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } finally { vi.useRealTimers(); } - const [filtered] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, stagedId)); + const [filtered] = await readChatDeliveriesById(stagedId); expect(filtered).toMatchObject({ conversationId: null, state: "filtered", @@ -64688,10 +63398,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, stagedId)); await service.processPendingDeliveries(1, stagedId); - const [filtered] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, stagedId)); + const [filtered] = await readChatDeliveriesById(stagedId); expect(filtered).toMatchObject({ conversationId: null, state: "filtered", @@ -64842,10 +63549,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ nextAttemptAt: new Date(0) }) .where(eq(chatDeliveries.id, stagedId)); await service.processPendingDeliveries(1, stagedId); - const [replayed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, stagedId)); + const [replayed] = await readChatDeliveriesById(stagedId); expect(replayed).toMatchObject({ conversationId: firstConversation.id, state: "processed", @@ -64889,10 +63593,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!callbacks.onReaction) throw new Error("Slack reaction callback was not registered"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); expect(conversation).toMatchObject({ externalThreadId: dm.thread.id, isDirectMessage: true, @@ -65076,10 +63777,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }), trigger: "mention", }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if ( [ "outbound_link", @@ -65108,10 +63806,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { direction: "outbound", }); } - const before = await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation!.issueId)); + const before = await readIssueCommentsByIssueId(conversation!.issueId); const send = (signingSecret = webhookSecret) => service.handleWebhook( endpoint.publicId, @@ -65197,10 +63892,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(delivery!.nextAttemptAt === null).toBe(filtered); expect(fixtureWakeups()).toHaveLength(1); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.issueId, conversation!.issueId)), + await readIssueCommentsByIssueId(conversation!.issueId), ).toEqual(before); if (filtered) { expect(JSON.stringify(delivery!.normalizedEvent)).not.toContain( @@ -65381,10 +64073,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toBe(true); }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); const comments = await db .select({ body: issueComments.body }) .from(issueComments) @@ -65590,10 +64279,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await sendLifecycle(sameSecondEdit); await sendLifecycle(sameSecondEdit); await vi.waitFor(async () => { - const lifecycle = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const lifecycle = await readChatDeliveriesByEndpointId(endpoint.id); expect( lifecycle.filter((row) => ["message_updated", "message_deleted"].includes(row.eventKind), @@ -66073,10 +64759,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "mention", }); - const [disabledDelivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [disabledDelivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(disabledDelivery).toMatchObject({ state: "filtered", attempts: 0, @@ -66406,10 +65089,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "direct_message", }); if (deferred) { - const [received] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [received] = await readChatDeliveriesByEndpointId(endpoint.id); expect(received).toMatchObject({ state: "received", attempts: 0 }); expect(providerRequests).toEqual([]); expect(storage.putFile).not.toHaveBeenCalled(); @@ -66468,10 +65148,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(providerRequests).toEqual([]); expect(wakeup).not.toHaveBeenCalled(); expect(restarted?.wakeup.mock.calls.length ?? 0).toBe(0); - const [denied] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [denied] = await readChatDeliveriesByEndpointId(endpoint.id); if (mode === "revoked_after_receipt") expect(denied.state).toBe("filtered"); else @@ -66496,10 +65173,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const [conversation] = await ( restarted?.service ?? service ).listConversations(endpoint.id); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", redactedError: null, @@ -66707,10 +65381,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(emptyFetch).toHaveBeenCalledTimes(1); expect(failedDownload).toHaveBeenCalledTimes(1); expect(storage.putFile).toHaveBeenCalledTimes(4); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", attempts: 1, @@ -66808,10 +65479,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .from(issueComments) .where(eq(issueComments.issueId, conversation.issueId)), ).resolves.toEqual([{ body: visibleFailure }]); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", redactedError: expect.stringContaining( @@ -67082,10 +65750,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: "direct_message", }); if (mode !== "final_before_add") await publishFinal(); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); const [removal] = await db .select() .from(chatActions) @@ -67127,10 +65792,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }; } if (mode === "superseded") { - const [current] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [current] = await readChatEndpointsById(endpoint.id); await db .update(chatEndpoints) .set({ @@ -67144,10 +65806,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } await activeService.processPendingReceiptReactions(1, removal!.id); if (mode === "remove_retry") { - const [failed] = await db - .select() - .from(chatActions) - .where(eq(chatActions.id, removal!.id)); + const [failed] = await readChatActionsById(removal!.id); expect(failed).toMatchObject({ status: "failed", result: { retryable: true, attempts: 1 }, @@ -67183,12 +65842,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { () => ({ chat_id: "77115569", message_id: 43, reaction: [] }), ), ); - const [add] = await db - .select() - .from(chatActions) - .where( - eq(chatActions.providerActionId, `receipt_reaction:${delivery.id}`), - ); + const [add] = await readChatActionsByProviderActionId(`receipt_reaction:${delivery.id}`); await db .update(chatActions) .set({ @@ -67348,10 +66002,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); const beforeAttempt = Date.now(); await service.publishComment(endpoint.id, conversation.id, comment.id); - const [retrying] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comment.id)); + const [retrying] = await readChatPublicationsByCommentId(comment.id); expect(retrying).toMatchObject({ state: "retry", attempts: 1 }); expect(retrying.nextAttemptAt?.getTime()).toBeGreaterThanOrEqual( beforeAttempt + 6_500, @@ -67363,10 +66014,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ attempts: 4, nextAttemptAt: new Date(0) }) .where(eq(chatPublications.id, retrying.id)); await service.processPendingPublications(); - const [stillRetrying] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, retrying.id)); + const [stillRetrying] = await readChatPublicationsById(retrying.id); expect(stillRetrying).toMatchObject({ state: "retry", attempts: 5 }); providerRuntime.postError = null; @@ -67731,10 +66379,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, }) .where(eq(heartbeatRuns.id, context.runId)); - const [beforeIssue] = await db - .select() - .from(issues) - .where(eq(issues.id, context.issue.id)); + const [beforeIssue] = await readIssuesById(context.issue.id); const repair = () => repairCommittedNativeChatResponse(db, { companyId: context.fixture.companyId, @@ -67768,20 +66413,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.createdByRunId, context.runId)); expect(comments).toHaveLength(1); expect(comments[0].body).toBe(context.result.summary); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, comments[0].id)); + const publications = await readChatPublicationsByCommentId(comments[0].id); expect(publications).toHaveLength(1); expect(publications[0]).toMatchObject({ state: "pending", endpointId: context.endpoint.id, conversationId: context.conversation.id, }); - const [run] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, context.runId)); + const [run] = await readHeartbeatRunsById(context.runId); expect(run).toMatchObject({ status: "succeeded", errorCode: null, @@ -67801,10 +66440,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { presentationDecision: { commentId: comments[0].id }, }, }); - const [afterIssue] = await db - .select() - .from(issues) - .where(eq(issues.id, context.issue.id)); + const [afterIssue] = await readIssuesById(context.issue.id); expect(afterIssue).toMatchObject({ status: context.beforeIssue.status, statusVersion: context.beforeIssue.statusVersion, @@ -67938,10 +66574,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.createdByRunId, context.runId)), ).resolves.toHaveLength(0); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)), + readChatPublicationsByEndpointId(context.endpoint.id), ).resolves.toHaveLength(mutation === "ambiguous_delivery" ? 1 : 0); } finally { await context.service.shutdown(); @@ -67955,10 +66588,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "port", ); try { - const [originalRun] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, context.runId)); + const [originalRun] = await readHeartbeatRunsById(context.runId); const laterId = randomUUID(); await db.insert(heartbeatRuns).values({ ...originalRun, @@ -67990,10 +66620,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { runId: laterId, workspaceFinalizeStatus: "succeeded", }); - const [laterIssue] = await db - .select() - .from(issues) - .where(eq(issues.id, context.issue.id)); + const [laterIssue] = await readIssuesById(context.issue.id); expect(laterIssue.lastStatusDecisionId).not.toBe( context.coordinator.decisionId, ); @@ -68005,10 +66632,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.createdByRunId, context.runId)); expect(comments).toHaveLength(1); expect(comments[0].body).toBe(context.result.summary); - const [after] = await db - .select() - .from(issues) - .where(eq(issues.id, context.issue.id)); + const [after] = await readIssuesById(context.issue.id); expect(after).toMatchObject({ status: laterIssue.status, statusVersion: laterIssue.statusVersion, @@ -68235,10 +66859,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { state: mutation === "delivery_unknown" ? "pending" : "cancelled" }, ]); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, failure.id)), + readChatPublicationsById(failure.id), ).resolves.toEqual([ { ...failure, @@ -68276,10 +66897,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); let currentLinkPublicationId = failure.id; if (mutation === "payload_claim_only") { - const [run] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, context.runId)); + const [run] = await readHeartbeatRunsById(context.runId); const callerPayload = { ...answer.payload, nativeCommittedChatResponse: @@ -68418,10 +67036,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ], ); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, unresolved.id)), + readChatPublicationsById(unresolved.id), ).resolves.toEqual([unresolved]); await expect( db @@ -68585,10 +67200,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.createdByRunId, context.runId)), ).resolves.toHaveLength(1); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)), + readChatPublicationsByEndpointId(context.endpoint.id), ).resolves.toHaveLength(0); } finally { await context.service.shutdown(); @@ -68625,10 +67237,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.createdByRunId, context.runId)), ).resolves.toHaveLength(0); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)), + readChatPublicationsByEndpointId(context.endpoint.id), ).resolves.toHaveLength(0); await expect( db @@ -68670,10 +67279,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await qualifySetupRoundTrip(service, endpoint.id); await service.test(endpoint.id, "owner-user"); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(endpoint.id); if (!conversation) throw new Error("Expected review transport conversation"); const inbound = await db @@ -68976,10 +67582,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { workspaceFinalizeStatus: "succeeded", projectRunStatus: true, }); - const [committedRun] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, runId)); + const [committedRun] = await readHeartbeatRunsById(runId); expect(committedRun).toMatchObject({ status: "succeeded", resultJson: { @@ -69284,10 +67887,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { trigger: id === "1999000.1" ? "mention" : "subscribed_message", }); await send(); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.endpointId, configured.endpoint.id)); + const [conversation] = await readChatConversationsByEndpointId(configured.endpoint.id); const [action] = await db .select() .from(chatActions) @@ -69298,10 +67898,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ); const promote = async () => { - const [owner] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, ownerId!)); + const [owner] = await readAgentWakeupRequestsById(ownerId!); const runId = randomUUID(); const contextSnapshot = owner.payload!._paperclipWakeContext as Record< string, @@ -69341,17 +67938,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { try { const { service, endpoint, conversation, fixture, action } = context; await expect( - db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)), + readHeartbeatRunsByCompanyId(fixture.companyId), ).resolves.toEqual([]); await expect(service.enqueueInboundWakeupPublications()).resolves.toBe(1); await service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(endpoint.id); expect(queued).toMatchObject({ commentId: action.payload.commentId, state: "published", @@ -69505,19 +68096,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { final = await addFinal(); await context.service.processPendingPublications(); } - const [answer] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, final!.id)); - const [failure] = await db - .select() - .from(chatPublications) - .where( - eq( - chatPublications.idempotencyKey, - `run:${runId}:failed:${context.endpoint.id}`, - ), - ); + const [answer] = await readChatPublicationsByCommentId(final!.id); + const [failure] = await readChatPublicationsByIdempotencyKey(`run:${runId}:failed:${context.endpoint.id}`); expect(answer?.state).toBe("published"); expect(failure?.state).toBe("published"); expect(answer?.providerMessageId).not.toBe(failure?.providerMessageId); @@ -69560,10 +68140,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await deferredChatQueueFixture(); try { await context.service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(context.endpoint.id); const runId = await context.promote(); const interactionId = randomUUID(); await db.insert(issueThreadInteractions).values({ @@ -69615,10 +68192,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await deferredChatQueueFixture(); try { await context.service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(context.endpoint.id); const runId = await context.promote(); await db.transaction(async (tx) => { await tx @@ -69716,10 +68290,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect( context.runtime.endpoints.get(context.endpoint.id)?.posts ?? [], ).toEqual([]); - const publications = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const publications = await readChatPublicationsByEndpointId(context.endpoint.id); expect(publications).toHaveLength(1); expect(publications[0]?.state).toBe("cancelled"); } finally { @@ -69732,10 +68303,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await deferredChatQueueFixture(); try { await context.service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(context.endpoint.id); await db .update(agentWakeupRequests) .set({ @@ -69773,16 +68341,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.runtime.endpoints.get(context.endpoint.id)?.posts ?? [], ).toEqual([]); await expect( - db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)), + readChatPublicationsByEndpointId(context.endpoint.id), ).resolves.toEqual([]); await expect( - db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, context.fixture.companyId)), + readHeartbeatRunsByCompanyId(context.fixture.companyId), ).resolves.toEqual([]); } finally { await context.service.shutdown(); @@ -69793,10 +68355,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const context = await deferredChatQueueFixture(); try { await context.service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(context.endpoint.id); await context.send("1999000.2"); const [second] = await db .select() @@ -69808,10 +68367,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { sql`${chatActions.id} <> ${context.action.id}`, ), ); - const [owner] = await db - .select() - .from(agentWakeupRequests) - .where(eq(agentWakeupRequests.id, context.action.id)); + const [owner] = await readAgentWakeupRequestsById(context.action.id); const remainingContext = { ...(owner.payload!._paperclipWakeContext as Record), wakeCommentId: second.payload.commentId, @@ -69940,10 +68496,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); await observed; await context.service.processPendingPublications(); - const [queued] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.endpointId, context.endpoint.id)); + const [queued] = await readChatPublicationsByEndpointId(context.endpoint.id); expect(queued).toMatchObject({ state: "retry", providerMessageId: null }); expect( context.runtime.endpoints.get(context.endpoint.id)?.posts ?? [], @@ -69991,18 +68544,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ nextAttemptAt: new Date(0) }) .where(eq(chatDeliveries.id, delivery.id)); await context.service.processPendingDeliveries(25, delivery.id); - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, delivery.id)); + const [processed] = await readChatDeliveriesById(delivery.id); expect(processed).toMatchObject({ state: "processed", normalizedEvent: delivery.normalizedEvent, }); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, processed!.conversationId!)); + const [conversation] = await readChatConversationsById(processed!.conversationId!); const snapshot = await chatWakeContext({ endpointId: context.endpoint.id, issueId: conversation!.issueId, @@ -70200,10 +68747,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message, trigger: "direct_message", }); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, context.endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(context.endpoint.id); expect(delivery?.state).toBe("received"); expect(message.metadata.dateSent.getTime()).toBe(display); await assertTimestampBinding({ @@ -70313,10 +68857,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); expect(response.status).toBe(200); } - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, context.endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(context.endpoint.id); expect(delivery?.state).toBe("received"); await assertTimestampBinding({ context, @@ -70436,10 +68977,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: context.message, trigger: "direct_message", }); - const [received] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, context.endpoint.id)); + const [received] = await readChatDeliveriesByEndpointId(context.endpoint.id); expect(received).toMatchObject({ state: "received", attempts: 0, @@ -70490,16 +69028,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatDeliveries.id, received!.id)); restarted = context.restart(); await restarted.service.processPendingDeliveries(1, received!.id); - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, received!.id)); + const [processed] = await readChatDeliveriesById(received!.id); expect(processed).toMatchObject({ state: "processed" }); expect(processed!.normalizedEvent).toEqual(received!.normalizedEvent); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, processed!.conversationId!)); + const [conversation] = await readChatConversationsById(processed!.conversationId!); const [principal] = await db .select() .from(chatExternalPrincipals) @@ -70540,10 +69072,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(restarted.wakeup).toHaveBeenCalledTimes(1); expect( ( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, received!.id)) + await readChatDeliveriesById(received!.id) )[0]!.normalizedEvent, ).toEqual(received!.normalizedEvent); } finally { @@ -70606,10 +69135,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await context.service.shutdown(); restarted = context.restart(); await restarted.service.processPendingDeliveries(1, received!.id); - const [processed] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, received!.id)); + const [processed] = await readChatDeliveriesById(received!.id); expect(processed).toMatchObject({ state: "failed", redactedError: @@ -70654,10 +69180,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message: context.message, trigger: "direct_message", }); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, context.endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(context.endpoint.id); expect(delivery).toMatchObject({ state: "filtered", conversationId: null, @@ -70906,14 +69429,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { sql`${chatDeliveries.normalizedEvent}->'message'->>'providerMessageId' = ${raw.id}`, ), ); - const [conversation] = await db - .select() - .from(chatConversations) - .where(eq(chatConversations.id, sourceDelivery!.conversationId!)); - const [issue] = await db - .select() - .from(issues) - .where(eq(issues.id, conversation!.issueId)); + const [conversation] = await readChatConversationsById(sourceDelivery!.conversationId!); + const [issue] = await readIssuesById(conversation!.issueId); const [sourceAction] = await db .select() .from(chatActions) @@ -71152,10 +69669,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); if (!conversation) throw new Error("Expected exact private topic conversation"); - const [currentEndpoint] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, endpoint.id)); + const [currentEndpoint] = await readChatEndpointsById(endpoint.id); const botId = Number(currentEndpoint!.botExternalId); const requests: Array<{ method: string; body: Record }> = []; @@ -71391,10 +69905,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: "running", contextSnapshot: {}, }); - const runsBefore = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, lane.fixture.companyId)); + const runsBefore = await readHeartbeatRunsByCompanyId(lane.fixture.companyId); let stoppedId = 0; lane.setHook(async (method, body) => { if (method.endsWith("Draft") && !stoppedId) { @@ -71423,10 +69934,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(issueComments.id, publication!.commentId!)); expect(comment!.body).toContain("END-first-stopped"); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, lane.fixture.companyId)), + await readHeartbeatRunsByCompanyId(lane.fixture.companyId), ).toEqual(runsBefore); expect(lane.context.cancelRun).not.toHaveBeenCalled(); expect( @@ -71610,14 +70118,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { // Scheduler-only fixtures model a full already-confirmed first page. // They have no credentials; no provider qualification is claimed for // these rows. The target still uses actual configure/runtime/Stop. - const [templateEndpoint] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, lane.endpoint.id)); - const [templateConnection] = await db - .select() - .from(toolConnections) - .where(eq(toolConnections.id, lane.endpoint.connectionId)); + const [templateEndpoint] = await readChatEndpointsById(lane.endpoint.id); + const [templateConnection] = await readToolConnectionsById(lane.endpoint.connectionId); const fingerprint = createHash("sha256").update("[]").digest("hex"); for (let index = 0; index < 25; index++) { const id = `00000000-${randomUUID().slice(9)}`; @@ -71908,10 +70410,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, }); if (mode === "changed_generation") { - const [current] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, lane.endpoint.id)); + const [current] = await readChatEndpointsById(lane.endpoint.id); await db .update(chatEndpoints) .set({ @@ -72176,10 +70675,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect((await lane.deliver(draftId)).status).toBe(200); await lane.context.service.processPendingPublications(); expect(lane.requests).toHaveLength(before); - const [current] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, publication!.id)); + const [current] = await readChatPublicationsById(publication!.id); expect(current!.state).toBe("delivery_unknown"); await expect( lane.context.service.replayPublication( @@ -72203,10 +70699,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { if (!method.endsWith("Draft") || checked) return; checked = true; const [action] = await lane.actions(); - const [endpoint] = await db - .select() - .from(chatEndpoints) - .where(eq(chatEndpoints.id, lane.endpoint.id)); + const [endpoint] = await readChatEndpointsById(lane.endpoint.id); if (changed === "bot") await db .update(chatEndpoints) @@ -72461,10 +70954,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { issueId: context.issue.id, runId, }); - const [run] = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.id, runId)); + const [run] = await readHeartbeatRunsById(runId); expect(run).toMatchObject({ status: "succeeded", resultJson: { @@ -72512,10 +71002,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: 200, }); await context.service.processPendingPublications(); - const [done] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, unknown!.id)); + const [done] = await readChatTeamsFileTransfersById(unknown!.id); expect(done).toMatchObject({ phase: "delivered", consentMessageId: null, @@ -72611,17 +71098,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }) .where(eq(chatTeamsFileTransfers.id, unknown!.id)); } - const [before] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, unknown!.id)); + const [before] = await readChatTeamsFileTransfersById(unknown!.id); await expect(context.dispatchConsent()).resolves.toEqual({ status: 403, }); - const [after] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, unknown!.id)); + const [after] = await readChatTeamsFileTransfersById(unknown!.id); expect(after).toEqual(before); expect(context.consentCards).toHaveLength(1); expect(context.uploadRequest).not.toHaveBeenCalled(); @@ -72675,10 +71156,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { details: { code: "chat_file_transfer_resolution_required" }, }); } - const [unchanged] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, unknown!.id)); + const [unchanged] = await readChatTeamsFileTransfersById(unknown!.id); expect(unchanged).toEqual(unknown); context.controls.beforeFileInfoReceipt = undefined; await context.service.resolvePublication( @@ -72689,10 +71167,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { { phase: "file_info_unknown", version: unknown!.version }, ); await context.service.processPendingPublications(); - const [done] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, unknown!.id)); + const [done] = await readChatTeamsFileTransfersById(unknown!.id); expect(done).toMatchObject({ phase: "delivered", fileInfoMessageId: "native-file-2", @@ -72774,10 +71249,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: 200, }); await context.service.processPendingPublications(); - const [done] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer!.id)); + const [done] = await readChatTeamsFileTransfersById(transfer!.id); expect(done).toMatchObject({ phase: "delivered", consentMessageId: "native-consent-1", @@ -73002,10 +71474,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ), ); - const [before] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)); + const [before] = await readChatTeamsFileTransfersById(transfer.id); if (boundary === "accept") { const outcome = await context.dispatchConsent(); // No success ACK for a revoked principal. The current callback @@ -73014,10 +71483,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } await context.service.processPendingPublications(); expect( - await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)), + await readChatTeamsFileTransfersById(transfer.id), ).toEqual([before]); expect(context.uploadRequest).not.toHaveBeenCalled(); expect(context.fileCards).toEqual([]); @@ -73051,18 +71517,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(context.runtime.endpoints.get(context.endpoint.id)).not.toBe( oldRuntime, ); - const [before] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)); + const [before] = await readChatTeamsFileTransfersById(transfer.id); await expect( context.dispatchConsent("accept", {}, oldHandler), ).resolves.toEqual({ status: 403 }); expect( - await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)), + await readChatTeamsFileTransfersById(transfer.id), ).toEqual([before]); expect(context.uploadRequest).not.toHaveBeenCalled(); expect(context.fileCards).toEqual([]); @@ -73083,20 +71543,14 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { context.dispatchConsent("accept", activity), ).resolves.toEqual({ status: 200 }); await context.service.processPendingPublications(); - const [before] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)); + const [before] = await readChatTeamsFileTransfersById(transfer.id); expect(before?.phase).toBe("delivered"); await expect( context.dispatchConsent("accept", activity), ).resolves.toEqual({ status: 200 }); await context.service.processPendingPublications(); expect( - await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)), + await readChatTeamsFileTransfersById(transfer.id), ).toEqual([before]); expect( await db @@ -73149,10 +71603,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { [attachment.id], ); expect(duringPost).toBeDefined(); - const [finished] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, duringPost!.id)); + const [finished] = await readChatTeamsFileTransfersById(duringPost!.id); expect(finished).toMatchObject({ phase: "delivered", consentMessageId: "native-consent-1", @@ -73187,10 +71638,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { attemptExpiresAt: null, }); const app = routesApp(db, context.fixture.companyId, context.service); - const before = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, publication.commentId!)); + const before = await readChatPublicationsByCommentId(publication.commentId!); const activity = await request(app) .get(`/api/chat-endpoints/${context.endpoint.id}/activity`) .expect(200); @@ -73213,10 +71661,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(chatTeamsFileTransfers.publicationId, publication.id)), ).toEqual([conflict]); expect( - await db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, publication.commentId!)), + await readChatPublicationsByCommentId(publication.commentId!), ).toEqual(before); const path = `/api/chat-endpoints/${context.endpoint.id}/publications/${publication.id}/resolve`; const hint = { phase: "conflict", version: conflict!.version }; @@ -73301,10 +71746,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { status: 200, }); await context.service.processPendingPublications(); - const [unknown] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)); + const [unknown] = await readChatTeamsFileTransfersById(transfer.id); expect(unknown).toMatchObject({ phase: "file_info_unknown", consentMessageId: "native-consent-1", @@ -73322,10 +71764,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "owner-user", { phase: "file_info_unknown", version: unknown!.version }, ); - const [finished] = await db - .select() - .from(chatTeamsFileTransfers) - .where(eq(chatTeamsFileTransfers.id, transfer.id)); + const [finished] = await readChatTeamsFileTransfersById(transfer.id); expect(finished).toMatchObject({ phase: "delivered", fileInfoMessageId: "native-file-2", @@ -73511,10 +71950,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } } const orderedBatch = (commentId: string) => - db - .select() - .from(chatPublications) - .where(eq(chatPublications.commentId, commentId)) + readChatPublicationsByCommentId(commentId) .orderBy( asc(chatPublications.createdAt), asc(sql`${chatPublications.payload}->'transportPart'->>'orderKey'`), @@ -73844,10 +72280,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .returning(); await service.processPendingPublications(); expect(transport.posts).toEqual([]); - const [after] = await db - .select() - .from(chatPublications) - .where(eq(chatPublications.id, publication!.id)); + const [after] = await readChatPublicationsById(publication!.id); expect(after!.state).toBe("failed"); expect(after!.redactedError).not.toContain("PRIVATE"); expect(after!.providerMessageId).toBeNull(); @@ -73936,10 +72369,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }, }), }); - const [original] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, first.endpoint.id)); + const [original] = await readChatDeliveriesByEndpointId(first.endpoint.id); expect(original).toMatchObject({ state: seededWake ? "processed" : "received", }); @@ -74030,14 +72460,8 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .set({ providerMessageId: messageId }) .where(eq(chatMessageLinks.deliveryId, original.id)); } - const commentsBefore = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)); - const issuesBefore = await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)); + const commentsBefore = await readIssueCommentsByCompanyId(fixture.companyId); + const issuesBefore = await readIssuesByCompanyId(fixture.companyId); const publicationsBefore = await db .select() .from(chatPublications) @@ -74153,10 +72577,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ); } await restarted.service.processPendingDeliveries(25, original.id); - const [settled] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, original.id)); + const [settled] = await readChatDeliveriesById(original.id); if (denied) { expect(restarted.wakeup).not.toHaveBeenCalled(); expect(settled).toMatchObject({ @@ -74167,16 +72588,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { "PRIVATE_RETAINED_ZERO_BODY", ); expect( - await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)), + await readIssueCommentsByCompanyId(fixture.companyId), ).toEqual(commentsBefore); expect( - await db - .select() - .from(issues) - .where(eq(issues.companyId, fixture.companyId)), + await readIssuesByCompanyId(fixture.companyId), ).toEqual(issuesBefore); expect( await db @@ -74186,10 +72601,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ).toEqual(publicationsBefore); if (seededWake) expect( - await db - .select() - .from(chatActions) - .where(eq(chatActions.id, actions[0]!.id)), + await readChatActionsById(actions[0]!.id), ).toEqual([ expect.objectContaining({ status: "failed", @@ -74217,19 +72629,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { .where(eq(agentWakeupRequests.companyId, fixture.companyId)), ).toHaveLength(denied ? 0 : 1); expect( - await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.companyId, fixture.companyId)), + await readHeartbeatRunsByCompanyId(fixture.companyId), ).toEqual([]); await restarted.service.processPendingDeliveries(25, original.id); expect(restarted.wakeup).toHaveBeenCalledTimes(denied ? 0 : 1); if (denied) expect( - await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.id, original.id)), + await readChatDeliveriesById(original.id), ).toEqual([settled]); } finally { try { @@ -74535,10 +72941,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { body: photo, contentType: "image/jpeg", }); - const [comment] = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)); + const [comment] = await readIssueCommentsByCompanyId(fixture.companyId); expect(comment.body).toBe( "Before files\n\nFile section\n\nPhoto caption\n\nQuoted tail\n\nQuote author", ); @@ -74686,10 +73089,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { }); expect(requests).toEqual(["getFile", "download"]); } else { - const [comment] = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)); + const [comment] = await readIssueCommentsByCompanyId(fixture.companyId); expect(comment?.body).toBe( mode === "mixed" ? "Before\n\nExact quoted tail\n\nVisible credit\n\nAfter" @@ -74826,10 +73226,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { message, trigger: topic ? "mention" : "direct_message", }); - const [received] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [received] = await readChatDeliveriesByEndpointId(endpoint.id); expect(received).toMatchObject({ state: "received", attempts: 0 }); expect(received.normalizedEvent).toMatchObject({ message: { @@ -74930,10 +73327,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { mode === "revoked_during_fetch" ? ["getFile", "download"] : [], ); if (mode === "changed_locator") { - const [comment] = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)); + const [comment] = await readIssueCommentsByCompanyId(fixture.companyId); expect(comment!.body).toContain("could not safely import"); } } @@ -75101,10 +73495,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ? ["getFile:optional-media", "download:optional-media"] : [], ); - const [comment] = await db - .select() - .from(issueComments) - .where(eq(issueComments.companyId, fixture.companyId)); + const [comment] = await readIssueCommentsByCompanyId(fixture.companyId); expect(comment!.body).toContain( `under ${formatAttachmentSize(MAX_ATTACHMENT_BYTES)}`, ); @@ -75131,10 +73522,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { ), ).toBe(true); expect(wakeup).toHaveBeenCalledOnce(); - const [delivery] = await db - .select() - .from(chatDeliveries) - .where(eq(chatDeliveries.endpointId, endpoint.id)); + const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); expect(delivery).toMatchObject({ state: "processed", redactedError: null, diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index a0979025f24..3b2005cc961 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -10825,158 +10825,42 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }, ); - it("requeues accepted interaction continuations stranded in_review without execution state", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); + + async function seedResolvedInteractionRecoveryFixture(input: { + title: string; + kind?: "request_confirmation" | "ask_user_questions"; + issueStatus?: "in_review" | "in_progress"; + agentName?: string; + }) { + const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), interactionId = randomUUID(); const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, + id: companyId, name: "Paperclip", issuePrefix, defaultResponsibleUserId: "responsible-user", requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ - id: agentId, - companyId, - name: "CodexCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, + id: agentId, companyId, name: input.agentName ?? "CodexCoder", role: "engineer", status: "idle", adapterType: "codex_local", adapterConfig: {}, + runtimeConfig: { heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 } }, permissions: {}, }); await db.insert(issues).values({ - id: issueId, - companyId, - title: "Accepted plan never resumed", - status: "in_review", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, + id: issueId, companyId, title: input.title, status: input.issueStatus ?? "in_review", priority: "medium", assigneeAgentId: agentId, + responsibleUserId: "responsible-user", issueNumber: 1, identifier: `${issuePrefix}-1`, }); + const kind = input.kind ?? "request_confirmation"; await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "request_confirmation", - status: "accepted", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { version: 1, prompt: "Approve the plan?" }, - result: { outcome: "accepted" }, + id: interactionId, companyId, issueId, kind, status: kind === "ask_user_questions" ? "answered" : "accepted", + continuationPolicy: "wake_assignee_on_accept", createdByAgentId: agentId, resolvedByUserId: "responsible-user", resolvedAt, updatedAt: resolvedAt, + payload: kind === "ask_user_questions" ? { version: 1, questions: [{ + id: "format", prompt: "Choose a format", selectionMode: "single", required: true, options: [{ id: "markdown", label: "Markdown" }], + }] } : { version: 1, prompt: "Approve the plan?" }, + result: kind === "ask_user_questions" ? { version: 1, answers: [{ questionId: "format", optionIds: ["markdown"] }] } : { outcome: "accepted" }, }); - - const heartbeat = heartbeatService(db); - const result = await heartbeat.reconcileStrandedAssignedIssues(); - - expect(result.continuationRequeued).toBe(1); - expect(result.issueIds).toEqual([issueId]); - - const run = await db - .select({ - agentId: heartbeatRuns.agentId, - contextSnapshot: heartbeatRuns.contextSnapshot, - retryOfRunId: heartbeatRuns.retryOfRunId, - }) - .from(heartbeatRuns) - .where(eq(heartbeatRuns.agentId, agentId)) - .then((rows) => rows[0] ?? null); - expect(run?.agentId).toBe(agentId); - expect(run?.retryOfRunId).toBeNull(); - expect(run?.contextSnapshot).toMatchObject({ - issueId, - taskId: issueId, - wakeReason: "issue_continuation_needed", - retryReason: "issue_continuation_needed", - source: "issue.interaction_continuation_recovery", - interactionId, - interactionKind: "request_confirmation", - interactionStatus: "accepted", - interactionContinuationPolicy: "wake_assignee_on_accept", - interactionResolvedAt: resolvedAt.toISOString(), - }); - }); + return { companyId, agentId, issueId, interactionId, resolvedAt }; + } it("recovers an answered question with its interaction-specific continuation context", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); - const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); - const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values({ - id: agentId, - companyId, - name: "OpenCodeCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, - }); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Answered question never resumed", - status: "in_progress", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, - }); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "ask_user_questions", - status: "answered", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { - version: 1, - questions: [ - { - id: "format", - prompt: "Choose a format", - selectionMode: "single", - required: true, - options: [{ id: "markdown", label: "Markdown" }], - }, - ], - }, - result: { - version: 1, - answers: [{ questionId: "format", optionIds: ["markdown"] }], - }, - }); + const { companyId, agentId, issueId, interactionId, resolvedAt } = + await seedResolvedInteractionRecoveryFixture({ title: "Answered question never resumed", kind: "ask_user_questions", issueStatus: "in_progress", agentName: "OpenCodeCoder" }); const heartbeat = heartbeatService(db); const result = await heartbeat.reconcileStrandedAssignedIssues(); @@ -11085,58 +10969,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); it("counts five historical review-park cancellations against the upgraded disposition-repair ceiling", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); - const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); - const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values({ - id: agentId, - companyId, - name: "CodexCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, - }); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Accepted plan cancellation loop", - status: "in_review", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, - }); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "request_confirmation", - status: "accepted", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { version: 1, prompt: "Approve the plan?" }, - result: { outcome: "accepted" }, - }); + const { companyId, agentId, issueId, interactionId, resolvedAt } = + await seedResolvedInteractionRecoveryFixture({ title: "Accepted plan cancellation loop" }); for (let attempt = 1; attempt <= 5; attempt += 1) { const finishedAt = new Date(resolvedAt.getTime() + attempt * 60_000); await db.insert(heartbeatRuns).values({ @@ -11227,59 +11061,9 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); it("skips accepted interaction recovery after its continuation succeeds", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); - const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); + const { companyId, agentId, issueId, interactionId, resolvedAt } = + await seedResolvedInteractionRecoveryFixture({ title: "Accepted plan already resumed" }); const succeededAt = new Date("2026-03-19T00:06:00.000Z"); - const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values({ - id: agentId, - companyId, - name: "CodexCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, - }); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Accepted plan already resumed", - status: "in_review", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, - }); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "request_confirmation", - status: "accepted", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { version: 1, prompt: "Approve the plan?" }, - result: { outcome: "accepted" }, - }); await db.insert(heartbeatRuns).values({ id: randomUUID(), companyId, @@ -11316,59 +11100,9 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); it("requeues accepted interaction continuations even when a later successful run is unrelated", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); - const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); + const { companyId, agentId, issueId, interactionId, resolvedAt } = + await seedResolvedInteractionRecoveryFixture({ title: "Accepted plan masked by unrelated run" }); const unrelatedRunAt = new Date("2026-03-19T00:06:00.000Z"); - const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values({ - id: agentId, - companyId, - name: "CodexCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, - }); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Accepted plan masked by unrelated run", - status: "in_review", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, - }); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "request_confirmation", - status: "accepted", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { version: 1, prompt: "Approve the plan?" }, - result: { outcome: "accepted" }, - }); await db.insert(heartbeatRuns).values({ id: randomUUID(), companyId, @@ -11417,58 +11151,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { // *no* wake request and *no* run at all. Pre-P1 the recovery sweep skipped in_review issues // lacking an execution policy, so this limbo persisted forever. The sweep now requeues it. it("recovers a plan approval whose accept-time continuation wake enqueue was silently dropped", async () => { - const companyId = randomUUID(); - const agentId = randomUUID(); - const issueId = randomUUID(); - const interactionId = randomUUID(); - const resolvedAt = new Date("2026-03-19T00:05:00.000Z"); - const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix, - defaultResponsibleUserId: "responsible-user", - requireBoardApprovalForNewAgents: false, - }); - await db.insert(agents).values({ - id: agentId, - companyId, - name: "CodexCoder", - role: "engineer", - status: "idle", - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: { - heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 }, - }, - permissions: {}, - }); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Approved plan whose wake enqueue was dropped", - status: "in_review", - priority: "medium", - assigneeAgentId: agentId, - responsibleUserId: "responsible-user", - issueNumber: 1, - identifier: `${issuePrefix}-1`, - }); - await db.insert(issueThreadInteractions).values({ - id: interactionId, - companyId, - issueId, - kind: "request_confirmation", - status: "accepted", - continuationPolicy: "wake_assignee_on_accept", - createdByAgentId: agentId, - resolvedByUserId: "responsible-user", - resolvedAt, - updatedAt: resolvedAt, - payload: { version: 1, prompt: "Approve the plan?" }, - result: { outcome: "accepted" }, - }); + const { companyId, agentId, issueId, interactionId, resolvedAt } = + await seedResolvedInteractionRecoveryFixture({ title: "Approved plan whose wake enqueue was dropped" }); // Precondition of the silent-enqueue-drop bug: the accept produced no wake and no run. const priorWakeups = await db @@ -11492,15 +11176,23 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { .select({ agentId: heartbeatRuns.agentId, contextSnapshot: heartbeatRuns.contextSnapshot, + retryOfRunId: heartbeatRuns.retryOfRunId, }) .from(heartbeatRuns) .where(eq(heartbeatRuns.agentId, agentId)) .then((rows) => rows[0] ?? null); expect(run?.agentId).toBe(agentId); + expect(run?.retryOfRunId).toBeNull(); expect(run?.contextSnapshot).toMatchObject({ issueId, + taskId: issueId, + wakeReason: "issue_continuation_needed", + retryReason: "issue_continuation_needed", interactionId, + interactionKind: "request_confirmation", interactionStatus: "accepted", + interactionContinuationPolicy: "wake_assignee_on_accept", + interactionResolvedAt: resolvedAt.toISOString(), source: "issue.interaction_continuation_recovery", }); @@ -17032,5 +16724,4 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); - }); diff --git a/server/src/__tests__/issue-interactions-read-only-contract.test.ts b/server/src/__tests__/issue-interactions-read-only-contract.test.ts deleted file mode 100644 index 6437504f58a..00000000000 --- a/server/src/__tests__/issue-interactions-read-only-contract.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import fs from "node:fs"; -import { describe, expect, it } from "vitest"; - -function assertReadOnlyInteractionsGet(source: string) { - const start = source.search( - /router\.get\(\s*["']\/issues\/:id\/interactions["']/, - ); - expect(start).toBeGreaterThanOrEqual(0); - const followingRoutes = source.slice(start); - const end = followingRoutes.search( - /router\.post\(\s*["']\/issues\/:id\/interactions["']/, - ); - expect(end).toBeGreaterThan(0); - const handler = followingRoutes.slice(0, end); - - expect(handler).toContain("listForIssue"); - expect(handler).not.toContain("expireRequestConfirmations"); - expect(handler).not.toContain("expirePendingInteractions"); - expect(handler).not.toContain("logActivity"); -} - -describe("issue interactions GET contract", () => { - it("does not perform expiry sweeps or activity writes", () => { - const source = fs.readFileSync( - new URL("../routes/issues.ts", import.meta.url), - "utf8", - ); - assertReadOnlyInteractionsGet(source); - }); - - it.each(["", "\n "])( - "accepts a read-only GET with route whitespace %j", - (whitespace) => { - assertReadOnlyInteractionsGet(` - router.get(${whitespace}"/issues/:id/interactions", async (req, res) => { - res.json(await service.listForIssue(req.params.id)); - }); - router.post(${whitespace}"/issues/:id/interactions", async () => { - await service.expirePendingInteractions(); - await logActivity(); - }); - `); - }, - ); - - it.each([ - "expireRequestConfirmations", - "expirePendingInteractions", - "logActivity", - ])("still rejects %s inside a formatted GET", (mutation) => { - expect(() => - assertReadOnlyInteractionsGet(` - router.get( - "/issues/:id/interactions", async (req, res) => { - await service.${mutation}(); - res.json(await service.listForIssue(req.params.id)); - }, - ); - router.post( - "/issues/:id/interactions", async () => {}, - ); - `), - ).toThrow(); - }); - - it.each(["get", "post"])( - "fails closed if the %s boundary is missing", - (missingMethod) => { - const routes = ["get", "post"] - .filter((method) => method !== missingMethod) - .map( - (method) => - `router.${method}("/issues/:id/interactions", () => service.listForIssue());`, - ) - .join("\n"); - expect(() => assertReadOnlyInteractionsGet(routes)).toThrow(); - }, - ); -}); diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts index e2db55feecd..6e9146d7f05 100644 --- a/server/src/__tests__/issue-thread-interaction-routes.test.ts +++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts @@ -629,18 +629,21 @@ describe("issue thread interaction routes", () => { }), expect.anything()); }); - it("does not run historical-comment catch-up or queue recovery from the interaction read path", async () => { + it.each(["in_review", "done"])("keeps interaction reads free of expiry, activity writes, and recovery while %s", async (status) => { mockIssueService.getById.mockResolvedValue(createIssue({ - status: "in_review", + status, assigneeAgentId: ASSIGNEE_AGENT_ID, })); mockInteractionService.listForIssue.mockResolvedValue([]); await request(await createApp()) .get("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions") - .expect(200); + .expect(200, []); + expect(mockInteractionService.listForIssue).toHaveBeenCalledExactlyOnceWith(ISSUE_ID); expect(mockInteractionService.expireRequestConfirmationsSupersededByHistoricalComments).not.toHaveBeenCalled(); + expect(mockInteractionService.expirePendingInteractionsForTerminalIssue).not.toHaveBeenCalled(); + expect(mockLogActivity).not.toHaveBeenCalled(); expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); }); diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index c40bc18031d..6ccc594cd26 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -5187,82 +5187,6 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { await tempDb?.cleanup(); }); - it("inherits the parent issue workspace linkage when child workspace fields are omitted", async () => { - const companyId = randomUUID(); - const projectId = randomUUID(); - const parentIssueId = randomUUID(); - const projectWorkspaceId = randomUUID(); - const executionWorkspaceId = randomUUID(); - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - requireBoardApprovalForNewAgents: false, - }); - await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true }); - - await db.insert(projects).values({ - id: projectId, - companyId, - name: "Workspace project", - status: "in_progress", - }); - - await db.insert(projectWorkspaces).values({ - id: projectWorkspaceId, - companyId, - projectId, - name: "Primary workspace", - isPrimary: true, - sharedWorkspaceKey: "workspace-key", - }); - - await db.insert(executionWorkspaces).values({ - id: executionWorkspaceId, - companyId, - projectId, - projectWorkspaceId, - mode: "isolated_workspace", - strategyType: "git_worktree", - name: "Issue worktree", - status: "active", - providerType: "git_worktree", - providerRef: `/tmp/${executionWorkspaceId}`, - }); - - await db.insert(issues).values({ - id: parentIssueId, - companyId, - projectId, - projectWorkspaceId, - title: "Parent issue", - status: "in_progress", - priority: "medium", - executionWorkspaceId, - executionWorkspacePreference: "reuse_existing", - executionWorkspaceSettings: { - mode: "isolated_workspace", - workspaceRuntime: { profile: "agent" }, - }, - }); - - const child = await svc.create(companyId, { - parentId: parentIssueId, - projectId, - title: "Child issue", - }); - - expect(child.parentId).toBe(parentIssueId); - expect(child.projectWorkspaceId).toBe(projectWorkspaceId); - expect(child.executionWorkspaceId).toBe(executionWorkspaceId); - expect(child.executionWorkspacePreference).toBe("reuse_existing"); - expect(child.executionWorkspaceSettings).toEqual({ - mode: "isolated_workspace", - workspaceRuntime: { profile: "agent" }, - }); - }); - it("preserves the parent project when a generic child create inherits workspace linkage", async () => { const companyId = randomUUID(); const projectId = randomUUID(); @@ -5463,105 +5387,6 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { }); }); - it("keeps explicit workspace fields instead of inheriting the parent linkage", async () => { - const companyId = randomUUID(); - const projectId = randomUUID(); - const parentIssueId = randomUUID(); - const parentProjectWorkspaceId = randomUUID(); - const parentExecutionWorkspaceId = randomUUID(); - const explicitProjectWorkspaceId = randomUUID(); - const explicitExecutionWorkspaceId = randomUUID(); - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, - requireBoardApprovalForNewAgents: false, - }); - await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true }); - - await db.insert(projects).values({ - id: projectId, - companyId, - name: "Workspace project", - status: "in_progress", - }); - - await db.insert(projectWorkspaces).values([ - { - id: parentProjectWorkspaceId, - companyId, - projectId, - name: "Parent workspace", - }, - { - id: explicitProjectWorkspaceId, - companyId, - projectId, - name: "Explicit workspace", - }, - ]); - - await db.insert(executionWorkspaces).values([ - { - id: parentExecutionWorkspaceId, - companyId, - projectId, - projectWorkspaceId: parentProjectWorkspaceId, - mode: "isolated_workspace", - strategyType: "git_worktree", - name: "Parent worktree", - status: "active", - providerType: "git_worktree", - }, - { - id: explicitExecutionWorkspaceId, - companyId, - projectId, - projectWorkspaceId: explicitProjectWorkspaceId, - mode: "shared_workspace", - strategyType: "project_primary", - name: "Explicit shared workspace", - status: "active", - providerType: "local_fs", - }, - ]); - - await db.insert(issues).values({ - id: parentIssueId, - companyId, - projectId, - projectWorkspaceId: parentProjectWorkspaceId, - title: "Parent issue", - status: "in_progress", - priority: "medium", - executionWorkspaceId: parentExecutionWorkspaceId, - executionWorkspacePreference: "reuse_existing", - executionWorkspaceSettings: { - mode: "isolated_workspace", - }, - }); - - const child = await svc.create(companyId, { - parentId: parentIssueId, - projectId, - title: "Child issue", - projectWorkspaceId: explicitProjectWorkspaceId, - executionWorkspaceId: explicitExecutionWorkspaceId, - executionWorkspacePreference: "reuse_existing", - executionWorkspaceSettings: { - mode: "shared_workspace", - }, - }); - - expect(child.projectWorkspaceId).toBe(explicitProjectWorkspaceId); - expect(child.executionWorkspaceId).toBe(explicitExecutionWorkspaceId); - expect(child.executionWorkspacePreference).toBe("reuse_existing"); - expect(child.executionWorkspaceSettings).toEqual({ - mode: "shared_workspace", - }); - }); - it("inherits workspace linkage from an explicit source issue without creating a parent-child relationship", async () => { const companyId = randomUUID(); const projectId = randomUUID(); diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index 84b3152e3bf..09cdeecdfb4 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -3619,6 +3619,49 @@ describeEmbeddedPostgres("tool access service", () => { }), ]), ); + + const gatewayAudit = await db + .select() + .from(activityLog) + .where( + and( + eq(activityLog.companyId, company.id), + eq(activityLog.action, "tool_gateway.approval_requested"), + ), + ); + expect(gatewayAudit).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + actorType: "user", + actorId: userId, + agentId: agent.id, + details: expect.objectContaining({ + source: "test", + actionRequestId: res.body.actionRequestId, + invocationId: res.body.invocationId, + }), + }), + ]), + ); + + const dedicatedAudit = await db + .select() + .from(toolAccessAuditEvents) + .where(eq(toolAccessAuditEvents.companyId, company.id)); + expect(dedicatedAudit).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + actorType: "user", + actorId: userId, + details: expect.objectContaining({ + source: "test", + agentId: agent.id, + actionRequestId: res.body.actionRequestId, + runId: null, + }), + }), + ]), + ); }); it("cancels an ask-first test request when approval signing is unavailable", async () => { @@ -3672,77 +3715,6 @@ describeEmbeddedPostgres("tool access service", () => { }); }); - it("audits ask-first test calls with the real board actor and selected agent", async () => { - const company = await createCompany(db); - const userId = `tool-tester-${randomUUID()}`; - await grantBoardUser(db, company.id, userId, ["tools:use"]); - const agent = await createAgent(db, company.id); - const { connection } = await createRemoteToolFixture(db, company.id); - await db.insert(toolPolicies).values({ - companyId: company.id, - name: `Ask first ${randomUUID()}`, - policyType: "require_approval", - priority: 100, - selectors: { connectionId: connection.id }, - }); - const app = createRouteApp( - db, - boardSessionActor(company.id, "operator", userId), - createToolGatewayService(db, { toolActionSigningSecret: "test-secret" }), - ); - - const res = await request(app) - .post(`/api/tool-connections/${connection.id}/test-calls`) - .send({ - agentId: agent.id, - toolName: "send_email", - parameters: { to: "a@example.com" }, - }) - .expect(200); - - const gatewayAudit = await db - .select() - .from(activityLog) - .where( - and( - eq(activityLog.companyId, company.id), - eq(activityLog.action, "tool_gateway.approval_requested"), - ), - ); - expect(gatewayAudit).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - actorType: "user", - actorId: userId, - agentId: agent.id, - details: expect.objectContaining({ - source: "test", - actionRequestId: res.body.actionRequestId, - invocationId: res.body.invocationId, - }), - }), - ]), - ); - - const dedicatedAudit = await db - .select() - .from(toolAccessAuditEvents) - .where(eq(toolAccessAuditEvents.companyId, company.id)); - expect(dedicatedAudit).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - actorType: "user", - actorId: userId, - details: expect.objectContaining({ - source: "test", - agentId: agent.id, - actionRequestId: res.body.actionRequestId, - runId: null, - }), - }), - ]), - ); - }); it("drives an ask-first test call through its live lifecycle (waiting → approved/done with the real result)", async () => { const company = await createCompany(db); @@ -3778,6 +3750,10 @@ describeEmbeddedPostgres("tool access service", () => { const actionRequestId = created.body.actionRequestId as string; expect(actionRequestId).toEqual(expect.any(String)); + await request(app) + .get(`/api/tool-connections/${connection.id}/test-calls/${randomUUID()}`) + .expect(404); + // 2. Status starts as "waiting" and surfaces the redacted "Where" snapshot. const waiting = await request(app) .get( @@ -3949,24 +3925,6 @@ describeEmbeddedPostgres("tool access service", () => { }); }); - it("404s a single-id test-call status fetch for a non-test-origin action request", async () => { - const company = await createCompany(db); - const userId = `tool-tester-${randomUUID()}`; - await grantBoardUser(db, company.id, userId, ["tools:use"]); - const { connection } = await createRemoteToolFixture(db, company.id); - const gateway = createToolGatewayService(db, { - toolActionSigningSecret: "test-secret", - }); - const app = createRouteApp( - db, - boardSessionActor(company.id, "operator", userId), - gateway, - ); - - await request(app) - .get(`/api/tool-connections/${connection.id}/test-calls/${randomUUID()}`) - .expect(404); - }); it("returns off for blocked test calls without executing the remote tool", async () => { const company = await createCompany(db); @@ -4014,35 +3972,6 @@ describeEmbeddedPostgres("tool access service", () => { agentId: agent.id, runId: null, }); - }); - - it("audits blocked test calls with the real board actor and selected agent", async () => { - const company = await createCompany(db); - const userId = `tool-tester-${randomUUID()}`; - await grantBoardUser(db, company.id, userId, ["tools:use"]); - const agent = await createAgent(db, company.id); - const { connection } = await createRemoteToolFixture(db, company.id); - await db.insert(toolPolicies).values({ - companyId: company.id, - name: `Block ${randomUUID()}`, - policyType: "block", - priority: 100, - selectors: { connectionId: connection.id }, - }); - const app = createRouteApp( - db, - boardSessionActor(company.id, "operator", userId), - createToolGatewayService(db, { toolActionSigningSecret: "test-secret" }), - ); - - const res = await request(app) - .post(`/api/tool-connections/${connection.id}/test-calls`) - .send({ - agentId: agent.id, - toolName: "send_email", - parameters: { to: "a@example.com" }, - }) - .expect(200); const gatewayAudit = await db .select() @@ -4089,6 +4018,7 @@ describeEmbeddedPostgres("tool access service", () => { ); }); + it("denies test calls through agents the board user cannot task", async () => { const company = await createCompany(db); const userId = `tool-tester-${randomUUID()}`; @@ -5536,7 +5466,6 @@ describeEmbeddedPostgres("tool access service", () => { - it("retires saved Composio broker and child records without touching credentials until removal", async () => { const company = await createCompany(db); const { parent, child } = await createComposioParentAndChild(db, company.id); diff --git a/server/src/__tests__/tool-gateway.test.ts b/server/src/__tests__/tool-gateway.test.ts index 473c3f4d1a8..bc5604c0423 100644 --- a/server/src/__tests__/tool-gateway.test.ts +++ b/server/src/__tests__/tool-gateway.test.ts @@ -3640,6 +3640,11 @@ rl.on("line", (line) => { .from(toolInvocations) .where(eq(toolInvocations.id, approvalRequest.invocationId)); expect(approvalInvocation).toMatchObject({ + companyId: company.id, + actorType: "agent", + agentId: agent.id, + runId: run.id, + issueId: issue.id, status: "awaiting_approval", policyDecision: "require_approval", connectionId: approvalTool.connection.id, @@ -3648,6 +3653,14 @@ rl.on("line", (line) => { upstreamToolName: "kv_set", }); + // Matching persisted company/connection rows must still be hidden from + // the Test tab when a real agent run owns the approval. + await expect(gateway.getTestCallStatus({ + companyId: company.id, + connectionId: approvalTool.connection.id, + actionRequestId: approvalRequest.id, + })).rejects.toMatchObject({ status: 404, reasonCode: "action_request_not_found" }); + await db .update(issueThreadInteractions) .set({ diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index acf53d2f896..418c9dad8df 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -2588,110 +2588,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod } }); - it( - "provisions worktree-local pnpm node_modules instead of reusing base-repo links", - async () => { - const repoRoot = await createTempRepo(); - await writeRegisteredSourceConfig(repoRoot); - await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); - await fs.mkdir(path.join(repoRoot, "packages", "shared"), { recursive: true }); - await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); - await fs.writeFile( - path.join(repoRoot, "package.json"), - JSON.stringify( - { - name: "workspace-root", - private: true, - packageManager: "pnpm@9.15.4", - }, - null, - 2, - ), - "utf8", - ); - await fs.writeFile( - path.join(repoRoot, "pnpm-workspace.yaml"), - ["packages:", " - packages/*", " - server", ""].join("\n"), - "utf8", - ); - await fs.writeFile( - path.join(repoRoot, "packages", "shared", "package.json"), - JSON.stringify( - { - name: "@repo/shared", - version: "1.0.0", - private: true, - type: "module", - exports: "./index.js", - }, - null, - 2, - ), - "utf8", - ); - await fs.writeFile(path.join(repoRoot, "packages", "shared", "index.js"), "export const value = 'shared';\n", "utf8"); - await fs.writeFile( - path.join(repoRoot, "server", "package.json"), - JSON.stringify( - { - name: "server", - private: true, - type: "module", - dependencies: { - "@repo/shared": "workspace:*", - }, - }, - null, - 2, - ), - "utf8", - ); - await fs.writeFile(path.join(repoRoot, "server", "index.js"), "export {};\n", "utf8"); - await fs.copyFile(provisionWorktreeScriptPath, path.join(repoRoot, "scripts", "provision-worktree.sh")); - await fs.chmod(path.join(repoRoot, "scripts", "provision-worktree.sh"), 0o755); - await runPnpm(repoRoot, ["install"]); - await runGit(repoRoot, ["add", "."]); - await runGit(repoRoot, ["commit", "-m", "Add pnpm workspace fixture"]); - const workspace = await realizeExecutionWorkspace({ - base: { - baseCwd: repoRoot, - source: "project_primary", - projectId: "project-1", - workspaceId: "workspace-1", - repoUrl: null, - repoRef: "HEAD", - }, - config: { - workspaceStrategy: { - type: "git_worktree", - branchTemplate: "{{issue.identifier}}-{{slug}}", - provisionCommand: "bash ./scripts/provision-worktree.sh", - }, - }, - issue: { - id: "issue-1", - identifier: "PAP-551", - title: "Provision local workspace dependencies", - }, - agent: { - id: "agent-1", - name: "Codex Coder", - companyId: "company-1", - }, - }); - - expect((await fs.lstat(path.join(workspace.cwd, "node_modules"))).isSymbolicLink()).toBe(false); - expect((await fs.lstat(path.join(workspace.cwd, "server", "node_modules"))).isSymbolicLink()).toBe(false); - await expect(fs.realpath(path.join(workspace.cwd, "server", "node_modules", "@repo", "shared"))).resolves.toBe( - await fs.realpath(path.join(workspace.cwd, "packages", "shared")), - ); - await expect(fs.realpath(path.join(repoRoot, "server", "node_modules", "@repo", "shared"))).resolves.toBe( - await fs.realpath(path.join(repoRoot, "packages", "shared")), - ); - }, - 15_000, - ); it("records worktree setup and provision operations when a recorder is provided", async () => { const repoRoot = await createTempRepo(); 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 a78296fc189..52844fcdd63 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -9561,6 +9561,44 @@ describe("native process ownership", () => { }); describe("runnerd provider runtime wiring", () => { + function scopedNativeExecution(suffix: string): NativeExecutionInputV1 { + return { + ...execution, + binding: { + ...execution.binding, + companyId: `company-${suffix}`, + runId: `run-${suffix}`, + agentId: `agent-${suffix}`, + executionWorkspaceId: `workspace-${suffix}`, + }, + session: { ...execution.session, normalizedSessionId: `session-${suffix}` }, + }; + } + + function priorNativeRunDb(priorExecution: NativeExecutionInputV1, status = "succeeded"): Db { + return { + select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ + status, + runnerProfileJson: { nativeExecutionInput: priorExecution }, + }] }) }) }), + } as unknown as Db; + } + + async function initializeScopedRunnerRoot(current: NativeExecutionInputV1, runnerInstanceId: string) { + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await createRunnerdBackend({ db: leaseDb(current), execution: current, runnerInstanceId }); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + return state.createTransport.mock.calls[0]![0].stateDirectory!; + } + + async function writeRunnerAuthority(root: string, identity: Record, lifecycle: string) { + await mkdir(join(root, "control-plane"), { recursive: true }); + await mkdir(join(root, "runner"), { recursive: true }); + await writeFile(join(root, "control-plane", "control-plane-state.json"), JSON.stringify(durableControlPlaneState(identity))); + await writeFile(join(root, "runner", "runner-state.json"), JSON.stringify(durableRunnerState(identity, lifecycle))); + } + let isolatedStateDirectory: string; let previousStateDirectory: string | undefined; @@ -9880,11 +9918,8 @@ describe("runnerd provider runtime wiring", () => { }); it("carries the verified runner and lease binding into a projectless continuation", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-runner-binding-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + + const prior = { ...execution, binding: { @@ -9907,120 +9942,102 @@ describe("runnerd provider runtime wiring", () => { }, } as NativeExecutionInputV1; const remoteCwd = "/home/daytona/paperclip-workspace"; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(prior), - execution: prior, - runnerInstanceId: "runner-projectless-stable", - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - const priorIdentity = { - runId: "run-projectless-prior", - normalizedSessionId: continuation.session.normalizedSessionId, - runnerInstanceId: "runner-projectless-stable", - environmentLeaseId: "lease-projectless-stable", - }; - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(priorIdentity)), - ); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(priorIdentity, "suspended")), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - state.execute.mockReset().mockResolvedValue({ - result: { summary: "completed" }, - terminal: { runTerminalState: "succeeded" }, - turnId: "turn", - normalizedSessionId: continuation.session.normalizedSessionId, - providerSessionId: null, - driverKind: "test", - driverVersion: "1", - nativeEventCount: 1, - highestContiguousSourceSeq: 1, - }); - const continuationDb = { - ...leaseDb(continuation), - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { nativeExecutionInput: prior }, - }, - ]), - }), + const scopedRoot = await initializeScopedRunnerRoot(prior, "runner-projectless-stable"); + await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + const priorIdentity = { + runId: "run-projectless-prior", + normalizedSessionId: continuation.session.normalizedSessionId, + runnerInstanceId: "runner-projectless-stable", + environmentLeaseId: "lease-projectless-stable", + }; + await writeFile( + join(scopedRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(priorIdentity)), + ); + await writeFile( + join(scopedRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(priorIdentity, "suspended")), + ); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + state.execute.mockReset().mockResolvedValue({ + result: { summary: "completed" }, + terminal: { runTerminalState: "succeeded" }, + turnId: "turn", + normalizedSessionId: continuation.session.normalizedSessionId, + providerSessionId: null, + driverKind: "test", + driverVersion: "1", + nativeEventCount: 1, + highestContiguousSourceSeq: 1, + }); + const continuationDb = { + ...leaseDb(continuation), + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => + Promise.resolve([ + { + status: "succeeded", + runnerProfileJson: { nativeExecutionInput: prior }, + }, + ]), }), }), - } as unknown as Db; + }), + } as unknown as Db; - await executePaperclipNativeSession({ - db: continuationDb, - execution: continuation, - runnerInstanceId: "runner-new-heartbeat", - useRunnerd: true, - runnerExecutionTarget: { - kind: "remote", - transport: "ssh", + await executePaperclipNativeSession({ + db: continuationDb, + execution: continuation, + runnerInstanceId: "runner-new-heartbeat", + useRunnerd: true, + runnerExecutionTarget: { + kind: "remote", + transport: "ssh", + remoteCwd, + spec: { + host: "runner.internal", + port: 22, + username: "runner", + remoteWorkspacePath: remoteCwd, remoteCwd, - spec: { - host: "runner.internal", - port: 22, - username: "runner", - remoteWorkspacePath: remoteCwd, - remoteCwd, - privateKey: null, - knownHosts: null, - strictHostKeyChecking: true, - }, + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, }, - }); - expect(state.createBackend).toHaveBeenCalledWith( - expect.objectContaining({ + }, + }); + expect(state.createBackend).toHaveBeenCalledWith( + expect.objectContaining({ + workspace: expect.objectContaining({ cwd: remoteCwd }), + }), + expect.objectContaining({ + workingDirectoryAuthority: "remote_runner", + }), + ); + expect(state.execute).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ workspace: expect.objectContaining({ cwd: remoteCwd }), }), - expect.objectContaining({ - workingDirectoryAuthority: "remote_runner", + requireSessionCloseBeforeReturn: true, + }), + ); + const backendOptions = state.createBackend.mock.calls[0]![1]; + backendOptions.codexTransportFactory!(); + expect(state.createTransport).toHaveBeenCalledWith( + expect.objectContaining({ + stateDirectory: scopedRoot, + prpIdentity: expect.objectContaining({ + runnerInstanceId: "runner-projectless-stable", + environmentLeaseId: "lease-projectless-stable", + runId: "run-projectless-next", }), - ); - expect(state.execute).toHaveBeenCalledWith( - expect.objectContaining({ - input: expect.objectContaining({ - workspace: expect.objectContaining({ cwd: remoteCwd }), - }), - requireSessionCloseBeforeReturn: true, - }), - ); - const backendOptions = state.createBackend.mock.calls[0]![1]; - backendOptions.codexTransportFactory!(); - expect(state.createTransport).toHaveBeenCalledWith( - expect.objectContaining({ - stateDirectory: scopedRoot, - prpIdentity: expect.objectContaining({ - runnerInstanceId: "runner-projectless-stable", - environmentLeaseId: "lease-projectless-stable", - runId: "run-projectless-next", - }), - }), - ); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + }), + ); }); it.each([ @@ -10194,11 +10211,8 @@ describe("runnerd provider runtime wiring", () => { }); it("atomically migrates legacy unscoped state only for its exact durable run identity", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-legacy-runner-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + const stateBase = isolatedStateDirectory; + const legacyExecution = { ...execution, binding: { @@ -10215,97 +10229,72 @@ describe("runnerd provider runtime wiring", () => { stateBase, createHash("sha256").update("session-legacy-state").digest("hex"), ); - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await mkdir(join(legacyRoot, "runner"), { recursive: true }); - const legacyIdentity = { - runId: "run-legacy-state", - normalizedSessionId: "session-legacy-state", + await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); + await mkdir(join(legacyRoot, "runner"), { recursive: true }); + const legacyIdentity = { + runId: "run-legacy-state", + normalizedSessionId: "session-legacy-state", + runnerInstanceId: "runner-legacy-state", + environmentLeaseId: "lease-legacy-state", + }; + await writeFile( + join(legacyRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(legacyIdentity)), + ); + await writeFile( + join(legacyRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(legacyIdentity, "ready")), + ); + state.createBackend.mockClear(); + await createRunnerdBackend({ + db: leaseDb(legacyExecution), + execution: legacyExecution, + runnerInstanceId: "runner-legacy-state", + }); + state.createTransport.mockClear(); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + const migratedRoot = + state.createTransport.mock.calls[0]![0].stateDirectory!; + expect(migratedRoot).not.toBe(legacyRoot); + await expect(access(legacyRoot)).rejects.toThrow(); + await expect( + access(join(migratedRoot, "control-plane", "control-plane-state.json")), + ).resolves.toBeUndefined(); + expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( + expect.objectContaining({ runnerInstanceId: "runner-legacy-state", environmentLeaseId: "lease-legacy-state", - }; - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(legacyIdentity)), - ); - await writeFile( - join(legacyRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(legacyIdentity, "ready")), - ); - state.createBackend.mockClear(); - await createRunnerdBackend({ - db: leaseDb(legacyExecution), - execution: legacyExecution, - runnerInstanceId: "runner-legacy-state", - }); - state.createTransport.mockClear(); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const migratedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - expect(migratedRoot).not.toBe(legacyRoot); - await expect(access(legacyRoot)).rejects.toThrow(); - await expect( - access(join(migratedRoot, "control-plane", "control-plane-state.json")), - ).resolves.toBeUndefined(); - expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( - expect.objectContaining({ - runnerInstanceId: "runner-legacy-state", - environmentLeaseId: "lease-legacy-state", - runId: "run-legacy-state", - }), - ); - expect(state.createTransport.mock.calls[0]![0].runnerBinary).toBe( - "/tmp/paperclip-runnerd", - ); - expect(state.resolveRunnerBinary).toHaveBeenCalled(); + runId: "run-legacy-state", + }), + ); + expect(state.createTransport.mock.calls[0]![0].runnerBinary).toBe( + "/tmp/paperclip-runnerd", + ); + expect(state.resolveRunnerBinary).toHaveBeenCalled(); - const unrelatedExecution = { - ...legacyExecution, - binding: { - ...legacyExecution.binding, - companyId: "company-unrelated-state", - runId: "run-unrelated-state", - }, - } as NativeExecutionInputV1; - await createRunnerdBackend({ - db: leaseDb(unrelatedExecution), - execution: unrelatedExecution, - runnerInstanceId: "runner-unrelated-state", - }); - state.createBackend.mock.calls[1]![1].codexTransportFactory!(); - expect(state.createTransport.mock.calls[1]![0].stateDirectory).not.toBe( - legacyRoot, - ); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + const unrelatedExecution = { + ...legacyExecution, + binding: { + ...legacyExecution.binding, + companyId: "company-unrelated-state", + runId: "run-unrelated-state", + }, + } as NativeExecutionInputV1; + await createRunnerdBackend({ + db: leaseDb(unrelatedExecution), + execution: unrelatedExecution, + runnerInstanceId: "runner-unrelated-state", + }); + state.createBackend.mock.calls[1]![1].codexTransportFactory!(); + expect(state.createTransport.mock.calls[1]![0].stateDirectory).not.toBe( + legacyRoot, + ); }); it("migrates the former company/session scope into the full native session scope", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-company-session-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const legacyExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-former-scope", - runId: "run-former-scope", - agentId: "agent-former-scope", - executionWorkspaceId: "workspace-former-scope", - }, - session: { - ...execution.session, - normalizedSessionId: "session-former-scope", - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const legacyExecution = scopedNativeExecution("former-scope"); const legacyRoot = join( stateBase, createHash("sha256") @@ -10317,69 +10306,34 @@ describe("runnerd provider runtime wiring", () => { ) .digest("hex"), ); - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await mkdir(join(legacyRoot, "runner"), { recursive: true }); - const legacyIdentity = { - runId: legacyExecution.binding.runId, - normalizedSessionId: legacyExecution.session.normalizedSessionId, - runnerInstanceId: "runner-former-scope", - environmentLeaseId: "lease-former-scope", - }; - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(legacyIdentity)), - ); - await writeFile( - join(legacyRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(legacyIdentity, "ready")), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - - await createRunnerdBackend({ - db: leaseDb(legacyExecution), - execution: legacyExecution, - runnerInstanceId: "runner-former-scope", - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const migratedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - expect(migratedRoot).not.toBe(legacyRoot); - await expect(access(legacyRoot)).rejects.toThrow(); - await expect( - access(join(migratedRoot, "control-plane", "control-plane-state.json")), - ).resolves.toBeUndefined(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); + await mkdir(join(legacyRoot, "runner"), { recursive: true }); + const legacyIdentity = { + runId: legacyExecution.binding.runId, + normalizedSessionId: legacyExecution.session.normalizedSessionId, + runnerInstanceId: "runner-former-scope", + environmentLeaseId: "lease-former-scope", + }; + await writeFile( + join(legacyRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(legacyIdentity)), + ); + await writeFile( + join(legacyRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(legacyIdentity, "ready")), + ); + const migratedRoot = await initializeScopedRunnerRoot(legacyExecution, "runner-former-scope"); + expect(migratedRoot).not.toBe(legacyRoot); + await expect(access(legacyRoot)).rejects.toThrow(); + await expect( + access(join(migratedRoot, "control-plane", "control-plane-state.json")), + ).resolves.toBeUndefined(); }); it.each([0, 2048])("migrates a suspended prior-run authority in the same full session scope with %i retained events", async (eventCount) => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-prior-run-session-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const priorExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-prior-run-scope", - runId: "run-prior-run-scope", - agentId: "agent-prior-run-scope", - executionWorkspaceId: "workspace-prior-run-scope", - }, - session: { - ...execution.session, - normalizedSessionId: "session-prior-run-scope", - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const priorExecution = scopedNativeExecution("prior-run-scope"); const currentExecution = { ...priorExecution, binding: { @@ -10398,119 +10352,94 @@ describe("runnerd provider runtime wiring", () => { ) .digest("hex"), ); - const priorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await mkdir(join(legacyRoot, "runner"), { recursive: true }); - const identity = { - runId: priorExecution.binding.runId, - normalizedSessionId: priorExecution.session.normalizedSessionId, - runnerInstanceId: "runner-prior-run-scope", - environmentLeaseId: "lease-prior-run-scope", - }; - const controlPlaneBytes = JSON.stringify({ - ...durableControlPlaneState(identity), - // The identity shares a file with the retained PRP event window. A - // verbose valid turn can exceed the old 2 MiB identity-read limit. - committedEvents: Array.from({ length: eventCount }, (_, index) => ({ - sourceSeq: index + 1, + const priorRunDb = priorNativeRunDb(priorExecution); + await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); + await mkdir(join(legacyRoot, "runner"), { recursive: true }); + const identity = { + runId: priorExecution.binding.runId, + normalizedSessionId: priorExecution.session.normalizedSessionId, + runnerInstanceId: "runner-prior-run-scope", + environmentLeaseId: "lease-prior-run-scope", + }; + const controlPlaneBytes = JSON.stringify({ + ...durableControlPlaneState(identity), + // The identity shares a file with the retained PRP event window. A + // verbose valid turn can exceed the old 2 MiB identity-read limit. + committedEvents: Array.from({ length: eventCount }, (_, index) => ({ + sourceSeq: index + 1, + sourceEventId: `event-${index + 1}`, + eventType: "item.delta", + priority: 1, + envelope: { + schema: "paperclip.prp.event.v1", + schemaVersion: 1, + sourceKind: "runner", + sourceInstanceId: identity.runnerInstanceId, sourceEventId: `event-${index + 1}`, + sourceSeq: index + 1, + normalizedSessionId: identity.normalizedSessionId, + runId: identity.runId, + turnId: "turn-prior-run-scope", + itemId: "item-prior-run-scope", eventType: "item.delta", priority: 1, - envelope: { - schema: "paperclip.prp.event.v1", - schemaVersion: 1, - sourceKind: "runner", - sourceInstanceId: identity.runnerInstanceId, - sourceEventId: `event-${index + 1}`, - sourceSeq: index + 1, - normalizedSessionId: identity.normalizedSessionId, - runId: identity.runId, - turnId: "turn-prior-run-scope", - itemId: "item-prior-run-scope", - eventType: "item.delta", - priority: 1, - emittedAt: "2026-09-24T00:00:00.000Z", - payload: { delta: "x".repeat(1024) }, + emittedAt: "2026-09-24T00:00:00.000Z", + payload: { delta: "x".repeat(1024) }, + }, + deliveryCount: 1, + logicalEffectCount: 1, + })), + }); + if (eventCount > 0) expect(Buffer.byteLength(controlPlaneBytes)).toBeGreaterThan(2 * 1024 * 1024); + await writeFile( + join(legacyRoot, "control-plane", "control-plane-state.json"), + controlPlaneBytes, + ); + await writeFile( + join(legacyRoot, "runner", "runner-state.json"), + JSON.stringify( + durableRunnerState( + { + runId: priorExecution.binding.runId, + normalizedSessionId: priorExecution.session.normalizedSessionId, + runnerInstanceId: "runner-prior-run-scope", + environmentLeaseId: "lease-prior-run-scope", }, - deliveryCount: 1, - logicalEffectCount: 1, - })), - }); - if (eventCount > 0) expect(Buffer.byteLength(controlPlaneBytes)).toBeGreaterThan(2 * 1024 * 1024); - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - controlPlaneBytes, - ); - await writeFile( - join(legacyRoot, "runner", "runner-state.json"), - JSON.stringify( - durableRunnerState( - { - runId: priorExecution.binding.runId, - normalizedSessionId: priorExecution.session.normalizedSessionId, - runnerInstanceId: "runner-prior-run-scope", - environmentLeaseId: "lease-prior-run-scope", - }, - "suspended", - ), + "suspended", ), - ); + ), + ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: priorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-current-run-scope", - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const migratedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - expect(migratedRoot).not.toBe(legacyRoot); - await expect(access(legacyRoot)).rejects.toThrow(); - // Re-enter through the canonical scoped root as ordinary continuation - // does, not only through the legacy migration path above. - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: priorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-current-run-scope", - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - expect(state.createTransport.mock.calls[0]![0].stateDirectory).toBe(migratedRoot); - expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( - expect.objectContaining({ - runId: currentExecution.binding.runId, - runnerInstanceId: "runner-prior-run-scope", - environmentLeaseId: "lease-prior-run-scope", - }), - ); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await createRunnerdBackend({ + db: priorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-current-run-scope", + }); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + const migratedRoot = + state.createTransport.mock.calls[0]![0].stateDirectory!; + expect(migratedRoot).not.toBe(legacyRoot); + await expect(access(legacyRoot)).rejects.toThrow(); + // Re-enter through the canonical scoped root as ordinary continuation + // does, not only through the legacy migration path above. + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await createRunnerdBackend({ + db: priorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-current-run-scope", + }); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + expect(state.createTransport.mock.calls[0]![0].stateDirectory).toBe(migratedRoot); + expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( + expect.objectContaining({ + runId: currentExecution.binding.runId, + runnerInstanceId: "runner-prior-run-scope", + environmentLeaseId: "lease-prior-run-scope", + }), + ); }); it.each([ @@ -10559,25 +10488,9 @@ describe("runnerd provider runtime wiring", () => { ] as const)( "uses remote prior-run backup when acceptance=$accepted direct=$directLifecycle backup=$backupLifecycle corrupt=$corrupt", async ({ directLifecycle, backupLifecycle, corrupt, accepted }) => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-remote-prior-run-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const priorExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-remote-prior-scope", - runId: "run-remote-prior-scope", - agentId: "agent-remote-prior-scope", - executionWorkspaceId: "workspace-remote-prior-scope", - }, - session: { - ...execution.session, - normalizedSessionId: "session-remote-prior-scope", - }, - } as NativeExecutionInputV1; + + + const priorExecution = scopedNativeExecution("remote-prior-scope"); const currentExecution = { ...priorExecution, binding: { @@ -10585,23 +10498,7 @@ describe("runnerd provider runtime wiring", () => { runId: "run-current-remote-scope", }, } as NativeExecutionInputV1; - const priorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; + const priorRunDb = priorNativeRunDb(priorExecution); const remoteTarget = { kind: "remote" as const, transport: "sandbox" as const, @@ -10618,135 +10515,110 @@ describe("runnerd provider runtime wiring", () => { runnerInstanceId: "runner-remote-prior-scope", environmentLeaseId: "lease-remote-prior-scope", }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(priorExecution), - execution: priorExecution, - runnerInstanceId: identity.runnerInstanceId, - runnerExecutionTarget: remoteTarget, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await createRunnerdBackend({ + db: leaseDb(priorExecution), + execution: priorExecution, + runnerInstanceId: identity.runnerInstanceId, + runnerExecutionTarget: remoteTarget, + }); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + const scopedRoot = + state.createTransport.mock.calls[0]![0].stateDirectory!; + await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + await writeFile( + join(scopedRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(identity)), + ); + if (directLifecycle === "empty") { + // Remote transports before the externally-owned-state fix left an + // empty local placeholder beside the controller state. It is not an + // authority record and must not mask a verified remote backup. + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + } else if (directLifecycle === "nonempty") { + await mkdir(join(scopedRoot, "runner"), { recursive: true }); await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), + join(scopedRoot, "runner", "unexpected-state.json"), + "{}", ); - if (directLifecycle === "empty") { - // Remote transports before the externally-owned-state fix left an - // empty local placeholder beside the controller state. It is not an - // authority record and must not mask a verified remote backup. - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - } else if (directLifecycle === "nonempty") { - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "runner", "unexpected-state.json"), - "{}", - ); - } else if (directLifecycle !== null) { - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify( - directLifecycle === "malformed" - ? { - ...durableRunnerState(identity, "suspended"), - runId: "conflicting-direct-run", - } - : durableRunnerState(identity, directLifecycle), - ), - ); - } - const backupRoot = join(scopedRoot, "failover-backups", "current"); - await mkdir(join(backupRoot, "runner"), { recursive: true }); - await mkdir(join(backupRoot, "codex-home"), { recursive: true }); + } else if (directLifecycle !== null) { + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + await writeFile( + join(scopedRoot, "runner", "runner-state.json"), + JSON.stringify( + directLifecycle === "malformed" + ? { + ...durableRunnerState(identity, "suspended"), + runId: "conflicting-direct-run", + } + : durableRunnerState(identity, directLifecycle), + ), + ); + } + const backupRoot = join(scopedRoot, "failover-backups", "current"); + await mkdir(join(backupRoot, "runner"), { recursive: true }); + await mkdir(join(backupRoot, "codex-home"), { recursive: true }); + await writeFile( + join(backupRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(identity, backupLifecycle)), + ); + const manifest = buildNativeHarnessBackupManifest({ + backupRoot, + execution: priorExecution, + runnerInstanceId: identity.runnerInstanceId, + providerSessionIdentity: { + providerSessionId: "provider-remote-prior-scope", + providerBackendSessionId: null, + providerSessionIdentity: null, + }, + sourceProviderLeaseId: "sandbox-remote-prior-scope", + }); + await writeFile( + join(backupRoot, "manifest.json"), + JSON.stringify(manifest), + ); + if (corrupt) { await writeFile( join(backupRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, backupLifecycle)), - ); - const manifest = buildNativeHarnessBackupManifest({ - backupRoot, - execution: priorExecution, - runnerInstanceId: identity.runnerInstanceId, - providerSessionIdentity: { - providerSessionId: "provider-remote-prior-scope", - providerBackendSessionId: null, - providerSessionIdentity: null, - }, - sourceProviderLeaseId: "sandbox-remote-prior-scope", - }); - await writeFile( - join(backupRoot, "manifest.json"), - JSON.stringify(manifest), - ); - if (corrupt) { - await writeFile( - join(backupRoot, "runner", "runner-state.json"), - JSON.stringify({ - ...durableRunnerState(identity, backupLifecycle), - x: 1, - }), - ); - } - state.createBackend.mockClear(); - state.createTransport.mockClear(); - - const continuation = createRunnerdBackend({ - db: priorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-current-remote-scope", - runnerExecutionTarget: remoteTarget, - }); - if (!accepted) { - await expect(continuation).rejects.toThrow( - "runner_state_identity_mismatch", - ); - expect(state.createBackend).not.toHaveBeenCalled(); - return; - } - await expect(continuation).resolves.toBeDefined(); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( - expect.objectContaining({ - runId: currentExecution.binding.runId, - runnerInstanceId: identity.runnerInstanceId, - environmentLeaseId: identity.environmentLeaseId, + JSON.stringify({ + ...durableRunnerState(identity, backupLifecycle), + x: 1, }), ); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); } + state.createBackend.mockClear(); + state.createTransport.mockClear(); + + const continuation = createRunnerdBackend({ + db: priorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-current-remote-scope", + runnerExecutionTarget: remoteTarget, + }); + if (!accepted) { + await expect(continuation).rejects.toThrow( + "runner_state_identity_mismatch", + ); + expect(state.createBackend).not.toHaveBeenCalled(); + return; + } + await expect(continuation).resolves.toBeDefined(); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + expect(state.createTransport.mock.calls[0]![0].prpIdentity).toEqual( + expect.objectContaining({ + runId: currentExecution.binding.runId, + runnerInstanceId: identity.runnerInstanceId, + environmentLeaseId: identity.environmentLeaseId, + }), + ); }, ); it("quarantines legacy prior-run state only after the database proves a terminal owner in the same full scope", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-legacy-terminal-unsuspended-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const priorExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-legacy-terminal-unsuspended", - runId: "run-legacy-terminal-unsuspended", - agentId: "agent-legacy-terminal-unsuspended", - executionWorkspaceId: "workspace-legacy-terminal-unsuspended", - }, - session: { - ...execution.session, - normalizedSessionId: "session-legacy-terminal-unsuspended", - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const priorExecution = scopedNativeExecution("legacy-terminal-unsuspended"); const currentExecution = { ...priorExecution, binding: { @@ -10765,86 +10637,36 @@ describe("runnerd provider runtime wiring", () => { ) .digest("hex"), ); - const terminalPriorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; + const terminalPriorRunDb = priorNativeRunDb(priorExecution); const identity = { runId: priorExecution.binding.runId, normalizedSessionId: priorExecution.session.normalizedSessionId, runnerInstanceId: "runner-legacy-terminal-unsuspended", environmentLeaseId: "lease-legacy-terminal-unsuspended", }; - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await mkdir(join(legacyRoot, "runner"), { recursive: true }); - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - await writeFile( - join(legacyRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "ready")), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); + await writeRunnerAuthority(legacyRoot, identity, "ready"); + state.createBackend.mockClear(); + state.createTransport.mockClear(); - await expect( - createRunnerdBackend({ - db: terminalPriorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-after-legacy-terminal-unsuspended", - }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(legacyRoot)).rejects.toThrow(); - const quarantineEntries = await readdir(join(stateBase, "quarantine")); - expect(quarantineEntries).toHaveLength(1); - expect(quarantineEntries[0]).toContain(".identity_indeterminate."); - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + await expect( + createRunnerdBackend({ + db: terminalPriorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-after-legacy-terminal-unsuspended", + }), + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(legacyRoot)).rejects.toThrow(); + const quarantineEntries = await readdir(join(stateBase, "quarantine")); + expect(quarantineEntries).toHaveLength(1); + expect(quarantineEntries[0]).toContain(".identity_indeterminate."); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }); it("rejects scoped prior-run state after restart while its heartbeat is still running", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-running-prior-run-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const priorExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-running-prior-scope", - runId: "run-running-prior-scope", - agentId: "agent-running-prior-scope", - executionWorkspaceId: "workspace-running-prior-scope", - }, - session: { - ...execution.session, - normalizedSessionId: "session-running-prior-scope", - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const priorExecution = scopedNativeExecution("running-prior-scope"); const currentExecution = { ...priorExecution, binding: { @@ -10852,72 +10674,29 @@ describe("runnerd provider runtime wiring", () => { runId: "run-after-running-prior-scope", }, } as NativeExecutionInputV1; - const runningPriorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "running", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; + const runningPriorRunDb = priorNativeRunDb(priorExecution, "running"); const identity = { runId: priorExecution.binding.runId, normalizedSessionId: priorExecution.session.normalizedSessionId, runnerInstanceId: "runner-running-prior-scope", environmentLeaseId: "lease-running-prior-scope", }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(priorExecution), - execution: priorExecution, - runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "suspended")), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); + const scopedRoot = await initializeScopedRunnerRoot(priorExecution, identity.runnerInstanceId); + await writeRunnerAuthority(scopedRoot, identity, "suspended"); + state.createBackend.mockClear(); + state.createTransport.mockClear(); - await expect( - createRunnerdBackend({ - db: runningPriorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-after-running-prior-scope", - }), - ).rejects.toThrow("runner_state_identity_mismatch: prior_owner_active"); - await expect(access(scopedRoot)).resolves.toBeUndefined(); - await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + await expect( + createRunnerdBackend({ + db: runningPriorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-after-running-prior-scope", + }), + ).rejects.toThrow("runner_state_identity_mismatch: prior_owner_active"); + await expect(access(scopedRoot)).resolves.toBeUndefined(); + await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }); it.each([ @@ -10947,11 +10726,8 @@ describe("runnerd provider runtime wiring", () => { ])( "automatically recovers only a proven settled local session: %s", async (scenario) => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-quiescent-recovery-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + const stateBase = isolatedStateDirectory; + const priorExecution = { ...execution, binding: { @@ -11063,16 +10839,7 @@ describe("runnerd provider runtime wiring", () => { }), } as unknown as Db; try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(priorExecution), - execution: priorExecution, - runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; + const scopedRoot = await initializeScopedRunnerRoot(priorExecution, identity.runnerInstanceId); const quarantineRoot = join(stateBase, "quarantine"); await mkdir(quarantineRoot, { recursive: true }); const candidate = @@ -11228,34 +10995,15 @@ describe("runnerd provider runtime wiring", () => { } } finally { processKill.mockRestore(); - if (previousStateDirectory === undefined) - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - else process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - await rm(stateBase, { recursive: true, force: true }); + } }, ); it("quarantines scoped prior-run state when the heartbeat is terminal but runnerd is not suspended", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-terminal-unsuspended-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const priorExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-terminal-unsuspended", - runId: "run-terminal-unsuspended", - agentId: "agent-terminal-unsuspended", - executionWorkspaceId: "workspace-terminal-unsuspended", - }, - session: { - ...execution.session, - normalizedSessionId: "session-terminal-unsuspended", - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const priorExecution = scopedNativeExecution("terminal-unsuspended"); const currentExecution = { ...priorExecution, binding: { @@ -11263,251 +11011,174 @@ describe("runnerd provider runtime wiring", () => { runId: "run-after-terminal-unsuspended", }, } as NativeExecutionInputV1; - const terminalPriorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; + const terminalPriorRunDb = priorNativeRunDb(priorExecution); const identity = { runId: priorExecution.binding.runId, normalizedSessionId: priorExecution.session.normalizedSessionId, runnerInstanceId: "runner-terminal-unsuspended", environmentLeaseId: "lease-terminal-unsuspended", }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(priorExecution), - execution: priorExecution, - runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "ready")), - ); - await mkdir(join(scopedRoot, "codex-home", "sessions"), { - recursive: true, - }); - await mkdir(join(scopedRoot, "codex-home", "tmp"), { recursive: true }); - await mkdir(join(scopedRoot, "codex-home", ".tmp"), { - recursive: true, - }); - await writeFile( - join(scopedRoot, "codex-home", "auth.json"), - '{"OPENAI_API_KEY":"fixture-secret"}', - ); - await writeFile( - join(scopedRoot, "codex-home", "config.toml"), - 'bearer_token = "fixture-secret"', - ); - await writeFile( - join(scopedRoot, "codex-home", "tmp", "transient"), - "transient", - ); - await writeFile( - join(scopedRoot, "codex-home", ".tmp", "transient"), - "transient", - ); - await writeFile( - join(scopedRoot, "codex-home", "sessions", "rollout.jsonl"), - "durable session history", - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); + const scopedRoot = await initializeScopedRunnerRoot(priorExecution, identity.runnerInstanceId); + await writeRunnerAuthority(scopedRoot, identity, "ready"); + await mkdir(join(scopedRoot, "codex-home", "sessions"), { + recursive: true, + }); + await mkdir(join(scopedRoot, "codex-home", "tmp"), { recursive: true }); + await mkdir(join(scopedRoot, "codex-home", ".tmp"), { + recursive: true, + }); + await writeFile( + join(scopedRoot, "codex-home", "auth.json"), + '{"OPENAI_API_KEY":"fixture-secret"}', + ); + await writeFile( + join(scopedRoot, "codex-home", "config.toml"), + 'bearer_token = "fixture-secret"', + ); + await writeFile( + join(scopedRoot, "codex-home", "tmp", "transient"), + "transient", + ); + await writeFile( + join(scopedRoot, "codex-home", ".tmp", "transient"), + "transient", + ); + await writeFile( + join(scopedRoot, "codex-home", "sessions", "rollout.jsonl"), + "durable session history", + ); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await expect( + createRunnerdBackend({ + db: terminalPriorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-after-terminal-unsuspended", + }), + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(scopedRoot)).rejects.toThrow(); + const quarantineEntries = await readdir(join(stateBase, "quarantine")); + expect(quarantineEntries).toHaveLength(1); + expect(quarantineEntries[0]).toContain(".identity_indeterminate."); + const quarantinedRoot = join( + stateBase, + "quarantine", + quarantineEntries[0]!, + ); + for (const entry of ["tmp", ".tmp", "auth.json", "config.toml"]) { await expect( - createRunnerdBackend({ - db: terminalPriorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-after-terminal-unsuspended", - }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(scopedRoot)).rejects.toThrow(); - const quarantineEntries = await readdir(join(stateBase, "quarantine")); - expect(quarantineEntries).toHaveLength(1); - expect(quarantineEntries[0]).toContain(".identity_indeterminate."); - const quarantinedRoot = join( - stateBase, - "quarantine", - quarantineEntries[0]!, - ); - for (const entry of ["tmp", ".tmp", "auth.json", "config.toml"]) { - await expect( - access(join(quarantinedRoot, "codex-home", entry)), - ).rejects.toThrow(); - } - await expect( - access( - join(quarantinedRoot, "codex-home", "sessions", "rollout.jsonl"), - ), - ).resolves.toBeUndefined(); - await expect( - access( - join(quarantinedRoot, "control-plane", "control-plane-state.json"), - ), - ).resolves.toBeUndefined(); - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); + access(join(quarantinedRoot, "codex-home", entry)), + ).rejects.toThrow(); } + await expect( + access( + join(quarantinedRoot, "codex-home", "sessions", "rollout.jsonl"), + ), + ).resolves.toBeUndefined(); + await expect( + access( + join(quarantinedRoot, "control-plane", "control-plane-state.json"), + ), + ).resolves.toBeUndefined(); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }); it("resumes a matching scoped authority with valid history above 64 MiB", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-current-scoped-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const currentExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-current-scoped-state", - runId: "run-current-scoped-state", - agentId: "agent-current-scoped-state", - executionWorkspaceId: "workspace-current-scoped-state", - }, - session: { - ...execution.session, - normalizedSessionId: "session-current-scoped-state", - }, - } as NativeExecutionInputV1; + + + const currentExecution = scopedNativeExecution("current-scoped-state"); const identity = { runId: currentExecution.binding.runId, normalizedSessionId: currentExecution.session.normalizedSessionId, runnerInstanceId: "runner-current-scoped-state", environmentLeaseId: "lease-current-scoped-state", }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(currentExecution), - execution: currentExecution, - runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - const controlPlaneStatePath = join( - scopedRoot, - "control-plane", - "control-plane-state.json", - ); - const stateWithHistory = JSON.stringify({ - ...durableControlPlaneState(identity), - committedEvents: [], - }); - const committedEventsMarker = '"committedEvents":[]'; - const eventsStart = stateWithHistory.indexOf(committedEventsMarker); - expect(eventsStart).toBeGreaterThanOrEqual(0); - const eventArrayStart = eventsStart + '"committedEvents":'.length; - const historyPrefix = stateWithHistory.slice(0, eventArrayStart + 1); - const historySuffix = stateWithHistory.slice(eventArrayStart + 2); - const payloadBytesPerEvent = 512 * 1024; - const eventCount = 128; - const delta = "x".repeat(payloadBytesPerEvent); - const event = (sourceSeq: number) => { - const sourceEventId = `event-current-scoped-state-${sourceSeq}`; - return JSON.stringify({ - sourceSeq, + const scopedRoot = await initializeScopedRunnerRoot(currentExecution, identity.runnerInstanceId); + await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + const controlPlaneStatePath = join( + scopedRoot, + "control-plane", + "control-plane-state.json", + ); + const stateWithHistory = JSON.stringify({ + ...durableControlPlaneState(identity), + committedEvents: [], + }); + const committedEventsMarker = '"committedEvents":[]'; + const eventsStart = stateWithHistory.indexOf(committedEventsMarker); + expect(eventsStart).toBeGreaterThanOrEqual(0); + const eventArrayStart = eventsStart + '"committedEvents":'.length; + const historyPrefix = stateWithHistory.slice(0, eventArrayStart + 1); + const historySuffix = stateWithHistory.slice(eventArrayStart + 2); + const payloadBytesPerEvent = 512 * 1024; + const eventCount = 128; + const delta = "x".repeat(payloadBytesPerEvent); + const event = (sourceSeq: number) => { + const sourceEventId = `event-current-scoped-state-${sourceSeq}`; + return JSON.stringify({ + sourceSeq, + sourceEventId, + eventType: "item.delta", + priority: 1, + envelope: { + schema: "paperclip.prp.event.v1", + schemaVersion: 1, + sourceKind: "runner", + sourceInstanceId: identity.runnerInstanceId, sourceEventId, + sourceSeq, + normalizedSessionId: identity.normalizedSessionId, + runId: identity.runId, + turnId: "turn-current-scoped-state", + itemId: "item-current-scoped-state", eventType: "item.delta", priority: 1, - envelope: { - schema: "paperclip.prp.event.v1", - schemaVersion: 1, - sourceKind: "runner", - sourceInstanceId: identity.runnerInstanceId, - sourceEventId, - sourceSeq, - normalizedSessionId: identity.normalizedSessionId, - runId: identity.runId, - turnId: "turn-current-scoped-state", - itemId: "item-current-scoped-state", - eventType: "item.delta", - priority: 1, - emittedAt: "2026-09-30T00:00:00.000Z", - payload: { delta }, - }, - deliveryCount: 1, - logicalEffectCount: 1, - }); - }; - await writeFile(controlPlaneStatePath, historyPrefix); - const stateHandle = await open(controlPlaneStatePath, "a"); - try { - for (let index = 0; index < eventCount; index += 1) { - if (index > 0) await stateHandle.write(","); - await stateHandle.write(event(index + 1)); - } - await stateHandle.write("]"); - await stateHandle.write(historySuffix); - } finally { - await stateHandle.close(); + emittedAt: "2026-09-30T00:00:00.000Z", + payload: { delta }, + }, + deliveryCount: 1, + logicalEffectCount: 1, + }); + }; + await writeFile(controlPlaneStatePath, historyPrefix); + const stateHandle = await open(controlPlaneStatePath, "a"); + try { + for (let index = 0; index < eventCount; index += 1) { + if (index > 0) await stateHandle.write(","); + await stateHandle.write(event(index + 1)); } - expect((await lstat(controlPlaneStatePath)).size).toBeGreaterThan( - 64 * 1024 * 1024, - ); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "ready")), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - - await expect( - createRunnerdBackend({ - db: leaseDb(currentExecution), - execution: currentExecution, - runnerInstanceId: "runner-restart-placeholder", - }), - ).resolves.toBeDefined(); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - expect(state.createTransport).toHaveBeenCalledWith( - expect.objectContaining({ - stateDirectory: scopedRoot, - prpIdentity: expect.objectContaining(identity), - }), - ); + await stateHandle.write("]"); + await stateHandle.write(historySuffix); } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); + await stateHandle.close(); } + expect((await lstat(controlPlaneStatePath)).size).toBeGreaterThan( + 64 * 1024 * 1024, + ); + await writeFile( + join(scopedRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(identity, "ready")), + ); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + + await expect( + createRunnerdBackend({ + db: leaseDb(currentExecution), + execution: currentExecution, + runnerInstanceId: "runner-restart-placeholder", + }), + ).resolves.toBeDefined(); + state.createBackend.mock.calls[0]![1].codexTransportFactory!(); + expect(state.createTransport).toHaveBeenCalledWith( + expect.objectContaining({ + stateDirectory: scopedRoot, + prpIdentity: expect.objectContaining(identity), + }), + ); }); it.each([ @@ -11522,11 +11193,8 @@ describe("runnerd provider runtime wiring", () => { ] as const)( "preserves unadmitted forward warm-transition evidence (%s)", async (variant) => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-pending-warm-transition-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + const stateBase = isolatedStateDirectory; + const currentExecution = { ...execution, binding: { @@ -11547,186 +11215,137 @@ describe("runnerd provider runtime wiring", () => { runnerInstanceId: `warm-runner-${variant}`, environmentLeaseId: currentExecution.binding.executionWorkspaceId, }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ + const root = await initializeScopedRunnerRoot(currentExecution, identity.runnerInstanceId); + await mkdir(join(root, "control-plane"), { recursive: true }); + await mkdir(join(root, "runner"), { recursive: true }); + await mkdir(join(root, "codex-home"), { recursive: true }); + // These are deliberately unadmitted selectors, not an invented valid + // receipt or a forged process-retirement claim. Even invalid/unsupported + // forward evidence must never fall through the legacy quarantine path. + const pending = { + phase: variant === "awaiting_result" ? "awaiting_result" : "prepared", + receipt: { + schema: "paperclip.runner.warm-transition.v1", + newIdentity: { + ...identity, + runId: + variant === "foreign_scope" ? "foreign-run" : identity.runId, + }, + leaseExpiresAtUnixMs: + variant === "expired" ? 1 : Date.now() + 60_000, + }, + credentialId: "unadmitted-credential", + }; + const core = + variant === "runner_prepared" + ? durableControlPlaneState(identity) + : { + ...durableControlPlaneState(identity), + schema: + "paperclip.runner.durable.control-plane-state.warm-transition.v1", + ...(variant === "schema_only" + ? {} + : { warmTransition: pending }), + leases: { + "unadmitted-credential": { + revokedAt: + variant === "revoked" ? new Date().toISOString() : null, + }, + }, + }; + const runner = { + ...durableRunnerState(identity, "ready"), + schema: "paperclip.runner.durable.state.warm-transition.v1", + warmTransition: pending, + }; + const coreBytes = + variant === "malformed" + ? '{"schema":"paperclip.runner.durable.control-plane-state.warm-transition.v1",' + : JSON.stringify(core); + const runnerBytes = JSON.stringify(runner); + const corePath = join( + root, + "control-plane", + "control-plane-state.json", + ); + const runnerPath = join(root, "runner", "runner-state.json"); + const launchMaterial = join(root, "codex-home", "config.toml"); + await writeFile(corePath, coreBytes); + await writeFile(runnerPath, runnerBytes); + await writeFile( + launchMaterial, + "fixture launch material must remain untouched\n", + ); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + await expect( + createRunnerdBackend({ db: leaseDb(currentExecution), execution: currentExecution, runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const root = state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(root, "control-plane"), { recursive: true }); - await mkdir(join(root, "runner"), { recursive: true }); - await mkdir(join(root, "codex-home"), { recursive: true }); - // These are deliberately unadmitted selectors, not an invented valid - // receipt or a forged process-retirement claim. Even invalid/unsupported - // forward evidence must never fall through the legacy quarantine path. - const pending = { - phase: variant === "awaiting_result" ? "awaiting_result" : "prepared", - receipt: { - schema: "paperclip.runner.warm-transition.v1", - newIdentity: { - ...identity, - runId: - variant === "foreign_scope" ? "foreign-run" : identity.runId, - }, - leaseExpiresAtUnixMs: - variant === "expired" ? 1 : Date.now() + 60_000, - }, - credentialId: "unadmitted-credential", - }; - const core = - variant === "runner_prepared" - ? durableControlPlaneState(identity) - : { - ...durableControlPlaneState(identity), - schema: - "paperclip.runner.durable.control-plane-state.warm-transition.v1", - ...(variant === "schema_only" - ? {} - : { warmTransition: pending }), - leases: { - "unadmitted-credential": { - revokedAt: - variant === "revoked" ? new Date().toISOString() : null, - }, - }, - }; - const runner = { - ...durableRunnerState(identity, "ready"), - schema: "paperclip.runner.durable.state.warm-transition.v1", - warmTransition: pending, - }; - const coreBytes = - variant === "malformed" - ? '{"schema":"paperclip.runner.durable.control-plane-state.warm-transition.v1",' - : JSON.stringify(core); - const runnerBytes = JSON.stringify(runner); - const corePath = join( - root, - "control-plane", - "control-plane-state.json", - ); - const runnerPath = join(root, "runner", "runner-state.json"); - const launchMaterial = join(root, "codex-home", "config.toml"); - await writeFile(corePath, coreBytes); - await writeFile(runnerPath, runnerBytes); - await writeFile( - launchMaterial, - "fixture launch material must remain untouched\n", - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await expect( - createRunnerdBackend({ - db: leaseDb(currentExecution), - execution: currentExecution, - runnerInstanceId: identity.runnerInstanceId, - }), - ).rejects.toThrow("native_runner_warm_transition_recovery_unproven"); - expect(await readFile(corePath, "utf8")).toBe(coreBytes); - expect(await readFile(runnerPath, "utf8")).toBe(runnerBytes); - expect(await readFile(launchMaterial, "utf8")).toBe( - "fixture launch material must remain untouched\n", - ); - await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - else process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - await rm(stateBase, { recursive: true, force: true }); - } + }), + ).rejects.toThrow("native_runner_warm_transition_recovery_unproven"); + expect(await readFile(corePath, "utf8")).toBe(coreBytes); + expect(await readFile(runnerPath, "utf8")).toBe(runnerBytes); + expect(await readFile(launchMaterial, "utf8")).toBe( + "fixture launch material must remain untouched\n", + ); + await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }, ); it.each(["unknown_schema", "unknown_lifecycle"] as const)( "quarantines an exact-run runner state with %s", async (caseName) => { - const stateBase = await mkdtemp( - join(tmpdir(), `paperclip-${caseName}-runner-state-`), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const currentExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: `company-${caseName}-runner-state`, - runId: `run-${caseName}-runner-state`, - agentId: `agent-${caseName}-runner-state`, - executionWorkspaceId: `workspace-${caseName}-runner-state`, - }, - session: { - ...execution.session, - normalizedSessionId: `session-${caseName}-runner-state`, - }, - } as NativeExecutionInputV1; + const stateBase = isolatedStateDirectory; + + const currentExecution = scopedNativeExecution(`${caseName}-runner-state`); const identity = { runId: currentExecution.binding.runId, normalizedSessionId: currentExecution.session.normalizedSessionId, runnerInstanceId: `runner-${caseName}-runner-state`, environmentLeaseId: currentExecution.binding.executionWorkspaceId, }; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ + const scopedRoot = await initializeScopedRunnerRoot(currentExecution, identity.runnerInstanceId); + await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + await writeFile( + join(scopedRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(identity)), + ); + const runnerState = durableRunnerState( + identity, + caseName === "unknown_lifecycle" ? "future_lifecycle" : "ready", + ); + await writeFile( + join(scopedRoot, "runner", "runner-state.json"), + JSON.stringify( + caseName === "unknown_schema" + ? { + ...runnerState, + schema: "paperclip.runner.durable.state.v999", + } + : runnerState, + ), + ); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + + await expect( + createRunnerdBackend({ db: leaseDb(currentExecution), execution: currentExecution, - runnerInstanceId: identity.runnerInstanceId, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - const runnerState = durableRunnerState( - identity, - caseName === "unknown_lifecycle" ? "future_lifecycle" : "ready", - ); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify( - caseName === "unknown_schema" - ? { - ...runnerState, - schema: "paperclip.runner.durable.state.v999", - } - : runnerState, - ), - ); - state.createBackend.mockClear(); - state.createTransport.mockClear(); - - await expect( - createRunnerdBackend({ - db: leaseDb(currentExecution), - execution: currentExecution, - runnerInstanceId: `runner-${caseName}-retry`, - }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(scopedRoot)).rejects.toThrow(); - const quarantineEntries = await readdir(join(stateBase, "quarantine")); - expect(quarantineEntries).toHaveLength(1); - expect(quarantineEntries[0]).toContain(".identity_indeterminate."); - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + runnerInstanceId: `runner-${caseName}-retry`, + }), + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(scopedRoot)).rejects.toThrow(); + const quarantineEntries = await readdir(join(stateBase, "quarantine")); + expect(quarantineEntries).toHaveLength(1); + expect(quarantineEntries[0]).toContain(".identity_indeterminate."); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }, ); @@ -11740,209 +11359,97 @@ describe("runnerd provider runtime wiring", () => { ] as const)( "fails closed on %s durable identity in an existing scoped root", async (caseName) => { - const stateBase = await mkdtemp( - join(tmpdir(), `paperclip-${caseName}-scoped-state-`), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const scopedExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: `company-${caseName}-scoped-state`, - runId: `run-${caseName}-scoped-state`, - agentId: `agent-${caseName}-scoped-state`, - executionWorkspaceId: `workspace-${caseName}-scoped-state`, - }, - session: { - ...execution.session, - normalizedSessionId: `session-${caseName}-scoped-state`, - }, - } as NativeExecutionInputV1; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(scopedExecution), - execution: scopedExecution, - runnerInstanceId: `runner-${caseName}-scoped-state`, - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); - if (caseName !== "missing") { - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - caseName === "malformed" - ? "{" - : caseName === "unknown_schema" - ? JSON.stringify({ - ...durableControlPlaneState({ - runId: scopedExecution.binding.runId, - normalizedSessionId: - scopedExecution.session.normalizedSessionId, - runnerInstanceId: `runner-${caseName}-scoped-state`, - environmentLeaseId: - scopedExecution.binding.executionWorkspaceId, - }), - schema: "paperclip.runner.durable.control-plane-state.v999", - }) - : JSON.stringify( - durableControlPlaneState({ - runId: scopedExecution.binding.runId, - normalizedSessionId: "session-owned-by-another-scope", - runnerInstanceId: "runner-owned-by-another-scope", - environmentLeaseId: "lease-owned-by-another-scope", - }), - ), - ); - } - if (caseName === "large_mismatched") { - await writeFile( - join(scopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify({ - ...durableControlPlaneState({ - runId: scopedExecution.binding.runId, - normalizedSessionId: "session-owned-by-another-scope", - runnerInstanceId: "runner-owned-by-another-scope", - environmentLeaseId: "lease-owned-by-another-scope", - }), - committedEvents: [ - { - eventType: "history", - payload: { text: "x".repeat(64 * 1024 * 1024 + 1) }, - }, - ], - }), - ); - } - if (caseName === "oversized") { - const identity = { - runId: scopedExecution.binding.runId, - normalizedSessionId: scopedExecution.session.normalizedSessionId, - runnerInstanceId: `runner-${caseName}-scoped-state`, - environmentLeaseId: scopedExecution.binding.executionWorkspaceId, - }; - // Keep the file valid JSON so only the byte limit rejects it. Append - // bounded whitespace chunks to avoid a 256 MiB test allocation. - const statePath = join( - scopedRoot, - "control-plane", - "control-plane-state.json", - ); - const serializedState = JSON.stringify( - durableControlPlaneState(identity), - ); - await writeFile( - statePath, - serializedState, - ); - const padding = Buffer.alloc(1024 * 1024, 0x20); - const remainingBytes = - 256 * 1024 * 1024 + 1 - Buffer.byteLength(serializedState); - const stateHandle = await open(statePath, "a"); - try { - for (let remaining = remainingBytes; remaining > 0;) { - const bytesToWrite = Math.min(remaining, padding.length); - await stateHandle.write(padding, 0, bytesToWrite); - remaining -= bytesToWrite; - } - } finally { - await stateHandle.close(); - } - await mkdir(join(scopedRoot, "runner"), { recursive: true }); - await writeFile( - join(scopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "ready")), - ); - } - state.createBackend.mockClear(); - state.createTransport.mockClear(); + const stateBase = isolatedStateDirectory; - await expect( - createRunnerdBackend({ - db: leaseDb(scopedExecution), - execution: scopedExecution, - runnerInstanceId: `runner-${caseName}-retry`, + const scopedExecution = scopedNativeExecution(`${caseName}-scoped-state`); + const scopedRoot = await initializeScopedRunnerRoot(scopedExecution, `runner-${caseName}-scoped-state`); + await mkdir(join(scopedRoot, "control-plane"), { recursive: true }); + if (caseName !== "missing") { + await writeFile( + join(scopedRoot, "control-plane", "control-plane-state.json"), + caseName === "malformed" + ? "{" + : caseName === "unknown_schema" + ? JSON.stringify({ + ...durableControlPlaneState({ + runId: scopedExecution.binding.runId, + normalizedSessionId: + scopedExecution.session.normalizedSessionId, + runnerInstanceId: `runner-${caseName}-scoped-state`, + environmentLeaseId: + scopedExecution.binding.executionWorkspaceId, + }), + schema: "paperclip.runner.durable.control-plane-state.v999", + }) + : JSON.stringify( + durableControlPlaneState({ + runId: scopedExecution.binding.runId, + normalizedSessionId: "session-owned-by-another-scope", + runnerInstanceId: "runner-owned-by-another-scope", + environmentLeaseId: "lease-owned-by-another-scope", + }), + ), + ); + } + if (caseName === "large_mismatched") { + await writeFile( + join(scopedRoot, "control-plane", "control-plane-state.json"), + JSON.stringify({ + ...durableControlPlaneState({ + runId: scopedExecution.binding.runId, + normalizedSessionId: "session-owned-by-another-scope", + runnerInstanceId: "runner-owned-by-another-scope", + environmentLeaseId: "lease-owned-by-another-scope", + }), + committedEvents: [ + { + eventType: "history", + payload: { text: "x".repeat(64 * 1024 * 1024 + 1) }, + }, + ], }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(scopedRoot)).rejects.toThrow(); - const quarantineRoot = join(stateBase, "quarantine"); - const quarantineEntries = await readdir(quarantineRoot, { - withFileTypes: true, - }); - expect(quarantineEntries).toHaveLength(1); - expect(quarantineEntries[0]!.isDirectory()).toBe(true); - expect(quarantineEntries[0]!.name).toContain( - caseName === "mismatched" || caseName === "large_mismatched" - ? ".identity_mismatch." - : ".identity_indeterminate.", + ); + } + if (caseName === "oversized") { + const identity = { + runId: scopedExecution.binding.runId, + normalizedSessionId: scopedExecution.session.normalizedSessionId, + runnerInstanceId: `runner-${caseName}-scoped-state`, + environmentLeaseId: scopedExecution.binding.executionWorkspaceId, + }; + // Keep the file valid JSON so only the byte limit rejects it. Append + // bounded whitespace chunks to avoid a 256 MiB test allocation. + const statePath = join( + scopedRoot, + "control-plane", + "control-plane-state.json", + ); + const serializedState = JSON.stringify( + durableControlPlaneState(identity), + ); + await writeFile( + statePath, + serializedState, + ); + const padding = Buffer.alloc(1024 * 1024, 0x20); + const remainingBytes = + 256 * 1024 * 1024 + 1 - Buffer.byteLength(serializedState); + const stateHandle = await open(statePath, "a"); + try { + for (let remaining = remainingBytes; remaining > 0;) { + const bytesToWrite = Math.min(remaining, padding.length); + await stateHandle.write(padding, 0, bytesToWrite); + remaining -= bytesToWrite; + } + } finally { + await stateHandle.close(); + } + await mkdir(join(scopedRoot, "runner"), { recursive: true }); + await writeFile( + join(scopedRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(identity, "ready")), ); - const quarantinedControlPlaneRoot = join( - quarantineRoot, - quarantineEntries[0]!.name, - "control-plane", - ); - await expect( - access(quarantinedControlPlaneRoot), - ).resolves.toBeUndefined(); - if (caseName !== "missing") { - await expect( - access( - join(quarantinedControlPlaneRoot, "control-plane-state.json"), - ), - ).resolves.toBeUndefined(); - } - expect(state.createBackend).not.toHaveBeenCalled(); - expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); } - }, - ); - - it("does not quarantine an unsafe scoped-root symlink", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-symlink-scoped-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; - const scopedExecution = { - ...execution, - binding: { - ...execution.binding, - companyId: "company-symlink-scoped-state", - runId: "run-symlink-scoped-state", - agentId: "agent-symlink-scoped-state", - executionWorkspaceId: "workspace-symlink-scoped-state", - }, - session: { - ...execution.session, - normalizedSessionId: "session-symlink-scoped-state", - }, - } as NativeExecutionInputV1; - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - await createRunnerdBackend({ - db: leaseDb(scopedExecution), - execution: scopedExecution, - runnerInstanceId: "runner-symlink-scoped-state", - }); - state.createBackend.mock.calls[0]![1].codexTransportFactory!(); - const scopedRoot = - state.createTransport.mock.calls[0]![0].stateDirectory!; - const symlinkTarget = join(stateBase, "symlink-target"); - await rm(scopedRoot, { recursive: true, force: true }); - await mkdir(symlinkTarget, { recursive: true }); - await writeFile(join(symlinkTarget, "must-remain"), "retained"); - await symlink(symlinkTarget, scopedRoot); state.createBackend.mockClear(); state.createTransport.mockClear(); @@ -11950,32 +11457,73 @@ describe("runnerd provider runtime wiring", () => { createRunnerdBackend({ db: leaseDb(scopedExecution), execution: scopedExecution, - runnerInstanceId: "runner-symlink-retry", + runnerInstanceId: `runner-${caseName}-retry`, }), - ).rejects.toThrow("runner_state_directory_unsafe"); - await expect(access(scopedRoot)).resolves.toBeUndefined(); + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(scopedRoot)).rejects.toThrow(); + const quarantineRoot = join(stateBase, "quarantine"); + const quarantineEntries = await readdir(quarantineRoot, { + withFileTypes: true, + }); + expect(quarantineEntries).toHaveLength(1); + expect(quarantineEntries[0]!.isDirectory()).toBe(true); + expect(quarantineEntries[0]!.name).toContain( + caseName === "mismatched" || caseName === "large_mismatched" + ? ".identity_mismatch." + : ".identity_indeterminate.", + ); + const quarantinedControlPlaneRoot = join( + quarantineRoot, + quarantineEntries[0]!.name, + "control-plane", + ); await expect( - access(join(symlinkTarget, "must-remain")), + access(quarantinedControlPlaneRoot), ).resolves.toBeUndefined(); - await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); + if (caseName !== "missing") { + await expect( + access( + join(quarantinedControlPlaneRoot, "control-plane-state.json"), + ), + ).resolves.toBeUndefined(); + } expect(state.createBackend).not.toHaveBeenCalled(); expect(state.createTransport).not.toHaveBeenCalled(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + }, + ); + + it("does not quarantine an unsafe scoped-root symlink", async () => { + const stateBase = isolatedStateDirectory; + + const scopedExecution = scopedNativeExecution("symlink-scoped-state"); + const scopedRoot = await initializeScopedRunnerRoot(scopedExecution, "runner-symlink-scoped-state"); + const symlinkTarget = join(stateBase, "symlink-target"); + await rm(scopedRoot, { recursive: true, force: true }); + await mkdir(symlinkTarget, { recursive: true }); + await writeFile(join(symlinkTarget, "must-remain"), "retained"); + await symlink(symlinkTarget, scopedRoot); + state.createBackend.mockClear(); + state.createTransport.mockClear(); + + await expect( + createRunnerdBackend({ + db: leaseDb(scopedExecution), + execution: scopedExecution, + runnerInstanceId: "runner-symlink-retry", + }), + ).rejects.toThrow("runner_state_directory_unsafe"); + await expect(access(scopedRoot)).resolves.toBeUndefined(); + await expect( + access(join(symlinkTarget, "must-remain")), + ).resolves.toBeUndefined(); + await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); }); it("rejects a suspended prior-run authority whose persisted execution belongs to another full session scope", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-prior-run-mismatched-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + const stateBase = isolatedStateDirectory; + const currentExecution = { ...execution, binding: { @@ -12009,66 +11557,38 @@ describe("runnerd provider runtime wiring", () => { ) .digest("hex"), ); - const priorRunDb = { - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: priorExecution, - }, - }, - ]), - }), - }), - }), - } as unknown as Db; - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await mkdir(join(legacyRoot, "runner"), { recursive: true }); - const identity = { - runId: priorExecution.binding.runId, - normalizedSessionId: currentExecution.session.normalizedSessionId, - runnerInstanceId: "runner-prior-run-mismatch", - environmentLeaseId: "lease-prior-run-mismatch", - }; - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - await writeFile( - join(legacyRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "suspended")), - ); + const priorRunDb = priorNativeRunDb(priorExecution); + await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); + await mkdir(join(legacyRoot, "runner"), { recursive: true }); + const identity = { + runId: priorExecution.binding.runId, + normalizedSessionId: currentExecution.session.normalizedSessionId, + runnerInstanceId: "runner-prior-run-mismatch", + environmentLeaseId: "lease-prior-run-mismatch", + }; + await writeFile( + join(legacyRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(identity)), + ); + await writeFile( + join(legacyRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(identity, "suspended")), + ); - await expect( - createRunnerdBackend({ - db: priorRunDb, - execution: currentExecution, - runnerInstanceId: "runner-current-prior-mismatch", - }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(legacyRoot)).resolves.toBeUndefined(); - await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + await expect( + createRunnerdBackend({ + db: priorRunDb, + execution: currentExecution, + runnerInstanceId: "runner-current-prior-mismatch", + }), + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(legacyRoot)).resolves.toBeUndefined(); + await expect(access(join(stateBase, "quarantine"))).rejects.toThrow(); }); it("fails closed instead of claiming a mismatched former session scope", async () => { - const stateBase = await mkdtemp( - join(tmpdir(), "paperclip-mismatched-session-state-"), - ); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + const stateBase = isolatedStateDirectory; + const currentExecution = { ...execution, binding: { @@ -12094,36 +11614,27 @@ describe("runnerd provider runtime wiring", () => { ) .digest("hex"), ); - try { - await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); - await writeFile( - join(legacyRoot, "control-plane", "control-plane-state.json"), - JSON.stringify( - durableControlPlaneState({ - runId: "run-unrelated-scope", - normalizedSessionId: currentExecution.session.normalizedSessionId, - runnerInstanceId: "runner-unrelated-scope", - environmentLeaseId: "lease-unrelated-scope", - }), - ), - ); - - await expect( - createRunnerdBackend({ - db: leaseDb(currentExecution), - execution: currentExecution, - runnerInstanceId: "runner-current-scope", + await mkdir(join(legacyRoot, "control-plane"), { recursive: true }); + await writeFile( + join(legacyRoot, "control-plane", "control-plane-state.json"), + JSON.stringify( + durableControlPlaneState({ + runId: "run-unrelated-scope", + normalizedSessionId: currentExecution.session.normalizedSessionId, + runnerInstanceId: "runner-unrelated-scope", + environmentLeaseId: "lease-unrelated-scope", }), - ).rejects.toThrow("runner_state_identity_mismatch"); - await expect(access(legacyRoot)).resolves.toBeUndefined(); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; - } - await rm(stateBase, { recursive: true, force: true }); - } + ), + ); + + await expect( + createRunnerdBackend({ + db: leaseDb(currentExecution), + execution: currentExecution, + runnerInstanceId: "runner-current-scope", + }), + ).rejects.toThrow("runner_state_identity_mismatch"); + await expect(access(legacyRoot)).resolves.toBeUndefined(); }); it("isolates durable state and tool authority for equal session ids in different companies", async () => { @@ -12200,9 +11711,8 @@ describe("runnerd provider runtime wiring", () => { }); it("scopes local durable sessions by agent, workspace, and provider profile while reusing them across runs", async () => { - const stateBase = await mkdtemp(join(tmpdir(), "paperclip-session-scope-")); - const previousStateDirectory = process.env.PAPERCLIP_RUNNER_STATE_DIR; - process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase; + + const scopedExecution = (input: { runId: string; agentId?: string; @@ -12270,116 +11780,107 @@ describe("runnerd provider runtime wiring", () => { providerKind: "opencode", }); - try { - state.createBackend.mockClear(); - state.createTransport.mockClear(); - state.toolAuthorityExecute - .mockReset() - .mockImplementation((binding: Record) => - Promise.resolve({ runId: binding.runId }), - ); - const tracedRuns: string[] = []; - let firstScopedRoot: string | undefined; - for (const candidate of [ - first, - continuation, - differentAgent, - differentWorkspace, - differentProviderProfile, - ]) { - const candidateDb = - candidate === continuation - ? ({ - ...leaseDb(candidate), - select: () => ({ - from: () => ({ - where: () => ({ - limit: () => - Promise.resolve([ - { - status: "succeeded", - runnerProfileJson: { - nativeExecutionInput: first, - }, + state.createBackend.mockClear(); + state.createTransport.mockClear(); + state.toolAuthorityExecute + .mockReset() + .mockImplementation((binding: Record) => + Promise.resolve({ runId: binding.runId }), + ); + const tracedRuns: string[] = []; + let firstScopedRoot: string | undefined; + for (const candidate of [ + first, + continuation, + differentAgent, + differentWorkspace, + differentProviderProfile, + ]) { + const candidateDb = + candidate === continuation + ? ({ + ...leaseDb(candidate), + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => + Promise.resolve([ + { + status: "succeeded", + runnerProfileJson: { + nativeExecutionInput: first, }, - ]), - }), + }, + ]), }), }), - } as unknown as Db) - : leaseDb(candidate); - await createRunnerdBackend({ - db: candidateDb, - execution: candidate, - runnerInstanceId: `runner-${candidate.binding.runId}`, - toolTrace: { - observe() {}, - async execute(_call, work) { - tracedRuns.push(candidate.binding.runId); - return await work(); - }, + }), + } as unknown as Db) + : leaseDb(candidate); + await createRunnerdBackend({ + db: candidateDb, + execution: candidate, + runnerInstanceId: `runner-${candidate.binding.runId}`, + toolTrace: { + observe() {}, + async execute(_call, work) { + tracedRuns.push(candidate.binding.runId); + return await work(); }, - }); - state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); - if (candidate === first) { - firstScopedRoot = - state.createTransport.mock.calls.at(-1)![0].stateDirectory!; - const identity = { - runId: first.binding.runId, - normalizedSessionId: first.session.normalizedSessionId, - runnerInstanceId: `runner-${first.binding.runId}`, - environmentLeaseId: first.binding.executionWorkspaceId, - }; - await mkdir(join(firstScopedRoot, "control-plane"), { - recursive: true, - }); - await mkdir(join(firstScopedRoot, "runner"), { recursive: true }); - await writeFile( - join(firstScopedRoot, "control-plane", "control-plane-state.json"), - JSON.stringify(durableControlPlaneState(identity)), - ); - await writeFile( - join(firstScopedRoot, "runner", "runner-state.json"), - JSON.stringify(durableRunnerState(identity, "suspended")), - ); - } - } - - const stateDirectories = state.createTransport.mock.calls.map( - ([options]) => options.stateDirectory, - ); - expect(stateDirectories[1]).toBe(stateDirectories[0]); - expect(stateDirectories[0]).toBe(firstScopedRoot); - expect( - new Set([ - stateDirectories[0], - stateDirectories[2], - stateDirectories[3], - stateDirectories[4], - ]).size, - ).toBe(4); - - const firstOptions = state.createBackend.mock.calls[0]![1]; - const continuationOptions = state.createBackend.mock.calls[1]![1]; - // The retained runner backend owns one stable callback. After run.attach, - // that callback routes through the session-scope authority registry to - // the new run; stale provider calls are rejected earlier by runnerd's - // turn identity boundary. - await expect(firstOptions.dynamicToolHandler!({})).resolves.toEqual({ - runId: continuation.binding.runId, + }, }); - await expect( - continuationOptions.dynamicToolHandler!({}), - ).resolves.toEqual({ runId: continuation.binding.runId }); - expect(tracedRuns).toEqual([continuation.binding.runId, continuation.binding.runId]); - } finally { - if (previousStateDirectory === undefined) { - delete process.env.PAPERCLIP_RUNNER_STATE_DIR; - } else { - process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateDirectory; + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + if (candidate === first) { + firstScopedRoot = + state.createTransport.mock.calls.at(-1)![0].stateDirectory!; + const identity = { + runId: first.binding.runId, + normalizedSessionId: first.session.normalizedSessionId, + runnerInstanceId: `runner-${first.binding.runId}`, + environmentLeaseId: first.binding.executionWorkspaceId, + }; + await mkdir(join(firstScopedRoot, "control-plane"), { + recursive: true, + }); + await mkdir(join(firstScopedRoot, "runner"), { recursive: true }); + await writeFile( + join(firstScopedRoot, "control-plane", "control-plane-state.json"), + JSON.stringify(durableControlPlaneState(identity)), + ); + await writeFile( + join(firstScopedRoot, "runner", "runner-state.json"), + JSON.stringify(durableRunnerState(identity, "suspended")), + ); } - await rm(stateBase, { recursive: true, force: true }); } + + const stateDirectories = state.createTransport.mock.calls.map( + ([options]) => options.stateDirectory, + ); + expect(stateDirectories[1]).toBe(stateDirectories[0]); + expect(stateDirectories[0]).toBe(firstScopedRoot); + expect( + new Set([ + stateDirectories[0], + stateDirectories[2], + stateDirectories[3], + stateDirectories[4], + ]).size, + ).toBe(4); + + const firstOptions = state.createBackend.mock.calls[0]![1]; + const continuationOptions = state.createBackend.mock.calls[1]![1]; + // The retained runner backend owns one stable callback. After run.attach, + // that callback routes through the session-scope authority registry to + // the new run; stale provider calls are rejected earlier by runnerd's + // turn identity boundary. + await expect(firstOptions.dynamicToolHandler!({})).resolves.toEqual({ + runId: continuation.binding.runId, + }); + await expect( + continuationOptions.dynamicToolHandler!({}), + ).resolves.toEqual({ runId: continuation.binding.runId }); + expect(tracedRuns).toEqual([continuation.binding.runId, continuation.binding.runId]); }); it("rejects a concurrent first-use backend for the same provider session scope", async () => { diff --git a/server/src/services/native-runtime/native-session-resume.test.ts b/server/src/services/native-runtime/native-session-resume.test.ts index 52f2f12606c..b0ba4e194f6 100644 --- a/server/src/services/native-runtime/native-session-resume.test.ts +++ b/server/src/services/native-runtime/native-session-resume.test.ts @@ -79,211 +79,13 @@ const previousRunId = "40000000-0000-4000-8000-000000000004"; const currentRunId = "50000000-0000-4000-8000-000000000005"; const normalizedSessionId = "60000000-0000-4000-8000-000000000006"; -const CONDITIONAL_READER_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v1", - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - ], - }), - ) - .digest("hex")}`; -const PRE_CHAT_ATTACHMENT_REUSE_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v2", - executionTargetKind: "local", - advertisementPolicy: { - readCurrentWakeComments: "always_advertised_binding_gated.v1", - registerDeliverable: "local_workspace_only.v1", - }, - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - ], - }), - ) - .digest("hex")}`; -const PRE_CHAT_ATTACHMENT_READ_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v3", - executionTargetKind: "local", - advertisementPolicy: { - readCurrentWakeComments: "always_advertised_binding_gated.v1", - historicalChatAttachments: - "always_advertised_conversation_binding_gated.v1", - registerDeliverable: "local_workspace_only.v1", - }, - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - { - name: "list_chat_attachments", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "reuse_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - ], - }), - ) - .digest("hex")}`; -const PRE_STRUCTURED_HUMAN_INPUT_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v4", - executionTargetKind: "local", - advertisementPolicy: { - readCurrentWakeComments: "always_advertised_binding_gated.v1", - historicalChatAttachments: - "always_advertised_conversation_binding_gated.v1", - registerDeliverable: "local_workspace_only.v1", - readChatAttachment: "always_advertised_run_scope_local_staging.v1", - }, - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - { - name: "list_chat_attachments", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "reuse_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "read_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-read.v1", - version: 1, - }, - ], - }), - ) - .digest("hex")}`; -const PRE_RESPONSE_WAKE_YIELD_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v5", - executionTargetKind: "local", - advertisementPolicy: { - readCurrentWakeComments: "always_advertised_binding_gated.v1", - historicalChatAttachments: - "always_advertised_conversation_binding_gated.v1", - registerDeliverable: "local_workspace_only.v1", - readChatAttachment: "always_advertised_run_scope_local_staging.v1", - structuredHumanInput: - "always_advertised_run_issue_agent_binding_gated.v1", - }, - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - { - name: "list_chat_attachments", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "reuse_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "read_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-read.v1", - version: 1, - }, - { name: "request_human_input", version: 1 }, - ], - }), - ) - .digest("hex")}`; -const PRE_EXPLICIT_CHAT_ATTACHMENT_GUIDANCE_TOOL_CONTRACT_FINGERPRINT = `sha256:${createHash( - "sha256", -) - .update( - JSON.stringify({ - schema: "paperclip.native-tool-contract.v7", - executionTargetKind: "local", - advertisementPolicy: { - readCurrentWakeComments: "always_advertised_binding_gated.v1", - historicalChatAttachments: - "always_advertised_conversation_binding_gated.v1", - registerDeliverable: "local_workspace_only.v1", - readChatAttachment: "always_advertised_run_scope_local_staging.v1", - structuredHumanInput: - "always_advertised_run_issue_agent_binding_gated.v1", - semanticCompletion: "finish_response_wake_concrete_object.v2", - }, - tools: [ - { name: "register_deliverable", version: 1 }, - { - name: "read_current_wake_comments", - semanticContract: "paperclip.server-current-wake-comments.v1", - version: 1, - }, - { - name: "list_chat_attachments", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "reuse_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-reuse.v1", - version: 1, - }, - { - name: "read_chat_attachment", - semanticContract: "paperclip.server-chat-attachment-read.v1", - version: 1, - }, - { name: "request_human_input", version: 1 }, - ], - }), - ) - .digest("hex")}`; + + function execution( runId: string, @@ -440,6 +242,8 @@ describe("capability-gated connection tool refresh", () => { current.runtimeContext.instructions.bundle.digest = "b".repeat(64); current.runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(current.runtimeContext); expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).toBeNull(); + current.runtimeContext = nativeRuntimeContextFixture(); + expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).not.toBeNull(); current.binding.agentId = randomUUID(); expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).toBeNull(); }); @@ -1918,6 +1722,12 @@ describe("rebindNativeSessionCheckpoint", () => { }); it.each([ + { contract: "conditional current-wake reader", retainedFingerprint: "sha256:fe7a2e17ca825228ea2f51f23acdc82db65c91da5ec6add330fefa39b65e1c85" }, + { contract: "same-conversation attachment reuse", retainedFingerprint: "sha256:2b84f2105c2cea10e0e5f37cf4a6ba9bf7fec90d8c8c19c655a0e21d3a8dab5f" }, + { contract: "historical file inspection", retainedFingerprint: "sha256:9b558a575dfa4e3dfaf97b7139b47d1676ab3e0307953ec85f2af0b3f443567c" }, + { contract: "structured human questions", retainedFingerprint: "sha256:1c3cd7e3223f6ed28e1c44a0ba3c961dd4c0c52e52c9c1100a4e41a83384259c" }, + { contract: "response-wake completion yield", retainedFingerprint: "sha256:09887bbf1f6a3674fcf529d872fe2853cc1d6554376019dd1a9b8d607d7eb7fb" }, + { contract: "explicit chat-attachment invocation guidance", retainedFingerprint: "sha256:d797c8bc1baef8407a8c80f2c4b28da63b2a4258a47cf52ee9b1fca45535f5e7" }, { contract: "completion descriptions", // Deployed v8 / finish_response_wake_concrete_object.v2 local catalog. @@ -1950,7 +1760,13 @@ describe("rebindNativeSessionCheckpoint", () => { }, ])( "refreshes retained $contract without changing the Paperclip task or prior history", - ({ retainedFingerprint }) => { + ({ contract, retainedFingerprint }) => { + if (contract === "explicit chat-attachment invocation guidance") { + expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain("Call with {}"); + expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain("1 through 50"); + expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain("All four arguments are required"); + expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain("same key unchanged on every retry"); + } expect(NATIVE_TOOL_CONTRACT_FINGERPRINT).not.toBe(retainedFingerprint); const prior = previousRun({ nativeToolContractFingerprint: retainedFingerprint, @@ -2001,35 +1817,7 @@ describe("rebindNativeSessionCheckpoint", () => { }, ); - it("rotates a provider thread created while the current-wake reader was conditionally advertised", () => { - expect(CONDITIONAL_READER_TOOL_CONTRACT_FINGERPRINT).not.toBe( - NATIVE_TOOL_CONTRACT_FINGERPRINT, - ); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - CONDITIONAL_READER_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); - it("rotates a provider thread that predates same-conversation attachment reuse", () => { - expect(PRE_CHAT_ATTACHMENT_REUSE_TOOL_CONTRACT_FINGERPRINT).not.toBe( - NATIVE_TOOL_CONTRACT_FINGERPRINT, - ); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - PRE_CHAT_ATTACHMENT_REUSE_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); it("binds the provider tool catalog to the local or remote execution target", () => { const remoteFingerprint = nativeToolContractFingerprintForTarget("remote"); @@ -2052,77 +1840,10 @@ describe("rebindNativeSessionCheckpoint", () => { ).not.toBeNull(); }); - it("rotates provider threads whose retained catalog cannot inspect historical files", () => { - expect(PRE_CHAT_ATTACHMENT_READ_TOOL_CONTRACT_FINGERPRINT).not.toBe( - NATIVE_TOOL_CONTRACT_FINGERPRINT, - ); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - PRE_CHAT_ATTACHMENT_READ_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); - it("rotates direct provider threads whose retained catalog cannot ask structured questions", () => { - expect(PRE_STRUCTURED_HUMAN_INPUT_TOOL_CONTRACT_FINGERPRINT).not.toBe( - NATIVE_TOOL_CONTRACT_FINGERPRINT, - ); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - PRE_STRUCTURED_HUMAN_INPUT_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); - it("rotates direct provider threads whose retained completion tool cannot yield for a response", () => { - expect(PRE_RESPONSE_WAKE_YIELD_TOOL_CONTRACT_FINGERPRINT).not.toBe( - NATIVE_TOOL_CONTRACT_FINGERPRINT, - ); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - PRE_RESPONSE_WAKE_YIELD_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); - it("rotates provider threads whose retained attachment tools lack explicit invocation guidance", () => { - expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain( - "Call with {}", - ); - expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain( - "1 through 50", - ); - expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain( - "All four arguments are required", - ); - expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain( - "same key unchanged on every retry", - ); - expect( - PRE_EXPLICIT_CHAT_ATTACHMENT_GUIDANCE_TOOL_CONTRACT_FINGERPRINT, - ).not.toBe(NATIVE_TOOL_CONTRACT_FINGERPRINT); - expect( - rebindNativeSessionCheckpoint({ - previousRun: previousRun({ - nativeToolContractFingerprint: - PRE_EXPLICIT_CHAT_ATTACHMENT_GUIDANCE_TOOL_CONTRACT_FINGERPRINT, - }), - currentExecution: execution(currentRunId), - }), - ).toBeNull(); - }); + it("permits legacy retry rebinding only before the replacement acquired authority", () => { const source = { diff --git a/server/src/services/native-runtime/runner-api.test.ts b/server/src/services/native-runtime/runner-api.test.ts index 608619120f8..cb75b0cfb3d 100644 --- a/server/src/services/native-runtime/runner-api.test.ts +++ b/server/src/services/native-runtime/runner-api.test.ts @@ -107,31 +107,7 @@ describe("runner API catalog", () => { expect(operation.responses).toBeDefined(); expect(operation.authorization.actor).toBeTruthy(); }); - it.each( - runnerApiCatalog().filter((operation) => operation.transport === "rest"), - )( - "resolves the catalog route $operationId inside the bound origin", - (operation) => { - const pathParams = Object.fromEntries( - operation.parameters - .filter((parameter) => parameter.in === "path") - .map((parameter) => [ - parameter.name, - parameter.name === "companyId" ? context.companyId : "fixture-id", - ]), - ); - const url = runnerApiUrl( - operation, - { operationId: operation.operationId, pathParams }, - context, - "https://paperclip.test", - ); - expect(url.origin).toBe("https://paperclip.test"); - expect(url.pathname).not.toContain("{"); - expect(operation.responses).toBeDefined(); - expect(operation.authorization.actor).toBeTruthy(); - }, - ); + it("ranks natural language, explains dedicated alternatives, and supports exact lookup", () => { expect( searchRunnerApi({ query: "create project" }).results.map( diff --git a/tests/runner-e2e/stock-harness-checks.mjs b/tests/runner-e2e/stock-harness-checks.mjs index 01d5c5a3cc2..76f4c56e69e 100644 --- a/tests/runner-e2e/stock-harness-checks.mjs +++ b/tests/runner-e2e/stock-harness-checks.mjs @@ -10,7 +10,6 @@ import { readStockInstructionVariant } from "./stock-harness-instruction-variant const root = resolve(import.meta.dirname, "../.."); export const stockHarnessGates = [ { id: "SH-1", name: "Native Codex additive instructions", cwd: "packages/paperclip-runner", files: [ - "src/drivers/codex/codex-app-server-driver.test.ts", "src/drivers/codex/codex-app-server-driver.lifecycle.test.ts", "src/live/runnerd-codex-transport.test.ts", "src/live/live-session.test.ts", diff --git a/ui/src/api/agents.test.ts b/ui/src/api/agents.test.ts index f398a665669..c6820c9144d 100644 --- a/ui/src/api/agents.test.ts +++ b/ui/src/api/agents.test.ts @@ -5,6 +5,17 @@ import { api } from "./client"; afterEach(() => vi.restoreAllMocks()); describe("agentsApi.retryFailedRun", () => { + it("sends only the selected failed run and preserves a retry waiting for dispatch", async () => { + const post = vi.spyOn(api, "post").mockResolvedValue({ + actionId: "retry-action", status: "queued", runId: null, issueId: "issue-1", + }); + await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1")) + .resolves.toEqual({ runId: null, issueId: "issue-1" }); + expect(post).toHaveBeenCalledExactlyOnceWith("/agents/agent-1/wakeup?companyId=company-1", { + source: "on_demand", triggerDetail: "manual", reason: "retry_failed_run", failedRunId: "original-failure", + }); + }); + it.each(["failed", "timed_out", "cancelled", "interrupted"])( "reports a replayed %s retry instead of silently succeeding", async (status) => { @@ -22,14 +33,27 @@ describe("agentsApi.retryFailedRun", () => { expect(post).toHaveBeenCalledOnce(); }); - it("preserves a durable chat retry that is waiting for dispatch", async () => { - vi.spyOn(api, "post").mockResolvedValue({ actionId: "retry-action", status: "queued", runId: null, issueId: "issue-1" }); + it.each(["queued", "deferred", "running", "succeeded"])("accepts a durable %s chat retry receipt", async (status) => { + const post = vi.spyOn(api, "post").mockResolvedValue({ actionId: "retry-action", status, runId: "successor", issueId: "issue-1" }); await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1")) - .resolves.toEqual({ runId: null, issueId: "issue-1" }); + .resolves.toEqual({ runId: "successor", issueId: "issue-1" }); + expect(post).toHaveBeenCalledOnce(); }); - it("reports a skipped wake", async () => { - vi.spyOn(api, "post").mockResolvedValue({ skipped: true, message: "Task execution is paused." }); + it.each(["failed", "cancelled"])("reports a %s chat retry that could not start", async (status) => { + vi.spyOn(api, "post").mockResolvedValue({ actionId: "retry-action", status, runId: null, issueId: "issue-1" }); + await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1")) + .rejects.toThrow("This retry could not start"); + }); + + it("accepts an ordinary run receipt without a status", async () => { + vi.spyOn(api, "post").mockResolvedValue({ id: "successor" }); + await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1")) + .resolves.toEqual({ runId: "successor", issueId: null }); + }); + + it.each([{ skipped: true }, { status: "skipped" }])("reports skipped-wake guidance for %j", async (receipt) => { + vi.spyOn(api, "post").mockResolvedValue({ ...receipt, message: "Task execution is paused." }); await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1")) .rejects.toThrow("Task execution is paused."); }); diff --git a/ui/src/api/chatEndpoints.test.ts b/ui/src/api/chatEndpoints.test.ts index b70c988632c..641e6c80359 100644 --- a/ui/src/api/chatEndpoints.test.ts +++ b/ui/src/api/chatEndpoints.test.ts @@ -9,76 +9,6 @@ const mockApi = vi.hoisted(() => ({ })); vi.mock("./client", () => ({ api: mockApi })); import { chatEndpointsApi } from "./chatEndpoints"; -import { agentsApi } from "./agents"; - -describe("exact failed chat run retry", () => { - beforeEach(() => Object.values(mockApi).forEach((mock) => mock.mockReset())); - - it("sends the selected failed run, never copied task or comment context", async () => { - mockApi.post.mockResolvedValue({ - actionId: "retry-1", - issueId: "issue-1", - runId: null, - status: "deferred", - }); - await expect( - agentsApi.retryFailedRun("agent-1", "failed-run-1", "company-1"), - ).resolves.toEqual({ runId: null, issueId: "issue-1" }); - expect(mockApi.post).toHaveBeenCalledExactlyOnceWith( - "/agents/agent-1/wakeup?companyId=company-1", - { - source: "on_demand", - triggerDetail: "manual", - reason: "retry_failed_run", - failedRunId: "failed-run-1", - }, - ); - }); - - it.each(["queued", "deferred", "running", "succeeded"])( - "accepts durable %s retry receipts", - async (status) => { - mockApi.post.mockResolvedValue({ - actionId: "retry-1", - issueId: "issue-1", - runId: "new-run-1", - status, - }); - await expect( - agentsApi.retryFailedRun("agent-1", "failed-run-1", "company-1"), - ).resolves.toEqual({ runId: "new-run-1", issueId: "issue-1" }); - }, - ); - - it.each(["failed", "cancelled"])( - "does not report a %s retry as successful", - async (status) => { - mockApi.post.mockResolvedValue({ - actionId: "retry-1", - issueId: "issue-1", - runId: null, - status, - }); - await expect( - agentsApi.retryFailedRun("agent-1", "failed-run-1", "company-1"), - ).rejects.toThrow("This retry could not start"); - }, - ); - - it("keeps ordinary non-chat retry results and skipped guidance", async () => { - mockApi.post.mockResolvedValueOnce({ id: "ordinary-run-1" }); - await expect( - agentsApi.retryFailedRun("agent-1", "failed-run-1", "company-1"), - ).resolves.toEqual({ runId: "ordinary-run-1", issueId: null }); - mockApi.post.mockResolvedValueOnce({ - status: "skipped", - message: "The agent is paused.", - }); - await expect( - agentsApi.retryFailedRun("agent-1", "failed-run-1", "company-1"), - ).rejects.toThrow("The agent is paused."); - }); -}); describe("chatEndpointsApi", () => { beforeEach(() => Object.values(mockApi).forEach((mock) => mock.mockReset())); diff --git a/ui/src/components/WorkspaceRuntimeControls.test.tsx b/ui/src/components/WorkspaceRuntimeControls.test.tsx index 407224eddfc..6e560fca96e 100644 --- a/ui/src/components/WorkspaceRuntimeControls.test.tsx +++ b/ui/src/components/WorkspaceRuntimeControls.test.tsx @@ -7,7 +7,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { WorkspaceRuntimeService } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - buildWorkspaceRuntimeControlItems, buildWorkspaceRuntimeControlSections, buildWorkspaceServiceControlEntries, resolveWorkspaceServiceControlRequests, @@ -177,108 +176,6 @@ describe("buildWorkspaceRuntimeControlSections", () => { }), ]); }); - - it("surfaces running stale runtime services separately from updated commands", () => { - const sections = buildWorkspaceRuntimeControlSections({ - runtimeConfig: { - commands: [ - { id: "web", name: "web", kind: "service", command: "pnpm dev:once --tailscale-auth" }, - ], - }, - runtimeServices: [ - createRuntimeService({ - id: "service-web", - serviceName: "web", - status: "running", - command: "pnpm dev", - }), - ], - canStartServices: true, - canRunJobs: true, - }); - - expect(sections.services).toEqual([ - expect.objectContaining({ - title: "web", - statusLabel: "stopped", - command: "pnpm dev:once --tailscale-auth", - runtimeServiceId: null, - }), - ]); - expect(sections.otherServices).toEqual([ - expect.objectContaining({ - title: "web", - statusLabel: "running", - command: "pnpm dev", - runtimeServiceId: "service-web", - disabledReason: "This runtime service no longer matches a configured workspace command.", - }), - ]); - }); - - it("surfaces running stale runtime services separately from updated commands", () => { - const sections = buildWorkspaceRuntimeControlSections({ - runtimeConfig: { - commands: [ - { id: "web", name: "web", kind: "service", command: "pnpm dev:once --tailscale-auth" }, - ], - }, - runtimeServices: [ - createRuntimeService({ - id: "service-web", - serviceName: "web", - status: "running", - command: "pnpm dev", - }), - ], - canStartServices: true, - canRunJobs: true, - }); - - expect(sections.services).toEqual([ - expect.objectContaining({ - title: "web", - statusLabel: "stopped", - command: "pnpm dev:once --tailscale-auth", - runtimeServiceId: null, - }), - ]); - expect(sections.otherServices).toEqual([ - expect.objectContaining({ - title: "web", - statusLabel: "running", - command: "pnpm dev", - runtimeServiceId: "service-web", - disabledReason: "This runtime service no longer matches a configured workspace command.", - }), - ]); - }); -}); - -describe("buildWorkspaceRuntimeControlItems", () => { - it("keeps the legacy flat export shape for stale importers", () => { - const items = buildWorkspaceRuntimeControlItems({ - runtimeConfig: { - commands: [ - { id: "web", name: "web", kind: "service", command: "pnpm dev" }, - { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, - ], - }, - runtimeServices: [ - createRuntimeService({ id: "service-web", serviceName: "web", status: "running" }), - ], - canStartServices: true, - canRunJobs: true, - }); - - expect(items).toHaveLength(1); - expect(items[0]).toMatchObject({ - title: "web", - status: "running", - statusLabel: "running", - runtimeServiceId: "service-web", - }); - }); }); describe("WorkspaceRuntimeControls", () => { @@ -628,36 +525,6 @@ describe("WorkspaceRuntimeControls", () => { act(() => root.unmount()); }); - - it("accepts the legacy items prop without crashing", () => { - const items = buildWorkspaceRuntimeControlItems({ - runtimeConfig: { - commands: [ - { id: "web", name: "web", kind: "service", command: "pnpm dev" }, - ], - }, - runtimeServices: [], - canStartServices: false, - }); - - const root = createRoot(container); - act(() => { - root.render(withQueryClient( - , - )); - }); - - expect(container.textContent).toContain("Services"); - expect(container.textContent).toContain("Add runtime settings first."); - expect(Array.from(container.querySelectorAll("button")).map((button) => button.textContent?.trim())).toEqual(["Start"]); - - act(() => root.unmount()); - }); }); describe("buildWorkspaceServiceControlEntries", () => { diff --git a/ui/src/components/WorkspaceRuntimeControls.tsx b/ui/src/components/WorkspaceRuntimeControls.tsx index d50d89905ae..4b94d1de89a 100644 --- a/ui/src/components/WorkspaceRuntimeControls.tsx +++ b/ui/src/components/WorkspaceRuntimeControls.tsx @@ -51,30 +51,12 @@ export type WorkspaceRuntimeControlSections = { otherServices: WorkspaceRuntimeControlItem[]; }; -type LegacyWorkspaceRuntimeControlItem = WorkspaceRuntimeControlItem & { - status?: string | null; -}; - type WorkspaceRuntimeControlsProps = { sections: WorkspaceRuntimeControlSections; - items?: never; isPending?: boolean; pendingRequest?: WorkspaceRuntimeControlRequest | null; serviceEmptyMessage?: string; jobEmptyMessage?: string; - emptyMessage?: never; - disabledHint?: string | null; - onAction: (request: WorkspaceRuntimeControlRequest) => void; - className?: string; - square?: boolean; -} | { - sections?: never; - items: LegacyWorkspaceRuntimeControlItem[]; - isPending?: boolean; - pendingRequest?: WorkspaceRuntimeControlRequest | null; - serviceEmptyMessage?: never; - jobEmptyMessage?: never; - emptyMessage?: string; disabledHint?: string | null; onAction: (request: WorkspaceRuntimeControlRequest) => void; className?: string; @@ -194,18 +176,6 @@ export function buildWorkspaceRuntimeControlSections(input: { }; } -export function buildWorkspaceRuntimeControlItems(input: { - runtimeConfig: Record | null | undefined; - runtimeServices: WorkspaceRuntimeService[] | null | undefined; - canStartServices: boolean; - canRunJobs?: boolean; -}): LegacyWorkspaceRuntimeControlItem[] { - return buildWorkspaceRuntimeControlSections(input).services.map((item) => ({ - ...item, - status: item.statusLabel, - })); -} - export function getRunningRuntimeServiceUrl( sections: WorkspaceRuntimeControlSections, ) { @@ -537,27 +507,16 @@ function CommandSection({ export function WorkspaceRuntimeControls({ sections, - items, isPending = false, pendingRequest = null, serviceEmptyMessage = "No services are configured for this workspace.", jobEmptyMessage = "No one-shot jobs are configured for this workspace.", - emptyMessage, disabledHint = null, onAction, className, square, }: WorkspaceRuntimeControlsProps) { - const resolvedSections = sections ?? { - services: (items ?? []).map((item) => ({ - ...item, - statusLabel: item.statusLabel ?? item.status ?? "stopped", - })), - jobs: [], - otherServices: [], - }; - const resolvedServiceEmptyMessage = emptyMessage ?? serviceEmptyMessage; - const runningCount = [...resolvedSections.services, ...resolvedSections.otherServices].filter( + const runningCount = [...sections.services, ...sections.otherServices].filter( (item) => item.statusLabel === "running" || item.statusLabel === "starting", ).length; const visibleDisabledHint = runningCount > 0 || disabledHint === null ? null : disabledHint; @@ -580,8 +539,8 @@ export function WorkspaceRuntimeControls({ {runningCount > 0 ? `${runningCount} services running` : "No services running"} - {resolvedSections.jobs.length > 0 - ? `${resolvedSections.jobs.length} job${resolvedSections.jobs.length === 1 ? "" : "s"} available to run on demand.` + {sections.jobs.length > 0 + ? `${sections.jobs.length} job${sections.jobs.length === 1 ? "" : "s"} available to run on demand.` : "Each command can be controlled independently."} @@ -592,8 +551,8 @@ export function WorkspaceRuntimeControls({ - {resolvedSections.otherServices.length > 0 ? ( + {sections.otherServices.length > 0 ? ( { }); }); - it("shows an unhealthy active runtime as unhealthy", () => { + it.each([ + ["active", "unhealthy", ["Unhealthy"], ["Running"]], + ["active", "unknown", [], ["Running", "Unhealthy"]], + ["open", "unhealthy", ["Unhealthy"], ["Running"]], + ["closed", "unhealthy", ["Stopped"], ["Unhealthy"]], + ] as const)("renders %s runtime with %s health", (status, healthStatus, visible, absent) => { const product = workProduct({ type: "runtime_service", provider: "paperclip", - status: "active", - healthStatus: "unhealthy", + status, + healthStatus, title: "Storybook", metadata: { service: "storybook", port: 6006 }, }); @@ -343,85 +348,13 @@ describe("TaskChatProtocolCard", () => { surface: "resource", resourceKind: "deliverable", title: product.title, - subtitle: "runtime service · active", + subtitle: `runtime service · ${status}`, href: product.url, workProduct: product, }); - expect(container.textContent).toContain("Unhealthy"); - expect(container.textContent).not.toContain("Running"); - }); - - it("does not show running for an active runtime with unknown health", () => { - const product = workProduct({ - type: "runtime_service", - provider: "paperclip", - status: "active", - healthStatus: "unknown", - title: "Storybook", - metadata: { service: "storybook", port: 6006 }, - }); - renderCard(root, { - id: "resource:deliverable:work-product-1", - kind: "protocol", - surface: "resource", - resourceKind: "deliverable", - title: product.title, - subtitle: "runtime service · active", - href: product.url, - workProduct: product, - }); - - expect(container.textContent).not.toContain("Running"); - expect(container.textContent).not.toContain("Unhealthy"); - }); - - it("shows an unhealthy runtime with a non-standard open status as unhealthy", () => { - const product = workProduct({ - type: "runtime_service", - provider: "paperclip", - status: "open", - healthStatus: "unhealthy", - title: "Storybook", - metadata: { service: "storybook", port: 6006 }, - }); - renderCard(root, { - id: "resource:deliverable:work-product-1", - kind: "protocol", - surface: "resource", - resourceKind: "deliverable", - title: product.title, - subtitle: "runtime service · open", - href: product.url, - workProduct: product, - }); - - expect(container.textContent).toContain("Unhealthy"); - expect(container.textContent).not.toContain("Running"); - }); - - it("shows a closed runtime as stopped even when its last health check failed", () => { - const product = workProduct({ - type: "runtime_service", - provider: "paperclip", - status: "closed", - healthStatus: "unhealthy", - title: "Storybook", - metadata: { service: "storybook", port: 6006 }, - }); - renderCard(root, { - id: "resource:deliverable:work-product-1", - kind: "protocol", - surface: "resource", - resourceKind: "deliverable", - title: product.title, - subtitle: "runtime service · closed", - href: product.url, - workProduct: product, - }); - - expect(container.textContent).toContain("Stopped"); - expect(container.textContent).not.toContain("Unhealthy"); + for (const label of visible) expect(container.textContent).toContain(label); + for (const label of absent) expect(container.textContent).not.toContain(label); }); it("renders provider plan steps and status", () => { diff --git a/ui/src/pages/CompanyEnvironments.test.tsx b/ui/src/pages/CompanyEnvironments.test.tsx index 1b4137624d4..e36be4e0ac9 100644 --- a/ui/src/pages/CompanyEnvironments.test.tsx +++ b/ui/src/pages/CompanyEnvironments.test.tsx @@ -878,18 +878,7 @@ describe("CompanyEnvironments — test provider button", () => { adapters: [], drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, + daytona: supportedDaytonaCapabilities().sandboxProviders.daytona, e2b: { status: "supported", supportsSavedProbe: true, @@ -957,24 +946,7 @@ describe("CompanyEnvironments — test provider button", () => { mockEnvironmentsApi.list.mockResolvedValue([ { id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } }, ]); - mockEnvironmentsApi.capabilities.mockResolvedValue({ - adapters: [], - drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, - sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, - }, - }); + mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities()); mockEnvironmentsApi.customImageTemplate.mockImplementation(async () => ({ activeTemplate: null, activeSession, @@ -1142,24 +1114,7 @@ describe("CompanyEnvironments — test provider button", () => { mockEnvironmentsApi.list.mockResolvedValue([ { id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } }, ]); - mockEnvironmentsApi.capabilities.mockResolvedValue({ - adapters: [], - drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, - sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, - }, - }); + mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities()); mockEnvironmentsApi.customImageTemplate.mockResolvedValue({ activeTemplate: null, activeSession: createSession(), @@ -1255,24 +1210,7 @@ describe("CompanyEnvironments — test provider button", () => { mockEnvironmentsApi.list.mockResolvedValue([ { id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } }, ]); - mockEnvironmentsApi.capabilities.mockResolvedValue({ - adapters: [], - drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, - sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, - }, - }); + mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities()); mockEnvironmentsApi.customImageTemplate.mockResolvedValue({ activeTemplate: createTemplate({ id: activeTemplateId }), activeSession: null, @@ -1487,24 +1425,7 @@ describe("CompanyEnvironments — test provider button", () => { mockEnvironmentsApi.list.mockResolvedValue([ { id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } }, ]); - mockEnvironmentsApi.capabilities.mockResolvedValue({ - adapters: [], - drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, - sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, - }, - }); + mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities()); mockEnvironmentsApi.customImageTemplate.mockResolvedValue({ activeTemplate: createTemplate({ id: "template-active" }), activeSession: null, @@ -1539,24 +1460,7 @@ describe("CompanyEnvironments — test provider button", () => { mockEnvironmentsApi.list.mockResolvedValue([ { id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } }, ]); - mockEnvironmentsApi.capabilities.mockResolvedValue({ - adapters: [], - drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" }, - sandboxProviders: { - daytona: { - status: "supported", - supportsSavedProbe: true, - supportsUnsavedProbe: true, - supportsRunExecution: true, - supportsReusableLeases: true, - supportsInteractiveSetup: true, - interactiveSetupConnectionTypes: ["ssh"], - supportsTemplateCapture: true, - supportsTemplateDelete: true, - displayName: "Daytona", - }, - }, - }); + mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities()); mockEnvironmentsApi.customImageTemplate.mockResolvedValue({ activeTemplate: createTemplate({ id: "template-active" }), activeTemplateMatchesConfig: false, diff --git a/ui/src/pages/Secrets.render.test.tsx b/ui/src/pages/Secrets.render.test.tsx index a14c5ea1e68..ec834c7533c 100644 --- a/ui/src/pages/Secrets.render.test.tsx +++ b/ui/src/pages/Secrets.render.test.tsx @@ -361,6 +361,28 @@ async function openAwsVaultDialog() { await flushReact(); } +async function renderSecrets( + container: HTMLDivElement, + { + path = "/", + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }), + }: { path?: string; queryClient?: QueryClient } = {}, +) { + const root = createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + return root; +} + describe("Secrets page layout", () => { let container: HTMLDivElement; @@ -398,22 +420,7 @@ describe("Secrets page layout", () => { }); it("uses the shared search/filter/tab affordances and keeps vault sections quiet", async () => { - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); expect(container.querySelector('input[data-page-search-target="true"][aria-label="Search secrets"]')).not.toBeNull(); expect(container.textContent).toContain("Use secrets by binding them to runtime environment variables."); @@ -459,22 +466,7 @@ describe("Secrets page layout", () => { }); it("refreshes existing AWS secrets from a provider vault card", async () => { - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const vaultTabButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Provider vaults"), @@ -512,22 +504,7 @@ describe("Secrets page layout", () => { it("warns that removing a provider vault only removes Paperclip config", async () => { mockSecretsApi.removeProviderConfig.mockResolvedValueOnce(providerConfigs[1]); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const vaultTabButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Provider vaults"), @@ -596,22 +573,7 @@ describe("Secrets page layout", () => { ], }); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const referencesButton = container.querySelector( 'button[aria-label="Actions for OPENAI_API_KEY"]', @@ -649,22 +611,7 @@ describe("Secrets page layout", () => { mockSecretsApi.usage.mockResolvedValue({ secretId: "secret-openai", bindings: [] }); mockSecretsApi.accessEvents.mockResolvedValue([]); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container, { path: "/company/settings/secrets?secret=secret-openai" }); // The detail sheet is open without any clicks: the deep link drives selection. const copyLinkButton = Array.from(document.body.querySelectorAll("button")).find( @@ -694,22 +641,7 @@ describe("Secrets page layout", () => { mockSecretsApi.accessEvents.mockResolvedValue([]); mockSecretsApi.rotate.mockResolvedValue({ ...externalSecret, latestVersion: 2 }); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container, { path: "/company/settings/secrets?secret=secret-neon" }); // AWS supports write-through, so the primary action is a value update. const updateValueButton = Array.from(document.body.querySelectorAll("button")).find( @@ -753,22 +685,7 @@ describe("Secrets page layout", () => { it("merges company secrets and each-user definitions into the Secrets list", async () => { mockSecretsApi.list.mockResolvedValue([makeCompanySecret()]); mockSecretsApi.listUserSecretDefinitions.mockResolvedValue([makeUserSecretDefinition()]); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); expect(container.textContent).toContain("OPENAI_API_KEY"); expect(container.textContent).toContain("Personal GitHub token"); @@ -801,22 +718,7 @@ describe("Secrets page layout", () => { it("creates an each-user secret from the unified New secret dialog", async () => { const definition = makeUserSecretDefinition({ name: "Personal GitHub token" }); mockSecretsApi.createUserSecretDefinition.mockResolvedValueOnce(definition); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -894,22 +796,7 @@ describe("Secrets page layout", () => { it("opens the New secret dialog when provider queries fail", async () => { mockSecretsApi.providers.mockRejectedValueOnce(new ApiError("Providers unavailable", 403, null)); mockSecretsApi.providerConfigs.mockRejectedValueOnce(new ApiError("Provider vaults unavailable", 403, null)); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -931,22 +818,7 @@ describe("Secrets page layout", () => { const definition = makeUserSecretDefinition(); mockSecretsApi.listUserSecretDefinitions.mockResolvedValue([definition]); mockSecretsApi.listMyUserSecrets.mockResolvedValue([{ definition, secret: null }]); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const definitionRow = Array.from(container.querySelectorAll("[role='row']")).find( (row) => row.textContent?.includes("Personal GitHub token"), @@ -990,22 +862,7 @@ describe("Secrets page layout", () => { }); it("keeps the new secret value textarea width-constrained for long tokens", async () => { - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -1034,6 +891,7 @@ describe("Secrets page layout", () => { mockSecretsApi.create.mockRejectedValueOnce( new ApiError("AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.", 403, { details: { + rawProviderMessage, code: "access_denied", provider: "aws_secrets_manager", operation: "secret.create", @@ -1048,22 +906,7 @@ describe("Secrets page layout", () => { }, }), ); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -1105,22 +948,7 @@ describe("Secrets page layout", () => { it("renders generic secret creation failures with a stable selector", async () => { mockSecretsApi.create.mockRejectedValueOnce(new ApiError("Secret creation failed", 500, null)); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -1155,22 +983,7 @@ describe("Secrets page layout", () => { it("discovers AWS provider vault candidates and applies selected values as prefill", async () => { mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValueOnce(makeDiscoveryPreview()); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); await openAwsVaultDialog(); const discoveryButton = document.querySelector( @@ -1237,6 +1050,7 @@ describe("Secrets page layout", () => { mockSecretsApi.providerConfigDiscoveryPreview.mockRejectedValueOnce( new ApiError("AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.", 403, { details: { + rawProviderMessage, code: "access_denied", provider: "aws_secrets_manager", operation: "secret_provider_config.discovery.preview", @@ -1252,22 +1066,7 @@ describe("Secrets page layout", () => { }, }), ); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); await openAwsVaultDialog(); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement; @@ -1320,22 +1119,7 @@ describe("Secrets page layout", () => { }, }), ); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); await openAwsVaultDialog(); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement; @@ -1363,22 +1147,7 @@ describe("Secrets page layout", () => { }); it("auto-generates the key from the name and keeps it read-only until Edit", async () => { - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), @@ -1459,22 +1228,7 @@ describe("Secrets page layout", () => { id === "agent-coder" ? coder : reviewer, ); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); const companyRow = Array.from(container.querySelectorAll("[role='row']")).find( (row) => row.textContent?.includes("OPENAI_API_KEY"), @@ -1551,22 +1305,7 @@ describe("Secrets page layout", () => { mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValueOnce( makeDiscoveryPreview({ candidates: [], sampledSecretCount: 0 }), ); - const root = createRoot(container); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); + const root = await renderSecrets(container); await openAwsVaultDialog(); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement; @@ -1608,21 +1347,8 @@ describe("Secrets folder view (PAP-14698)", () => { mockAgentsApi.list.mockResolvedValue([]); } - async function renderAt(path: string) { - const root = createRoot(container); - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); - return root; + function renderAt(path: string) { + return renderSecrets(container, { path }); } beforeEach(() => { @@ -1831,122 +1557,6 @@ describe("Secrets folder view (PAP-14698)", () => { await act(async () => root.unmount()); }); - it("creates a company secret from a folder prefix and derives the key from the full name", async () => { - mockSecretsApi.create.mockResolvedValue( - makeCompanySecret({ id: "created", name: "dev/github/oauth/clientsecret/deeper" }), - ); - const root = await renderAt("/?path=dev/github/oauth"); - - const newSecretButton = [...document.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "New secret", - ) as HTMLButtonElement; - await act(async () => newSecretButton.click()); - await flushReact(); - - expect(document.body.textContent).toContain("dev/github/oauth/"); - const nameInput = document.getElementById("new-secret-name") as HTMLInputElement; - expect(nameInput.placeholder).toBe("clientsecret"); - expect(nameInput.value).toBe(""); - await act(async () => setInputValue(nameInput, "clientsecret/deeper")); - await flushReact(); - - expect((document.getElementById("new-secret-key") as HTMLInputElement).value).toBe( - "dev-github-oauth-clientsecret-deeper", - ); - await act(async () => - setTextareaValue(document.getElementById("new-secret-value") as HTMLTextAreaElement, "secret-value"), - ); - const createButton = [...document.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "Create secret", - ) as HTMLButtonElement; - await act(async () => createButton.click()); - await flushReact(); - - expect(mockSecretsApi.create).toHaveBeenCalledWith( - "company-1", - expect.objectContaining({ name: "dev/github/oauth/clientsecret/deeper" }), - ); - - await act(async () => root.unmount()); - }); - - it("keeps the folder prefix for Each user and exposes the full name when the chip is removed", async () => { - const root = await renderAt("/?path=dev/github/oauth"); - const newSecretButton = [...document.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "New secret", - ) as HTMLButtonElement; - await act(async () => newSecretButton.click()); - await flushReact(); - - const eachUserTab = [...document.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "Each user", - ) as HTMLButtonElement; - await act(async () => { - eachUserTab.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })); - eachUserTab.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })); - eachUserTab.click(); - }); - await flushReact(); - - const nameInput = document.getElementById("new-secret-name") as HTMLInputElement; - await act(async () => setInputValue(nameInput, "personal-token")); - await flushReact(); - expect((document.getElementById("new-secret-key") as HTMLInputElement).value).toBe( - "DEV_GITHUB_OAUTH_PERSONAL_TOKEN", - ); - - const removePrefix = document.querySelector( - 'button[aria-label="Remove folder prefix"]', - ) as HTMLButtonElement; - await act(async () => removePrefix.click()); - await flushReact(); - expect((document.getElementById("new-secret-name") as HTMLInputElement).value).toBe( - "dev/github/oauth/personal-token", - ); - expect(document.querySelector('button[aria-label="Remove folder prefix"]')).toBeNull(); - - await act(async () => root.unmount()); - }); - - it("validates New folder inline and stages the trimmed segment in the URL-backed folder view", async () => { - const root = await renderAt("/?path=dev/github/oauth"); - const newFolderButton = [...container.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "New folder", - ) as HTMLButtonElement; - await act(async () => newFolderButton.click()); - await flushReact(); - - const folderInput = container.querySelector('input[aria-label="Folder name"]') as HTMLInputElement; - await act(async () => setInputValue(folderInput, "bad/name")); - const createFolderButton = [...container.querySelectorAll("button")].find( - (button) => button.textContent?.trim() === "Create folder", - ) as HTMLButtonElement; - await act(async () => createFolderButton.click()); - await flushReact(); - expect(container.querySelector('[role="alert"]')?.textContent).toContain( - "Folder name cannot contain slashes.", - ); - - await act(async () => setInputValue(folderInput, " staged ")); - await flushReact(); - await act(async () => createFolderButton.click()); - await waitForReact(() => - [...container.querySelectorAll('[aria-current="page"]')].some((node) => - node.textContent?.includes("staged"), - ), - ); - - expect( - [...container.querySelectorAll('[aria-current="page"]')].some((node) => - node.textContent?.includes("staged"), - ), - ).toBe(true); - expect(container.textContent).toContain("No secrets in this folder yet."); - expect(container.querySelector('input[aria-label="Folder name"]')).toBeNull(); - - await act(async () => root.unmount()); - }); - it("Flat toggle reproduces the raw, ungrouped list", async () => { const root = await renderAt("/"); @@ -1999,22 +1609,10 @@ describe("Secrets operator-hidden tabs", () => { mockAgentsApi.list.mockResolvedValue([]); } - async function renderWithHiddenSettings(hiddenSettings: string[]) { - const root = createRoot(container); + function renderWithHiddenSettings(hiddenSettings: string[]) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); queryClient.setQueryData(queryKeys.health, { hiddenSettings } as never); - await act(async () => { - root.render( - - - - - , - ); - }); - await flushReact(); - await flushReact(); - return root; + return renderSecrets(container, { queryClient }); } beforeEach(() => { diff --git a/ui/src/pages/apps/chat/chat-ui-contract.test.ts b/ui/src/pages/apps/chat/chat-ui-contract.test.ts index 4d4a906cf06..f67ccd5be70 100644 --- a/ui/src/pages/apps/chat/chat-ui-contract.test.ts +++ b/ui/src/pages/apps/chat/chat-ui-contract.test.ts @@ -1,4 +1,3 @@ -import { buildSlackAppManifest } from "@paperclipai/shared"; import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; @@ -241,23 +240,6 @@ describe("chat connector UI contract", () => { expect(setup).toContain("Verify Slack connection"); expect(setup).toContain("buildSlackAppManifest"); expect(setup).toContain("defaultSlackAppConfiguration"); - const manifest = buildSlackAppManifest({ - app: { appName: "Maya", botName: "maya", command: "/maya" }, - agentName: "Maya", - webhookUrl: "https://paperclip.example/api/chat-webhooks/test/slack", - }); - expect(manifest.settings.event_subscriptions.bot_events).toEqual(expect.arrayContaining([ - "member_joined_channel", "member_left_channel", "channel_left", "group_left", - "group_archive", "group_unarchive", "group_rename", "app_uninstalled", - "agent_session_stopped", "reaction_added", "reaction_removed", - ])); - expect(manifest.oauth_config.scopes.bot).toEqual(expect.arrayContaining(["reactions:write", "reactions:read", "assistant:write"])); - expect(manifest.features).toHaveProperty("agent_view"); - expect(manifest.features).not.toHaveProperty("assistant_view"); - expect(manifest.features.bot_user).not.toHaveProperty("always_online"); - expect(manifest.features.app_home).toEqual({ - home_tab_enabled: false, messages_tab_enabled: true, messages_tab_read_only_enabled: false, - }); expect(setup).not.toContain("No credentials"); expect(setup).not.toContain("managed Microsoft app"); expect(setup).toContain("endpoint.providerAccountId && !repairing"); @@ -270,23 +252,14 @@ describe("chat connector UI contract", () => { it("keeps provider setup failures visible without rendering submitted credentials", () => { const setup = source("./ChatEndpointSetup.tsx"); - const setupError = source("./chat-setup-error.ts"); expect(setup).toContain("sanitizedSetupErrorMessage"); expect(setup).toContain('role="alert"'); - expect(setupError).toContain('message.replaceAll(candidate, "[redacted]")'); - expect(setupError).toContain("return encodeURIComponent(value)"); expect(setup).toContain("onMutate: () => setSetupError(null)"); expect(setup).not.toContain('title: "Connection failed"'); }); it("keeps GitHub setup on the shipped customer-owned App path", () => { const setup = source("./ChatEndpointSetup.tsx"); - const generator = source( - "../../../../../doc/plans/chat-adapters/generate-wireframes-v8.mjs", - ); - const setupData = source( - "../../../../../doc/plans/chat-adapters/setup-wireframe-data-v8.mjs", - ); expect(setup).toContain("Generate webhook secret"); expect(setup).toContain("GitHub App ID"); expect(setup).toContain("Private key (PEM)"); @@ -299,8 +272,5 @@ describe("chat connector UI contract", () => { expect(setup).toContain('type="password"'); expect(setup).toContain('event.clipboardData.getData("text")'); expect(setup).not.toContain("WebkitTextSecurity"); - expect(generator).toContain("./setup-wireframe-data-v8.mjs"); - expect(generator).not.toContain("./setup-wireframe-data-v6.mjs"); - expect(setupData).not.toContain('primary: "Create in GitHub"'); }); });