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 790f983f156..6dc82ebf4d8 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts @@ -667,6 +667,7 @@ describe("sandbox managed runtime", () => { }, }; const runtimeStatuses: string[] = []; + const run = vi.spyOn(client, "run"); attachFallbackSyncIn(client); const prepared = await prepareSandboxManagedRuntime({ @@ -725,6 +726,7 @@ describe("sandbox managed runtime", () => { ])); await expect(readFile(path.join(localWorkspaceDir, cloneStaging), "utf8")).resolves.toBe("Incomplete private clone"); 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 () => { @@ -1062,6 +1064,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); @@ -4101,6 +4104,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 4e9492a8e51..fd8683ce3e5 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -1222,6 +1222,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. @@ -1586,10 +1676,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(); }); @@ -2193,7 +2280,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)); @@ -2306,7 +2393,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>) { @@ -2338,8 +2425,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)); @@ -2354,7 +2441,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. @@ -2378,7 +2465,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); @@ -2393,7 +2480,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); }, ); @@ -2403,7 +2490,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 => { @@ -2414,13 +2501,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", @@ -2430,18 +2517,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", @@ -2506,9 +2593,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(); @@ -2521,7 +2608,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"); @@ -2623,7 +2710,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)); @@ -2785,7 +2872,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]) }); } }); @@ -2837,20 +2924,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", @@ -2913,10 +3000,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({ @@ -2984,7 +3068,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 }); @@ -2993,7 +3077,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)!; @@ -3004,7 +3088,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 () => { @@ -3088,19 +3172,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", @@ -3164,10 +3242,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", { @@ -3263,10 +3338,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"); }); @@ -3305,10 +3377,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 () => { @@ -3391,10 +3460,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"); @@ -3731,10 +3797,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, { @@ -3887,10 +3950,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", @@ -3906,7 +3966,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)); @@ -4039,7 +4099,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)); @@ -4100,10 +4160,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) @@ -4426,14 +4483,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: { @@ -4496,15 +4547,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)"); @@ -4518,7 +4569,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"); }, @@ -4551,19 +4602,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) => { @@ -4577,8 +4628,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, { timeout: 10_000 }).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, { timeout: 10_000 }).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" }) }) }), @@ -4606,12 +4657,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 () => { @@ -4632,7 +4683,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, @@ -4640,20 +4691,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", @@ -4684,8 +4735,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(); @@ -4737,24 +4788,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, @@ -4766,10 +4808,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]); @@ -4811,7 +4850,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( @@ -4821,9 +4860,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, @@ -4862,14 +4900,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"); @@ -4904,10 +4942,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({ @@ -5023,11 +5058,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", @@ -5112,14 +5147,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, @@ -5133,7 +5168,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"); @@ -5310,10 +5345,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}`; @@ -6175,10 +6207,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", @@ -6631,7 +6660,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); @@ -6858,7 +6887,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(); }); @@ -6876,7 +6905,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); @@ -6929,7 +6958,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); @@ -6996,7 +7025,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); }); @@ -7049,7 +7078,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"); @@ -7093,7 +7122,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))); @@ -7115,7 +7144,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); } }); @@ -7147,15 +7176,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" })]); }); @@ -7316,7 +7345,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"); @@ -7509,12 +7538,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"); @@ -7603,7 +7632,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, @@ -7641,9 +7670,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); @@ -7664,12 +7693,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("snapshots channel workspace defaults on new tasks and preserves them on replies", async () => { @@ -7811,7 +7840,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"); @@ -9111,10 +9140,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) => ({ @@ -9314,10 +9340,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); @@ -10560,10 +10583,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), @@ -10603,10 +10623,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([]); @@ -10705,19 +10722,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( @@ -10781,10 +10792,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", @@ -10916,10 +10924,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, @@ -10935,16 +10940,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 }) @@ -10986,10 +10985,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() }) @@ -10998,10 +10994,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", @@ -11010,10 +11003,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(); @@ -11047,10 +11037,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) @@ -11096,20 +11083,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", @@ -11117,10 +11098,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); @@ -11150,10 +11128,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) @@ -11264,10 +11239,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" })]); }); @@ -11294,10 +11266,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, @@ -11593,10 +11562,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, @@ -11800,10 +11766,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", @@ -12413,10 +12376,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", @@ -12529,10 +12489,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. @@ -12543,10 +12500,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 }) @@ -13261,10 +13215,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( @@ -13275,10 +13226,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", @@ -13507,10 +13455,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", @@ -13531,10 +13476,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", @@ -13576,10 +13518,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" } }) @@ -13765,10 +13704,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", @@ -13803,10 +13739,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 = { @@ -13865,10 +13798,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, @@ -13896,10 +13826,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, @@ -15805,10 +15732,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({ @@ -15972,10 +15896,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 }) @@ -16007,10 +15928,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 @@ -16882,10 +16800,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); }, @@ -17016,10 +16931,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" }), @@ -17049,7 +16961,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, @@ -17067,10 +16979,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 }), @@ -17089,10 +16998,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", ), @@ -17107,10 +17013,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, @@ -17313,10 +17216,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")); @@ -17354,10 +17254,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( @@ -17586,10 +17483,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), @@ -18388,10 +18282,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) { @@ -18482,10 +18373,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({ @@ -18610,10 +18498,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 @@ -18858,10 +18743,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 @@ -19383,10 +19265,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, @@ -19413,10 +19292,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); @@ -19438,10 +19314,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 @@ -19696,10 +19569,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"); @@ -19955,10 +19825,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, @@ -19970,15 +19837,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" }, @@ -20153,10 +20012,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({ @@ -20207,15 +20063,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" }, @@ -20263,10 +20111,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 { @@ -20279,12 +20124,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", @@ -20428,10 +20268,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. @@ -20619,10 +20456,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, @@ -20653,10 +20487,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); @@ -20665,10 +20496,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 }) @@ -20771,10 +20599,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([ @@ -20854,10 +20679,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, @@ -20925,10 +20747,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) { @@ -20986,10 +20805,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, @@ -22333,10 +22149,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) @@ -22465,10 +22278,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 }) @@ -22532,10 +22342,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" @@ -22694,10 +22501,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", @@ -22818,10 +22622,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, ); @@ -23115,10 +22916,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(); @@ -23380,10 +23178,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 === @@ -23522,10 +23317,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: { @@ -23568,10 +23360,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: @@ -23792,10 +23581,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: @@ -23916,10 +23702,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, @@ -24014,10 +23797,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), @@ -24180,10 +23960,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, @@ -24401,10 +24178,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, @@ -24694,10 +24468,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") { @@ -24815,10 +24586,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", @@ -24857,10 +24625,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, @@ -24953,10 +24718,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 @@ -24987,10 +24749,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 @@ -25035,10 +24794,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 @@ -25125,10 +24881,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); @@ -25541,10 +25294,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); @@ -25608,10 +25358,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(); @@ -25623,19 +25370,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, ); @@ -25685,18 +25426,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 { @@ -25856,10 +25591,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, @@ -26088,10 +25820,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"]); @@ -26102,16 +25831,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 @@ -26271,10 +25994,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({ @@ -26297,10 +26017,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; @@ -26326,10 +26043,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(); }); @@ -26398,16 +26112,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(); }); @@ -26468,22 +26176,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); @@ -26517,18 +26213,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); @@ -26563,19 +26250,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"); @@ -26595,16 +26276,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); }); @@ -26646,10 +26321,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) @@ -26823,31 +26495,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({ @@ -27151,19 +26807,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({ @@ -27196,10 +26841,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({ @@ -27218,10 +26860,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 }, @@ -27236,10 +26875,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", ); @@ -27396,17 +27032,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, @@ -27551,10 +27181,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) @@ -27606,12 +27233,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({ @@ -28119,15 +27741,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(); @@ -28183,15 +27799,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")), ); @@ -28215,10 +27825,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() @@ -28232,10 +27839,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 @@ -28253,17 +27857,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"), @@ -28307,10 +27905,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", @@ -28374,7 +27969,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, @@ -28382,7 +27977,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, @@ -28394,16 +27989,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 @@ -28411,10 +28000,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( @@ -28439,7 +28025,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(); }); @@ -28709,10 +28295,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", @@ -28739,10 +28322,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) @@ -28779,10 +28359,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"); @@ -28876,10 +28453,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 @@ -28894,10 +28468,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 = @@ -29021,10 +28592,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 = @@ -29337,10 +28905,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); @@ -29430,7 +28995,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(); @@ -29464,7 +29029,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"); @@ -29522,10 +29087,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({ @@ -29673,10 +29235,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, @@ -29796,10 +29355,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(); @@ -29877,10 +29433,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({ @@ -30123,10 +29676,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() }) @@ -30192,10 +29742,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:"), ); @@ -30260,10 +29807,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", @@ -30436,10 +29980,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); }); @@ -31013,10 +30554,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 @@ -31108,15 +30646,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 }, @@ -31138,26 +30668,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 { @@ -31190,15 +30711,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) @@ -31217,10 +30730,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 { @@ -31295,10 +30805,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 }), ]); @@ -31313,10 +30820,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", @@ -31346,16 +30850,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(); @@ -31392,10 +30890,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); @@ -31682,10 +31177,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. @@ -31863,10 +31355,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, @@ -31917,10 +31406,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, @@ -32013,10 +31499,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({ @@ -32230,7 +31713,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"); @@ -32285,10 +31768,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, @@ -32457,10 +31937,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, @@ -32532,10 +32009,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 @@ -32839,10 +32313,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) @@ -33023,10 +32494,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"); @@ -33189,7 +32657,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)); @@ -33326,7 +32794,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 }); @@ -33942,10 +33410,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"; @@ -34021,10 +33486,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, @@ -34403,10 +33865,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) @@ -35516,10 +34975,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({ @@ -35633,10 +35089,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) @@ -35809,10 +35262,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", ); @@ -36015,10 +35465,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, @@ -36028,10 +35475,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 }, @@ -36106,10 +35550,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)", @@ -36175,10 +35616,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 }) @@ -36466,12 +35904,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: { @@ -36511,15 +35944,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: { @@ -36552,15 +35977,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"); }); @@ -36667,10 +36084,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]) ); }, @@ -37017,10 +36431,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, @@ -37695,22 +37106,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) @@ -37764,10 +37166,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", ); @@ -37777,10 +37176,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, @@ -37841,10 +37237,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 } }) @@ -37952,10 +37345,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, @@ -38190,10 +37580,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, @@ -38215,10 +37602,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]!; @@ -38747,10 +38131,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); @@ -38818,10 +38199,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`, ); @@ -38845,10 +38223,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`, ); @@ -38933,10 +38308,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, @@ -38958,10 +38330,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( @@ -39159,10 +38528,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", @@ -39175,10 +38541,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, @@ -39194,10 +38557,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() @@ -39206,10 +38566,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, @@ -39315,10 +38672,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", @@ -39330,10 +38684,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, @@ -39407,10 +38758,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", @@ -39564,10 +38912,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", @@ -39691,10 +39036,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]) ); }, @@ -39721,10 +39063,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, @@ -40230,10 +39569,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, @@ -40305,10 +39641,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 @@ -40325,10 +39658,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 @@ -40345,10 +39675,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(); @@ -40379,10 +39706,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, @@ -40418,10 +39742,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( @@ -41133,10 +40454,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(); @@ -41974,15 +41292,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"); @@ -42044,10 +41354,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 = @@ -42077,10 +41384,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 @@ -42121,15 +41425,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", @@ -42319,15 +41615,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"); @@ -42371,10 +41659,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( @@ -42391,15 +41676,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", @@ -42783,10 +42060,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(); @@ -43209,10 +42483,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" }, @@ -44025,15 +43296,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( @@ -44078,10 +43341,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 }, { @@ -44169,10 +43429,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 }, { @@ -44270,10 +43527,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]) ); }); @@ -44296,10 +43550,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. @@ -44345,15 +43596,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", @@ -44394,15 +43637,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(); @@ -44425,10 +43660,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); @@ -44474,10 +43706,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 }) @@ -44504,10 +43733,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( @@ -44568,15 +43794,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, @@ -44632,10 +43850,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, @@ -44894,10 +44109,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), ); @@ -44914,10 +44126,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( @@ -44933,10 +44142,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 @@ -45147,10 +44353,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", @@ -45243,10 +44446,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", @@ -45362,10 +44562,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}`, @@ -45411,10 +44608,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); @@ -45525,10 +44719,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}`, @@ -45883,10 +45074,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", }); @@ -45988,10 +45176,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() @@ -46524,10 +45709,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( @@ -46538,10 +45720,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 @@ -46594,10 +45773,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, @@ -46716,10 +45892,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([ @@ -47965,10 +47138,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) => @@ -48563,10 +47733,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, ); @@ -48617,10 +47784,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, @@ -48676,10 +47840,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({ @@ -48796,10 +47957,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") => { @@ -49249,10 +48407,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(); @@ -49579,10 +48734,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) @@ -49736,15 +48888,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); @@ -49765,15 +48909,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, @@ -49820,10 +48956,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", @@ -49841,10 +48974,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", @@ -49857,10 +48987,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); @@ -49887,15 +49014,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"); @@ -49934,10 +49055,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" }) @@ -49953,10 +49071,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, @@ -50518,10 +49633,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 { @@ -50552,10 +49664,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 { @@ -50601,10 +49710,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); @@ -50615,10 +49721,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 @@ -50669,10 +49772,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 @@ -50950,10 +50050,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); @@ -50974,10 +50071,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); @@ -50995,10 +50089,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) => { @@ -51065,10 +50156,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 { @@ -51283,10 +50371,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, @@ -51421,15 +50506,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"); } @@ -51913,10 +50990,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 { @@ -52098,10 +51172,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", ]); @@ -53739,10 +52810,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); }); @@ -53900,10 +52968,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"); @@ -54048,10 +53113,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", ); @@ -54068,10 +53130,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( @@ -54080,10 +53139,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" }), @@ -54107,17 +53163,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( @@ -54185,10 +53235,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) => @@ -54201,10 +53248,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 }; @@ -54364,10 +53408,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 @@ -54452,10 +53493,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 @@ -54471,10 +53509,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([ @@ -54805,10 +53840,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"); @@ -55258,10 +54290,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", @@ -55285,16 +54314,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); }); @@ -55487,10 +54510,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( @@ -55519,10 +54539,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 { @@ -55640,10 +54657,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 @@ -55708,17 +54722,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([ @@ -56357,10 +55365,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", @@ -56379,14 +55384,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( @@ -56434,10 +55433,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) @@ -57615,10 +56611,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() @@ -57736,10 +56729,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( @@ -57936,10 +56926,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) => @@ -58043,10 +57030,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 }) @@ -58131,10 +57115,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(); }); @@ -58382,10 +57363,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) @@ -58551,10 +57529,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); @@ -58574,10 +57549,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"), @@ -58699,14 +57671,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) => @@ -58786,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); const principal = await db .select() .from(chatExternalPrincipals) @@ -58923,10 +57886,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({ @@ -58969,10 +57929,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 }); }); @@ -59003,10 +57960,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({ @@ -59064,10 +58018,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 }); }, ); @@ -59097,10 +58048,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(); @@ -59135,10 +58083,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({ @@ -59154,10 +58099,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 @@ -59236,10 +58178,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(); @@ -59277,10 +58216,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 @@ -59344,15 +58280,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); }, ); @@ -59532,10 +58460,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", @@ -59587,10 +58512,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( ( @@ -59655,10 +58577,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( @@ -59699,10 +58618,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) @@ -59712,10 +58628,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, @@ -60121,10 +59034,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" || @@ -60137,19 +59047,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( ( @@ -60441,10 +59345,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], @@ -60470,10 +59371,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, { @@ -60489,10 +59387,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, @@ -60506,10 +59401,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 @@ -60550,10 +59442,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(); @@ -60612,10 +59501,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, @@ -60667,10 +59553,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(); @@ -60701,10 +59584,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 @@ -60744,10 +59624,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(); @@ -60878,10 +59755,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], @@ -60912,20 +59786,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, @@ -60969,10 +59832,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, @@ -61008,10 +59868,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) @@ -61044,16 +59901,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", @@ -61079,10 +59930,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, @@ -61128,10 +59976,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 @@ -61140,10 +59985,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 { @@ -61166,10 +60008,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, @@ -61186,10 +60025,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, @@ -61235,20 +60071,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(); @@ -61289,14 +60119,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, { @@ -61313,10 +60137,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, @@ -61332,16 +60153,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 @@ -61387,10 +60202,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(); @@ -61710,10 +60522,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, { @@ -61890,10 +60699,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"); @@ -62247,15 +61053,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) @@ -62273,15 +61071,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, @@ -62339,15 +61129,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", @@ -62434,24 +61216,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, @@ -62479,19 +61245,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( @@ -63219,10 +61974,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({ @@ -63308,10 +62060,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() }) @@ -63340,16 +62089,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", @@ -63417,18 +62160,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); }); @@ -63459,14 +62196,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 = { @@ -63511,10 +62242,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); @@ -63769,10 +62497,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", @@ -63960,7 +62685,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, @@ -64304,10 +63029,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, @@ -64496,10 +63218,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", @@ -64511,10 +63230,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", @@ -64607,10 +63323,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", @@ -64718,10 +63431,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", @@ -64822,10 +63532,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", @@ -64976,10 +63683,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", @@ -65023,10 +63727,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, @@ -65210,10 +63911,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", @@ -65242,10 +63940,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, @@ -65331,10 +64026,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( @@ -65515,10 +64207,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) @@ -65724,10 +64413,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), @@ -66207,10 +64893,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, @@ -66540,10 +65223,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(); @@ -66602,10 +65282,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 @@ -66630,10 +65307,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, @@ -66841,10 +65515,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, @@ -66942,10 +65613,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( @@ -67216,10 +65884,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) @@ -67261,10 +65926,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({ @@ -67278,10 +65940,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 }, @@ -67317,12 +65976,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({ @@ -67482,10 +66136,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, @@ -67497,10 +66148,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; @@ -67865,10 +66513,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, @@ -67902,20 +66547,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, @@ -67935,10 +66574,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, @@ -68072,10 +66708,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(); @@ -68089,10 +66722,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, @@ -68124,10 +66754,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, ); @@ -68139,10 +66766,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, @@ -68369,10 +66993,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, @@ -68410,10 +67031,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: @@ -68552,10 +67170,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 @@ -68719,10 +67334,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(); @@ -68759,10 +67371,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 @@ -68804,10 +67413,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 @@ -69110,10 +67716,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: { @@ -69418,10 +68021,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) @@ -69432,10 +68032,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, @@ -69475,17 +68072,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", @@ -69639,19 +68230,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); @@ -69694,10 +68274,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({ @@ -69749,10 +68326,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 @@ -69850,10 +68424,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 { @@ -69866,10 +68437,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({ @@ -69907,16 +68475,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(); @@ -69927,10 +68489,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() @@ -69942,10 +68501,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, @@ -70074,10 +68630,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 ?? [], @@ -70125,18 +68678,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, @@ -70334,10 +68881,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({ @@ -70447,10 +68991,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, @@ -70570,10 +69111,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, @@ -70624,16 +69162,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) @@ -70674,10 +69206,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 { @@ -70740,10 +69269,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: @@ -70788,10 +69314,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, @@ -71040,14 +69563,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) @@ -71286,10 +69803,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 }> = []; @@ -71525,10 +70039,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) { @@ -71557,10 +70068,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( @@ -71744,14 +70252,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)}`; @@ -72042,10 +70544,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({ @@ -72310,10 +70809,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( @@ -72337,10 +70833,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) @@ -72595,10 +71088,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: { @@ -72646,10 +71136,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, @@ -72745,17 +71232,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(); @@ -72809,10 +71290,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( @@ -72823,10 +71301,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", @@ -72908,10 +71383,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", @@ -73136,10 +71608,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 @@ -73148,10 +71617,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([]); @@ -73185,18 +71651,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([]); @@ -73217,20 +71677,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 @@ -73283,10 +71737,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", @@ -73321,10 +71772,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); @@ -73347,10 +71795,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 }; @@ -73435,10 +71880,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", @@ -73456,10 +71898,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", @@ -73645,10 +72084,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'`), @@ -73978,10 +72414,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(); @@ -74070,10 +72503,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", }); @@ -74164,14 +72594,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) @@ -74287,10 +72711,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({ @@ -74301,16 +72722,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 @@ -74320,10 +72735,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", @@ -74351,19 +72763,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 { @@ -74669,10 +73075,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", ); @@ -74820,10 +73223,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" @@ -74960,10 +73360,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: { @@ -75064,10 +73461,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"); } } @@ -75235,10 +73629,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)}`, ); @@ -75265,10 +73656,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 e92970a8e28..f9ce2234e52 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -10882,158 +10882,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(); @@ -11142,58 +11026,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({ @@ -11284,59 +11118,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, @@ -11373,59 +11157,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, @@ -11474,58 +11208,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 @@ -11549,15 +11233,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", }); @@ -17089,5 +16781,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 d5246010337..535990d7fd1 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/heartbeat.ts b/server/src/services/heartbeat.ts index acc85e0fac3..62bde894dbf 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1,4 +1,3 @@ -import { nativeChatWorkspaceCwd } from "./native-runtime/native-chat-workspace.js"; import { prepareHeartbeatRuntimeServiceIntents, startHeartbeatRuntimeServices, recordHeartbeatAdapterRuntimeServices } from "./heartbeat/runtime-services.js"; import { prepareHeartbeatExecutionConfiguration } from "./heartbeat/execution-configuration.js"; import { createHeartbeatRunOutput } from "./heartbeat/run-output.js"; @@ -349,6 +348,7 @@ import { resolveHeartbeatNativeRuntimeMode, } from "./native-runtime/index.js"; +import { nativeChatWorkspaceCwd } from "./native-runtime/native-chat-workspace.js"; import { parseNativeExecutionInput, type NativeExecutionInput, 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 1e9934853fa..52b40ff1c77 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"'); }); });