test: consolidate owner fixtures and retain real regression proof (#16047)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tests protect sandbox execution, run recovery, credentials and the
board UI.
> - Several owner suites repeat the same setup and prove the same result
more than once.
> - These copies add maintenance work and can hide weak assertions.
> - Each cut needs a retained test that proves every distinct contract.
> - This pull request consolidates repeated setup and removes twelve
redundant executions.
> - It also replaces two weak assertions with checks of real outputs.
> - The benefit is 2,831 fewer test lines with clearer regression proof.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The maintenance and regression strength of the existing Vitest owner
suites.

**Subsystem affected**

Sandbox adapter utilities, server heartbeat recovery and environment
capabilities, and the board UI.

**Current behavior**

Tests copy database rows, query builders, render calls and runtime
inputs. Some cases repeat an existing contract. One token test omits a
real bridge credential. A PostHog host comparison can compare absent
controls.

**Proposed behavior**

Use small local helpers with fresh values and the same action order.
Keep each distinct contract in an owner test. Check real bridge
credentials and fixed PostHog method states. Carry the exact persisted
first-run success assertion into the stronger cancellation keeper.

**Reason and benefit**

The ten test files shrink by 2,831 net lines. Twelve redundant
executions retire. All other case tables, inputs, security boundaries,
timeouts and cleanup rules remain.

**Breaking changes**

None. This round changes tests and audit evidence only.

**Additional context**

Refs: #15978, the merged first cleanup. Refs: #16022, the merged prior
cleanup. Its squash merge is `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`.

Related open work: #14634 repairs feedback fixture completion; #14477
supplies explicit sandbox fixture tools; #14198 changes cleanup retry
behavior in another heartbeat file. This audit preserves those separate
action and cleanup contracts. It does not apply their changes.

This PR was first stacked on `codex/test-audit-followup`. After #16022
was squash merged, only the two Round 6 commits were replayed onto
merged master `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. The original
stack head is preserved in a local snapshot branch. The current diff
contains only ten test files and two evidence files. No merge is
requested.

## What Changed

- Consolidate repeated setup in three sandbox adapter suites. Remove
seven redundant executions and retain the real protocol, cleanup and
scratch-file checks. Save 875 lines.
- Consolidate heartbeat database fixtures and read queries. Retain exact
wake cancellation, total run count and the actual first-run success
receipt. Remove one duplicate execution. Save 1,372 lines.
- Remove two repeated rejected-capability cases. Carry the absent
snapshot assertion into the retained runner case. Save 22 lines.
- Consolidate UI fixtures and render calls. Remove two repeated
connection cases. Replace the weak PostHog comparison with fixed method
and required-key checks in both real hosts. Save 562 lines.
- Record audit scope, decision and expansion counts, all twelve keeper
mappings and deliberate mutation results in the [audit
report](https://github.com/paperclipai/paperclip/blob/codex/test-audit-round-six-delivery/doc/plans/2026-10-12-test-audit-round6.md)
and [controls
evidence](https://github.com/paperclipai/paperclip/blob/codex/test-audit-round-six-delivery/doc/plans/2026-10-12-test-audit-round6-controls.json).

## Verification

Source baseline: `a090035cafb29c06f6fca2dbe43561ad866b7c1b`. All ten
original test programs are byte-identical at the stacked base
`58c5c4aa241c85f4665a3159c192888bbb2a5cfa`.

- Baseline: 1,841/1,841 cases passed in the main thirty-file
owner/sibling run. Another 219/219 cases passed in nine extra files.
- Preservation: all four proposal packages passed independent review.
Whole-program helper expansion retains ordered inputs, callbacks,
awaits, queries, fresh allocations and hooks outside the declared
assertion migrations and repairs.
- Candidate: all 2,048 cases passed across 39 complete owner/sibling
files. The main run passed 1,831 cases. The extra run passed 217 cases.
Both runs had zero failures and zero skips.
- PostHog control: the repaired positive case passed. The original weak
case passed with authentication choices removed. The repaired case
failed at its fixed OAuth aria-checked assertion under the same
mutation.
- Eight accepted mutations failed at their intended assertions. They
cover bridge-token exclusion, fallback cleanup, READY bytes, real
scratch-file preservation, exact wake cancellation, the persisted
first-run success receipt, rejected capability snapshots and reachable
PostHog method controls. Two original weak cases passed with the same
defects. All production source bytes were restored.
- Rejected control: changing run status before legacy release timed out
before wake cancellation. This result is not counted as assertion proof.
Its replacement changed only the persisted receipt after real drain in
the existing transaction. It failed at the direct fixed succeeded-row
assertion.
- No new code comments or 1,000-line threshold crossings were
introduced. No production, schema, lockfile or workflow changes are
included.

On the exact final test sources, later committed as
`d8de9422f0703d4508a9dc880b67a07f13e0f110`, `pnpm -r typecheck`, `pnpm
check:token-gates` and `pnpm build` passed. The required full `pnpm
test:run` completed with **exit 1** after **52.14 minutes** on clean,
immutable `d8de9422f0703d4508a9dc880b67a07f13e0f110`. HEAD and tracked
sources remained unchanged.

The first general-server group ran 889 files: **882 passed, one failed
and six skipped**. Its 18,032 cases were **17,925 passed, two failed and
105 skipped**. The only failing file was
`server/src/__tests__/remove-test-home.test.ts`. On macOS, the read-only
runtime bundle and symlink cleanup cases returned `ENOTEMPTY`. The
permission-error case expects `EACCES` or `EPERM`. Four reported failure
entries cover these two cases, including their cleanup errors. The test
and helper are byte-identical to previous master
`4b766b872993045a2e787404f0475503683e5ded` and audit master
`e364b92c82`.

The default wrapper stopped after that group failed. **Later local
groups did not run.** The earlier public-MCP, HTTP log redaction and
workspace snapshot failures did not recur. The 39 focused owner/sibling
files and all 48 remote CI jobs completed independently. This result is
an unresolved macOS cleanup limitation, not full local success. No test,
skip, retry, timeout or cleanup rule was changed to hide it.

The three adapter files received a whitespace tidy after focused
execution. Full runtime AST and string/template/regex byte equality link
those tested sources to the final sources. All 134 adapter helper calls
were checked again. Replayed code commit
`3d3defd2a60595a623045d9e2c6c586f4acb3452` has the exact final test
bytes. Evidence commit `e868e6645ce0d598d756d3458f0b3d71afd6db4a` adds
the report and controls JSON, with one historical ancestry
clarification. All ten accepted test hashes and all seven
production-owner source guards match. No package, lockfile, workspace,
TypeScript, Rust or Vitest dependency manifest changed since the audit
snapshot.

The [original stack CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/38312876910)
passed all 48 jobs on head `c171d6c0d603d22bb997c1f9a9bdcd5bcc5d22aa`.
Those results are historical stack proof.

The [fresh master-context CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/38318223856)
completed successfully with **all 48 jobs passed** on current head
`e868e6645ce0d598d756d3458f0b3d71afd6db4a`. Its tested merge commit was
`41292af60c6eb60d477637d23822bb17b08dc07e`, combining workflow base
`88bf4c6c1d222f818faa56257efe79207c892a6c` with that head. The delivery
commits remain based on pinned master
`dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. Both required master checks,
`ci / verify` and `ci / e2e`, passed. Two separate optional Storybook
checks were skipped by their scope gate.

[Fresh Greptile
review](https://github.com/paperclipai/paperclip/pull/16047#issuecomment-6133323165)
gives the current head **5/5**, with no findings, inline comments or
unresolved review threads. The [current-head commitperclip
review](https://github.com/paperclipai/paperclip/actions/runs/38318223666)
also completed successfully. This PR remains open and draft; no merge is
requested. The local macOS failure above remains unresolved.

## Risks

- A helper could share a value or capture the wrong binding. Independent
expansion and lexical binding checks guard the actual helper calls.
- A retired case could contain a distinct assertion. Each cut has an
exact keeper map and carried observations.
- A mutation could fail before the intended assertion. Controls reject
timeouts and earlier failures. The rejected receipt mutation remains in
the evidence.
- The delivery ancestry changed after the parent squash merge. Exact
source hashes preserve the audited tests and producer receipts. Fresh CI
and review passed in the current master context; the broad local run
remains evidence from its immutable audit snapshot.
- The complete local default failed in two unchanged macOS cleanup
cases. Later local groups were not run. This limitation remains visible
even though focused checks and remote CI passed.

## Model Used

OpenAI Codex, based on GPT-6, assisted with this work. The exact served
model ID and context window are not exposed in this session. It used
repository tools, code execution, AST analysis and parallel audit
agents.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-14 21:43:12 -05:00
1 parent cafa25029c
commit 3dffd2197d
12 files changed
+3005 -5436

No files matched your search

@@ -0,0 +1,304 @@
{
"auditBaseline": "a090035cafb29c06f6fca2dbe43561ad866b7c1b",
"stackBase": "58c5c4aa241c85f4665a3159c192888bbb2a5cfa",
"auditCodeCommit": "d8de9422f0703d4508a9dc880b67a07f13e0f110",
"savings": {
"netTestLinesRemoved": 2831,
"redundantExecutionsRemoved": 12,
"productionLinesRemoved": 0
},
"declarations": {
"total": 995,
"R": 619,
"F": 2,
"C": 370,
"D": 4
},
"actualExpandedHelperCalls": 625,
"packets": {
"adapter": {
"declarationDecisions": {
"R": 184,
"F": 1,
"C": 92,
"D": 2
},
"actualExpandedHelperCalls": 134,
"manifestSha256": "cb6f36fa4b836b7c16e23ce6f62326a15e27eed407742b0e09d4779f40f7c7b1",
"declarationLedgerSha256": "89bc46ce2eb796524adfdeaaa50c382b39ee7c555224fe2d4f0cb1be8ba036b1",
"expansionProofSha256": "8b3883f723147d2bcd53e0288d76fbf47b817d187a8fd79e4f85b258a840ad16"
},
"heartbeat": {
"declarationDecisions": {
"R": 141,
"F": 0,
"C": 130,
"D": 0
},
"actualExpandedHelperCalls": 282,
"manifestSha256": "85934e19feb6aed9247c09ff2057ab4acd1b9d5951e534047c597cbcc936417b",
"declarationLedgerSha256": "a6a5cd83ecc969c53e662a7e10b715741a11efc97bc28e52ab5e30aec8313d60",
"expansionProofSha256": "56e5b59358099059bc48204f74509a2bff8256ae91deeeea0f960575b0d221c1"
},
"ui": {
"declarationDecisions": {
"R": 276,
"F": 1,
"C": 147,
"D": 1
},
"actualExpandedHelperCalls": 209,
"manifestSha256": "0dd858bc5b8b2c3c23a6772ae0fea5087191af9a549a36d9db8f23be8e539127",
"declarationLedgerSha256": "ae284c6efbf141895df4dc54de49013f65eac40c9389ec29914089e9c29c739d",
"expansionProofSha256": "8bd16e860d15f25defa34dd49b6d7e48ac7bf1e0dccaa4c6bae5979562d60ad4"
},
"capability": {
"declarationDecisions": {
"R": 18,
"F": 0,
"C": 1,
"D": 1
},
"actualExpandedHelperCalls": 0,
"manifestSha256": "72fdcb9282dd7875bbc6df559fa8896c856151e60843657cdfe70054cd8b8924",
"declarationLedgerSha256": "ed2a5abcbba3fd17deca8b8158a4e90f3eeb8abcc8af0061a65dda63c3edf884"
}
},
"files": [
{
"path": "packages/adapter-utils/src/execution-target-sandbox.test.ts",
"originalSha256": "cf7db1f718e260cc62da670a48b5d35715bbb25cd7312e49433d73e2bbe1800c",
"executedCandidateSha256": "a3c8b8749df78efef34e9df476114ad09a7cbc8dd14e8ddbe50a40011ccde350",
"finalSha256": "7c1e921b0dcc3fce8bb14df98c739a35d938f75795986fcaf8a9f7dac70134be",
"testLinesRemoved": 191
},
{
"path": "packages/adapter-utils/src/sandbox-callback-bridge.test.ts",
"originalSha256": "2f687f0a26f0e1e32a92a464be46d5bfc6a3f821dda4b85d3cdaf7cef30f1216",
"executedCandidateSha256": "bb0f00b084ab342472ef72a1a4a20a03170a26ff7a66b8e7b571f92f9921402f",
"finalSha256": "79173674831037bde5938e1b2c37122f70bf4f63bd6a0df1a9ae2b2d4f94a25c",
"testLinesRemoved": 214
},
{
"path": "packages/adapter-utils/src/sandbox-managed-runtime.test.ts",
"originalSha256": "e5f81914f167dd36879823e25807e1f0098694f1748b34371f37d0d5c00597d6",
"executedCandidateSha256": "0751d74fd6b4396f5b84206ce33c45a680913c256ed3a9a97a305014d6046216",
"finalSha256": "1c575bfd8e391886a5a23facfa19ed149732139c908f19fb6fad650083270e25",
"testLinesRemoved": 470
},
{
"path": "server/src/__tests__/environment-execution-target-capabilities.test.ts",
"originalSha256": "7b89d2107451a0efc6e78664f9e8305cba940008a058d076d83963cf6cda4cbb",
"executedCandidateSha256": "b5da63ebba187ea7e9ac49524c9e5f6acbb149ab2c635ded2964a26b22e085ed",
"finalSha256": "b5da63ebba187ea7e9ac49524c9e5f6acbb149ab2c635ded2964a26b22e085ed",
"testLinesRemoved": 22
},
{
"path": "server/src/__tests__/heartbeat-comment-wake-batching.test.ts",
"originalSha256": "21d7fc3642316f55c2eff83c482aab5649673cbe8cf82fe4ed111e1cae4ba5be",
"executedCandidateSha256": "288cf2d50a3a31133889b9c2c7b65d3a5eae4ef198b27162583016558a4afebd",
"finalSha256": "288cf2d50a3a31133889b9c2c7b65d3a5eae4ef198b27162583016558a4afebd",
"testLinesRemoved": 688
},
{
"path": "server/src/__tests__/heartbeat-process-recovery.test.ts",
"originalSha256": "33b4140b1dff1ad5ed916463e997742c64dfa4b5e66e65c5658c22aa86ac9dc2",
"executedCandidateSha256": "bf7c888eaba44a56d12823bb948026d5db44f2249a75a6cf083343edbd7c0673",
"finalSha256": "bf7c888eaba44a56d12823bb948026d5db44f2249a75a6cf083343edbd7c0673",
"testLinesRemoved": 684
},
{
"path": "ui/src/components/IssueProperties.test.tsx",
"originalSha256": "8d604724e4c9c423b560d13dae6e939113f248b050905542970ee0660dfeab98",
"executedCandidateSha256": "2dd0c39f6be93e054ed7691984b68c25c32b9313dcfdacac8576d6e02d97128b",
"finalSha256": "2dd0c39f6be93e054ed7691984b68c25c32b9313dcfdacac8576d6e02d97128b",
"testLinesRemoved": 26
},
{
"path": "ui/src/components/TaskChatThread.test.tsx",
"originalSha256": "fc4707ae6a4b16ebf887b277d44f7432c7be3644b8eeb9388caa19a83405ffe4",
"executedCandidateSha256": "aa92b96c1f6fc2a87c4f99bdb4fc76eb8f839bcceb664114c11b3ee87dd392fa",
"finalSha256": "aa92b96c1f6fc2a87c4f99bdb4fc76eb8f839bcceb664114c11b3ee87dd392fa",
"testLinesRemoved": 250
},
{
"path": "ui/src/components/task-chat/TaskChatComposer.test.tsx",
"originalSha256": "3443b48fa7d7d4d11942ffe283ebd6d441e5bc2aa4cb5f30c87f6a98050b0a48",
"executedCandidateSha256": "93a07bec0a1d6d449754bbf856d557ea283c20e71ae8e5c1ff29ff2b53abd238",
"finalSha256": "93a07bec0a1d6d449754bbf856d557ea283c20e71ae8e5c1ff29ff2b53abd238",
"testLinesRemoved": 92
},
{
"path": "ui/src/pages/apps/AppsConnect.test.tsx",
"originalSha256": "7a3164ecef2d66c8a84d12cde1a0d7a0888ada5ca9c2f851e2a65420749091c7",
"executedCandidateSha256": "fcd9b439de18c5435b3ea6c6fc0f3de73f5880533f82652508b41cfaecc471b6",
"finalSha256": "fcd9b439de18c5435b3ea6c6fc0f3de73f5880533f82652508b41cfaecc471b6",
"testLinesRemoved": 194
}
],
"completeFileRuns": [
{
"run": "baseline",
"completeFiles": 30,
"passed": 1841,
"failed": 0,
"pending": 0,
"jsonSha256": "98cf86a556acc308c9d9dfd1b5fa9d32b7156be6b92f77cd820608120c4875a3",
"wallSeconds": 692.28
},
{
"run": "extra-baseline",
"completeFiles": 9,
"passed": 219,
"failed": 0,
"pending": 0,
"jsonSha256": "bdba4be5d0390309f2e90cfe6f83f350c6e0cb69f76d1be0d6b9d84d02a0454d",
"wallSeconds": 29.05
},
{
"run": "candidate",
"completeFiles": 30,
"passed": 1831,
"failed": 0,
"pending": 0,
"jsonSha256": "1ca64e93260e51d2621dcb5e6ac996554b87db871be44643014ecc3b05d126be",
"wallSeconds": 429.46
},
{
"run": "extra-candidate",
"completeFiles": 9,
"passed": 217,
"failed": 0,
"pending": 0,
"jsonSha256": "817daf21df1ffa8a3daad411ad14210a15b445e14f0a93392595f5ac3a84a420",
"wallSeconds": 15.48
}
],
"mutationControls": {
"acceptedIntendedFailures": [
{
"id": "F-actual-bridge-token-log-exclusion-retained",
"testTitle": "keeps sentinel route, query, body, and tokens off the http2 telemetry and logs",
"guard": "actual issued bridge-token exclusion from logDump",
"jsonSha256": "feaa61e1d9cebc1dbb2190dbed10c77901e4bd2c24e59871a11198d8dbf0d623",
"logSha256": "2b5ef18cb9ae39c203318fea74b448ab4752b26b50815afbbaf553d20d604def",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "C-missing-preface-cleanup-oracle-retained",
"testTitle": "test_fallback_to_queue_v1_happens_at_most_once_per_run",
"guard": "provider cleanup count before finally",
"jsonSha256": "d593dbe4bdfcc23958c1ebc5dabc7d62d5c9b042d1133ff5648ed98285c2173a",
"logSha256": "0eeea70f7b19da9b992caab26e32b393f8caad7b6a2673050a3153576c3f72cb",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "C-host-READY-newline-compatibility-retained",
"testTitle": "test_http2_gateway_writes_no_frame_between_ready_and_the_preface",
"guard": "independent host READY newline compatibility",
"jsonSha256": "4207fb4cbc0fd5dc6f51213e045f913ba7ec711e50132e4238a5ff1ae367ba82",
"logSha256": "894e44ae03faf6c35fd239e94a1cb469efbc4ac26373231ec5d1fdc3bf5db971",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "C-real-scratch-wipe-retained",
"testTitle": "the workspace wipe keeps an in-flight scratch tarball at the root but removes a stale sibling",
"guard": "seeded scratch-file .resolves bytes preservation",
"jsonSha256": "f2c2eb5d881f8bf294a6cc7344836315c0af53b3222facf0a523420a551f5671",
"logSha256": "2227abb5cef178d176dc3161e4ead11e9a0be84b85982f5b3a62b3c7181d3fb9",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "self-authored-comment-liveness-bypass-retained",
"testTitle": "cancels a deferred comment wake when its only queued comment was authored by the finishing run",
"guard": "exact cancelled wake self-authorship error",
"jsonSha256": "37db35d7e74e24546d93113301da10b34faba82556af855f26c1a202b9c9ebfe",
"logSha256": "9d9efb43f8d0ac1999cf749a87573df607fc13fad89945e97a42f25f48180790",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "F-reachable-PostHog-auth-methods-retained",
"testTitle": "renders the same PostHog method choices in page and task-dialog hosts",
"guard": "reachable OAuth aria-checked fixed assertion after Change",
"jsonSha256": "e05ddaa20035263df996b93af9e8af10af9ca60d91d9e12fff4659d2312a7a36",
"logSha256": "0d20720ae732945ccea500b0ca127d69bef8f93b8d64fefb17a64c377a83084f",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "post-drain-success-terminal-receipt-failed-retained",
"testTitle": "cancels a deferred comment wake when its only queued comment was authored by the finishing run",
"guard": "persisted exact firstRun id/succeeded matcher",
"jsonSha256": "e467fb18bc4b9857073a4e9fa2114f72690a87598664df7062e137bd88e03d0b",
"logSha256": "a823e1cc7c64c14ce7063f7c46d21ce91dd76529c149fc840d5cac47fd1ce98e",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
},
{
"id": "C-rejected-capability-snapshot-retained",
"testTitle": "never forces the persistent session when the resolution rejects",
"guard": "rejected-resolution snapshot toBeUndefined before execute",
"jsonSha256": "0af6106ffb09a5161e94bab6a41a89e27207bcc3bda656bdc6ce10e2b4ddfa5b",
"logSha256": "1474419f5cfeaf68845d2b7d7d26433b54e549cb296507fe0b63574c2389f694",
"failedAtIntendedAssertion": true,
"notSetupOrTimeout": true
}
],
"originalWeakControlsPassed": 2,
"positiveUniqueKeeperCasesPassed": 7,
"allProductionOwnersRestored": true,
"restoredProductionOwners": [
{
"path": "packages/adapter-utils/src/duplex-frame-codec.ts",
"currentSha256": "97efc3275ab763b79f2afcbcdf4d6ad2a187e713c91c35c0186b6aa8828816b9",
"equalsPinnedBaselineBytes": true
},
{
"path": "packages/adapter-utils/src/execution-target.ts",
"currentSha256": "9f61140f7c27698e7837b97dd78f9b3056045a41527da06bb0cee9f11c9f62a6",
"equalsPinnedBaselineBytes": true
},
{
"path": "packages/adapter-utils/src/sandbox-managed-runtime.ts",
"currentSha256": "2b6093b6481d9ef1f30a9c5798d2d9b4d3c8355867dad4b2bb685a554413b49d",
"equalsPinnedBaselineBytes": true
},
{
"path": "server/src/modules/wake-queue/adapters/postgres.ts",
"currentSha256": "86529cb3acf96b165148654d76980dd1aaef69571b16662cf041e2f7530c8851",
"equalsPinnedBaselineBytes": true
},
{
"path": "server/src/services/environment-execution-target.ts",
"currentSha256": "7f78af7a3726f25068648938f686a2adb5a28445b61a9eb6626960e22a18798a",
"equalsPinnedBaselineBytes": true
},
{
"path": "server/src/services/heartbeat/run-completion.ts",
"currentSha256": "0136bfae6e3ea5dd0af3beaff535f414158d2eedc9a86ad385ef49e68014d9d7",
"equalsPinnedBaselineBytes": true
},
{
"path": "ui/src/features/connections/ConnectionSetupFlow.tsx",
"currentSha256": "cf61ae14cf58c496712fb003aae61453f3e907fc22a49b3bd170b3b2a3a9eb23",
"equalsPinnedBaselineBytes": true
}
],
"coreRestorationReceiptSha256": "97a9bcdaaf480029ef05eeb9226acbf652ab516a2eee9554e111589f4baf892f",
"uiRestorationReceiptSha256": "2eef6a3afa1f876eef9c2fef3b82c2a0d0bb5711b9a9c55cc739b9ddab8f7c3e",
"rejectedEarlierReceiptMutation": "Changing the successful gateway receipt before queue drain timed out before the intended assertion. Excluded from accepted proof. The accepted replacement changes the persisted receipt atomically after real drain in the existing release transaction.",
"independentReviewSha256": "fadc62d0af5cae5c1347824cf8b7f86f4ae48a8aa5828f041a4b7292c0480982"
},
"formattingIdentity": {
"proofSha256": "4cd2af912973bf1a68a9ae34b1d50cc69f6e38d9b209a1fc8f6822a22075c339",
"completeRuntimeAstIncludingLiteralRawTextEqual": true,
"allStringTemplateRegexSourceBytesEqual": true,
"note": "The three adapter test files were tidied after the positive and negative runtime runs. Final source hashes differ only in leading indentation and one duplicate blank import line. Full literal-aware AST equality links the executed bytes to the final bytes; all 134 actual helper expansions were checked again."
},
"scope": "Owner and sibling preservation evidence. Whole-workspace verification and latest delivery-head CI/Greptile outcomes are recorded in the pull request."
}
+96
View File
@@ -0,0 +1,96 @@
# Test audit, Round 6 — preservation report
Round 6 removes **2,831 net test lines and 12 redundant executions**, repairs two false proofs, and changes **zero production lines**. Complete owner/sibling baselines passed **2,060/2,060**; candidates passed **2,048/2,048** across the same **39 complete files**, with no failures or skips. These counts establish preserved selected-suite coverage, not a wall-time speed improvement.
Audit baseline: `a090035cafb29c06f6fca2dbe43561ad866b7c1b` includes the prior cleanup and current master. Audit code commit: `d8de9422f0703d4508a9dc880b67a07f13e0f110`. The initial delivery replayed only this round's identical test changes on stacked base `58c5c4aa241c85f4665a3159c192888bbb2a5cfa`. After the parent squash merge, the same round's changes were replayed onto merged master `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. This report records preservation evidence; workspace checks and current delivery/CI status belong in the PR.
The audit followed OpenClaw's [test-audit skill](https://github.com/openclaw/openclaw/blob/main/.agents/skills/test-audit/SKILL.md) and [campaign process](https://github.com/openclaw/openclaw/blob/main/.agents/skills/test-audit/CAMPAIGN.md): baseline complete files, read owners/callers/siblings, classify each declaration, nominate exact keepers, independently review, then run candidates and deliberate production regressions.
## Exact scope
Only these ten existing test files change; no application, API, workflow, lockfile, telemetry, style or token contract changes.
| Test file | Net lines removed |
|---|---:|
| `packages/adapter-utils/src/execution-target-sandbox.test.ts` | 191 |
| `packages/adapter-utils/src/sandbox-managed-runtime.test.ts` | 470 |
| `packages/adapter-utils/src/sandbox-callback-bridge.test.ts` | 214 |
| `server/src/__tests__/heartbeat-process-recovery.test.ts` | 684 |
| `server/src/__tests__/heartbeat-comment-wake-batching.test.ts` | 688 |
| `server/src/__tests__/environment-execution-target-capabilities.test.ts` | 22 |
| `ui/src/components/TaskChatThread.test.tsx` | 250 |
| `ui/src/components/task-chat/TaskChatComposer.test.tsx` | 92 |
| `ui/src/components/IssueProperties.test.tsx` | 26 |
| `ui/src/pages/apps/AppsConnect.test.tsx` | 194 |
Totals: sandbox **875**, heartbeat **1,372**, UI **562**, capabilities **22**. The final sandbox total includes removal of one duplicate blank import line during indentation cleanup; the other savings include helper costs and repaired expectations.
All **995 original declarations** have decisions: **R619** retained, **F2** repaired, **C370** consolidated, **D4** deleted as duplicate or test-owned proof. C includes fixture/query/render consolidation and case migrations; it does not mean 370 executions were removed. Every remaining table, timeout, retry and distinct security/ownership/transport/platform/lifecycle contract remains.
## All twelve retired executions and their keepers
Owner labels below refer to the exact files in the scope table. Each row is one execution, including the two declarations retired from the handwritten latch group.
| Owner | Retired case | Retained keeper and preserved observation |
|---|---|---|
| execution-target-sandbox | `marks the orderly completion and reports a success for a healthy channel` | `test_close_after_orderly_completion_keeps_the_run_result`: actual bridge settle/read around provider exit, success and no loss event/counter. Retired case exercised only a handwritten fake. |
| execution-target-sandbox | `reports the failure and does not mark for a latched loss` | `fails a clean CLI completion closed when the bridge channel was lost mid-turn`, alongside retained actual bridge loss cases: typed failure translation remains; retired latch assertion exercised only the fake. |
| execution-target-sandbox | `cannot clear the loss latch with a later completion` | Same clean-CLI failure keeper: identical failed injected disposition, exitCode and typed error, plus provider_exit stderr. Extra mark affected only the handwritten fake. |
| execution-target-sandbox | `test_a_missing_preface_aborts_the_open_and_falls_back_to_queue_v1` | `test_fallback_to_queue_v1_happens_at_most_once_per_run`: same READY-without-preface path; migrated provider close/stop receipt **before finally**, one open and one queue fallback. |
| execution-target-sandbox | `encodes the READY frame to the exact byte-for-byte wire format` | callback-bridge `test_http2_gateway_writes_no_frame_between_ready_and_the_preface`: real generated gateway, literal version/key order/newline/preface, plus migrated host-encoder compatibility. |
| sandbox-managed-runtime | `the workspace wipe command preserves in-flight sync scratch tarballs (.paperclip-upload-*)` | `the workspace wipe keeps an in-flight scratch tarball at the root but removes a stale sibling`: executes the real shell wipe and checks independently seeded bytes and stale-file absence. |
| sandbox-callback-bridge | `maps an indeterminate host outcome to a non-retryable 409 in the in-sandbox bridge server` | execution-target-sandbox `forwards the host indeterminate-outcome header so the sandbox server maps the 504 to a non-retryable 409`: actual request/status/header/body. Separate actual HTTP/2 mapping remains. |
| heartbeat-comment-wake-batching | `cancels a deferred wake containing only a comment authored by the closing run` | `cancels a deferred comment wake when its only queued comment was authored by the finishing run`: exact persisted wake cancellation/error, migrated total-run count, and restored exact first-run succeeded receipt. |
| AppsConnect | `shows an in-flight state while Paperclip prepares Notion sign-in` | `keeps the same steps when Notion sign-in takes over the wizard`: identical pending setup/actions; all four original assertions retained, with three migrated, plus no-stepper checks. |
| AppsConnect | `uses the gallery app name automatically` | `leaving the default name connects with the app name`: identical GitHub/key/submit inputs and both original assertions, plus navigation. |
| environment-execution-target-capabilities | `omits the snapshot when the resolution rejects` | `never forces the persistent session when the resolution rejects`: identical rejected target; migrated undefined-snapshot assertion before actual runner delegation, then forceSession=false. |
| environment-execution-target-capabilities | `carries no snapshot for session-output streaming when the resolution rejects` | Same rejection keeper and identical setup; separate positive streaming, native-sync rejection and concurrent-sync controls remain. |
One additional assertion-only cut removes a literal test-owned `push("provisioned")`/equality in the generic asset round-trip. Real provisioned/helper bytes and the restore callback's actual remote-file receipt remain. A restore-diagnostic title was corrected; its assertions and ordering behavior remain unchanged.
## Two false proofs repaired
| Keeper | Original weakness | Repaired public observation |
|---|---|---|
| `keeps sentinel route, query, body, and tokens off the http2 telemetry and logs` | Negative sentinel inventory omitted the actual generated bridge token. | After real HTTP/2 readiness/request success, require the issued token to be nonempty and exclude those actual bytes from both recorder JSON and onLog chunks. Host token/route/query/body/forwarded Authorization checks remain. |
| `renders the same PostHog method choices in page and task-dialog hosts` | Compared subject-derived optional button states; real controls are collapsed radios with aria-checked, so missing controls could equal missing controls. | Open Change in both actual hosts; fixed selected/unselected radio expectations, required-key Connect gate, real typed key, and original dialog primary-action receipt. |
For each repair, the pinned original weak case **passed the same production regression** that made the repaired keeper fail at its intended assertion. No fabricated token, receipt, copied expected inventory or subject-derived expectation supplies the repaired oracle.
## Independent review and expansion
Independent reviewers expanded **625 calls from actual candidate helper bodies**: sandbox **134**, heartbeat **282**, UI **209**. Complete ordered runtime programs equal the originals outside the specifically recorded retirements, migrations and F bodies. Review separately checked lexical callee/free-reference bindings, immutable arguments, callback captures, field/JSX spread order, fresh nested allocations, genuine DB/process receipts, all outer awaits, timer/wait budgets, transaction/action ordering and cleanup. No expected assertions are generated by fixture helpers. No added helper comments or sub-1,000-to-over-1,000 file crossings occur.
**Review caught and restored a real heartbeat preservation gap:** the first proposal migrated cancellation and total-run counts but omitted the retired case's exact first-run `succeeded` receipt. The final keeper copies the original DB read, waits for real queue release and a settled row, then directly asserts `{ id: firstRun!.id, status: "succeeded" }` before cleanup. Waiting only for success would hide a failed receipt behind a timeout; the final settled-state wait permits the direct guard to fail. The correction was independently approved before integration.
The sandbox files were tidied **after** runtime controls. The candidate runs used the earlier bytes; final hashes differ only by leading indentation and one blank import line. `tidy-proof.json` proves complete AST identity including literal raw text and exact string/template/regex source bytes, and all 134 helper expansions were rechecked. The durable controls record connects each executed hash to its final hash; it does not claim the final formatting bytes were separately runtime-tested.
## Accepted regression evidence
All eight deliberate production regressions failed at their intended retained guard, with no setup/worker/timeout failure:
| Regression | Observed guard |
|---|---|
| Actual issued bridge token added to ready log | Actual-token exclusion from logDump. |
| Missing-preface close bypassed | Provider close/stop count before finally received zero. |
| Host READY encoder adds a newline | Host encoder versus independently literal-pinned generated gateway bytes. |
| Scratch exclusion removed from real wipe | Independently seeded scratch read failed with ENOENT at its resolves-byte assertion. |
| Self-authored deferred input kept live | Exact wake cancellation error differed. |
| PostHog authentication choices removed | Fixed OAuth aria-checked assertion received undefined after Change. |
| First-run success receipt corrupted after real drain | Exact persisted first-run matcher received failed instead of succeeded. |
| Rejected capability resolution fabricates a snapshot | Undefined-snapshot assertion received an object before runner execution. |
Seven unmutated positive keeper controls passed, and both original weak controls passed under their respective regressions. All production owner bytes were restored. **One earlier receipt mutation was rejected and excluded:** changing success before queue drain timed out before the intended assertion. The accepted replacement alters the persisted receipt atomically after real drain in the existing release transaction and reaches the direct status guard.
## Durable provenance
[Controls and source/packet digests](2026-10-12-test-audit-round6-controls.json) retain the complete-file run JSON digests, final/executed source hashes, accepted mutation JSON/log digests, restoration receipt and formatting identity digest. Packet manifest / expansion SHA256 values are:
| Packet | Manifest SHA256 | Expansion SHA256 |
|---|---|---|
| Sandbox | `cb6f36fa4b836b7c16e23ce6f62326a15e27eed407742b0e09d4779f40f7c7b1` | `8b3883f723147d2bcd53e0288d76fbf47b817d187a8fd79e4f85b258a840ad16` |
| Heartbeat | `85934e19feb6aed9247c09ff2057ab4acd1b9d5951e534047c597cbcc936417b` | `56e5b59358099059bc48204f74509a2bff8256ae91deeeea0f960575b0d221c1` |
| UI | `0dd858bc5b8b2c3c23a6772ae0fea5087191af9a549a36d9db8f23be8e539127` | `8bd16e860d15f25defa34dd49b6d7e48ac7bf1e0dccaa4c6bae5979562d60ad4` |
| Capabilities | `72fdcb9282dd7875bbc6df559fa8896c856151e60843657cdfe70054cd8b8924` | No helper expansion; exact semantic preservation review. |
Full source ledgers, verifiers and raw logs were temporary audit packets. This report and its adjacent controls JSON retain compact accepted decisions and provenance; digests identify those packets but do not retain their full payloads. Broad workspace and latest delivery-head checks remain separate PR evidence.
@@ -11,7 +11,6 @@ import path from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getSandboxDuplexGatewayCodecSource } from "./sandbox-callback-bridge.js";
import {
createBridgeBodyReservation,
getBridgeBodyReservedBytesForTest,
@@ -119,6 +118,28 @@ function createRecordingTraceContext(): {
describe("sandbox adapter execution targets", () => {
const cleanupDirs: string[] = [];
function processSessionInput(
runId: string,
target: AdapterSandboxExecutionTarget,
runtimeRootDir: string,
command: string,
args: string[],
cwd: string,
): Parameters<typeof startAdapterExecutionTargetProcessSessionBridge>[0] {
return {
runId,
target,
runtimeRootDir,
adapterKey: "acpx",
command,
args,
cwd,
env: {},
timeoutSec: 5,
onLog: async () => {},
};
}
async function createSandboxRoot(prefix: string): Promise<string> {
const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix));
cleanupDirs.push(rootDir);
@@ -512,18 +533,14 @@ describe("sandbox adapter execution targets", () => {
};
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-makedir",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-makedir",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
try {
@@ -769,16 +786,14 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-poll-parent",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput(
"run-process-session-poll-parent",
target,
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
),
getRuntimeParentContext: () => runParentToken,
});
expect(bridge).not.toBeNull();
@@ -824,18 +839,14 @@ describe("sandbox adapter execution targets", () => {
};
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-poll-nogetter",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-poll-nogetter",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
bridgeStarted = true;
@@ -886,16 +897,14 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir, runner);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-stdin-parent",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput(
"run-process-session-stdin-parent",
target,
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
),
getRuntimeParentContext: () => currentParent as never,
});
expect(bridge).not.toBeNull();
@@ -960,17 +969,14 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-sendinput-span",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
// Record each wrapper span name, then run the wrapped work.
...processSessionInput(
"run-process-session-sendinput-span",
target,
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
),
runtimeSpan: async (name, work) => {
spanNames.push(name);
if (name === "sandbox.agentSession.sendInput") resolveSendInput();
@@ -1024,17 +1030,14 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-poll-span",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
// Record each wrapper span name, then run the wrapped work.
...processSessionInput(
"run-process-session-poll-span",
target,
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
),
runtimeSpan: async (name, work) => {
spanNames.push(name);
if (name === "sandbox.agentSession.pollOutput") resolvePoll();
@@ -1066,18 +1069,7 @@ describe("sandbox adapter execution targets", () => {
);
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput("run-process-session", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir));
expect(bridge).not.toBeNull();
try {
@@ -1344,18 +1336,14 @@ describe("sandbox adapter execution targets", () => {
);
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-buffer",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-buffer",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
try {
@@ -1382,18 +1370,14 @@ describe("sandbox adapter execution targets", () => {
);
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-fast-exit",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-fast-exit",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
try {
@@ -1412,18 +1396,14 @@ describe("sandbox adapter execution targets", () => {
await writeFile(childPath, "process.stdout.write('guarded-out\\n');", "utf8");
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-auth",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-auth",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
let squatter: net.Socket | null = null;
@@ -1484,18 +1464,14 @@ describe("sandbox adapter execution targets", () => {
);
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-stream",
const bridge = await startAdapterExecutionTargetProcessSessionBridge(processSessionInput(
"run-process-session-stream",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
});
path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
process.execPath,
[childPath],
rootDir,
));
expect(bridge).not.toBeNull();
const child = spawn(bridge!.agentCommand, [], { stdio: ["pipe", "pipe", "pipe"] });
@@ -1563,16 +1539,7 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-echo",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-echo", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
});
expect(bridge).not.toBeNull();
@@ -1609,18 +1576,8 @@ describe("sandbox adapter execution targets", () => {
const spanNames: string[] = [];
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-span",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-span", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
// Record each wrapper span name, then run the wrapped work.
runtimeSpan: async (name, work) => {
spanNames.push(name);
return work();
@@ -1660,16 +1617,7 @@ describe("sandbox adapter execution targets", () => {
const runtimeSpan = createRuntimeSpanRunner(traceContext, () => runRootContext);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-parent",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-parent", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
runtimeSpan,
});
@@ -1761,16 +1709,7 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-buffer",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-buffer", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
});
expect(bridge).not.toBeNull();
@@ -1802,16 +1741,7 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-fast-exit",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-fast-exit", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
});
expect(bridge).not.toBeNull();
@@ -1847,16 +1777,7 @@ describe("sandbox adapter execution targets", () => {
const target: AdapterSandboxExecutionTarget = localSandboxTarget(rootDir);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-stream-live",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: {},
timeoutSec: 5,
onLog: async () => {},
...processSessionInput("run-stream-live", target, path.posix.join(rootDir, ".paperclip-runtime", "acpx"), process.execPath, [childPath], rootDir),
streamOutputViaSession: true,
});
expect(bridge).not.toBeNull();
@@ -4483,7 +4404,8 @@ describe("sandbox adapter execution targets", () => {
// sentinel reaches any of them on the http2 path.
const telemetryDump = JSON.stringify({ spans, counters, events });
const logDump = logLines.join("");
for (const sentinel of sentinels) {
expect(bridgeToken).not.toBe("");
for (const sentinel of [...sentinels, bridgeToken]) {
expect(telemetryDump).not.toContain(sentinel);
expect(logDump).not.toContain(sentinel);
}
@@ -4981,41 +4903,6 @@ describe("sandbox adapter execution targets", () => {
}
}, 20000);
it("test_a_missing_preface_aborts_the_open_and_falls_back_to_queue_v1", async () => {
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,
// then the bounded preface scan finds nothing before its bound; the host
// must abort the open and select the file bridge instead of hanging or
// leaking the channel.
const { runner, control } = makeHttp2SelectionRunner((ctx) => {
ctx.emitReady();
// No connectHttp2() call: the client preface never arrives.
});
const { recorder, counters } = createRecordingDuplexRecorder();
const target: AdapterSandboxExecutionTarget = duplexSandboxTarget(remoteCwd, runner);
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,
duplexObservabilityRecorder: recorder,
});
try {
expect(bridge).not.toBeNull();
expect(control.openCount).toBe(1);
// The missing preface aborted the open; the file bridge serves.
expect(bridge?.env.PAPERCLIP_API_BRIDGE_MODE).toBe("queue_v1");
const fallback = counters.find((c) => c.metric === DUPLEX_COUNTER_FALLBACK_TOTAL);
expect(fallback?.dimensions.fallback_reason).toBe("preface_missing");
// The bounded cleanup left no live provider session.
expect(control.closeCount + control.stopCount).toBeGreaterThanOrEqual(1);
} finally {
await bridge?.stop();
await api.close();
}
}, 20000);
it("test_disabled_flag_selects_queue_v1", async () => {
const { rootDir, remoteCwd } = await createSandboxWorkspace("paperclip-http2-disabled-");
@@ -5074,6 +4961,7 @@ describe("sandbox adapter execution targets", () => {
const fallbacks = counters.filter((c) => c.metric === DUPLEX_COUNTER_FALLBACK_TOTAL);
expect(fallbacks).toHaveLength(1);
expect(fallbacks[0]?.dimensions.fallback_reason).toBe("preface_missing");
expect(control.closeCount + control.stopCount).toBeGreaterThanOrEqual(1);
} finally {
await bridge?.stop();
await api.close();
@@ -5974,39 +5862,6 @@ describe("sandbox adapter execution targets", () => {
});
// The names the embedded codec source declares. A test wraps the source and
// reads these names back. The generated gateway never decodes, so the embedded
// copy declares only the frame version and the encode function.
interface EmbeddedCodec {
encodeDuplexFrame: (frame: unknown) => string;
DUPLEX_FRAME_VERSION: number;
}
// This describe block covers the zero-dependency codec every generated
// gateway embeds (`DUPLEX_GATEWAY_CODEC_SOURCE`). The `http2_v1` gateway
// embeds this same codec source to send its one READY line, so this coverage
// stays live for the active transport.
describe("embedded sandbox gateway codec", () => {
it("encodes the READY frame to the exact byte-for-byte wire format", () => {
// `JSON.stringify` writes object keys in insertion order, so this pins the
// exact key order the gateway writes: version, then type, then nonce. A
// reordered or reformatted call site would change the bytes on the wire
// without failing a looser, parse-then-compare assertion.
const codecFactory = new Function(
`${getSandboxDuplexGatewayCodecSource()}\nreturn { encodeDuplexFrame, DUPLEX_FRAME_VERSION };`,
) as unknown as () => EmbeddedCodec;
const codec = codecFactory();
expect(codec.DUPLEX_FRAME_VERSION).toBe(DUPLEX_FRAME_VERSION);
const nonce = "fixed-test-nonce";
const encoded = codec.encodeDuplexFrame({ version: codec.DUPLEX_FRAME_VERSION, type: "ready", nonce });
expect(encoded).toBe(`{"version":2,"type":"ready","nonce":"${nonce}"}\n`);
// The host encode side produces the same bytes for the same frame, so the
// two copies stay wire compatible on the one frame the gateway still sends.
expect(encoded).toBe(encodeDuplexFrame({ version: DUPLEX_FRAME_VERSION, type: "ready", nonce }));
});
});
/** Wait for the pending microtasks and macrotasks to settle. */
function flushMacrotasks(): Promise<void> {
@@ -6106,31 +5961,6 @@ function createFakeBridgeTransport() {
};
}
describe("settleRunDisposition atomic read and mark", () => {
it("marks the orderly completion and reports a success for a healthy channel", async () => {
const broker = createFakeBridgeTransport();
// The one atomic step marks the orderly completion and reads the success.
expect(broker.settleRunDisposition()).toEqual({ failed: false, lossReason: null });
// A later teardown loss orders after the mark, so it stays a normal teardown.
broker.emitExit();
await flushMacrotasks();
expect(broker.runDisposition).toEqual({ failed: false, lossReason: null });
});
it("reports the failure and does not mark for a latched loss", async () => {
const broker = createFakeBridgeTransport();
// A loss ordered before any orderly completion latches the failure.
broker.emitExit();
await flushMacrotasks();
// The atomic step reads the failure and no-ops the mark, so a later
// completion cannot clear the latch.
expect(broker.settleRunDisposition()).toEqual({ failed: true, lossReason: "provider_exit" });
broker.markOrderlyCompletion();
expect(broker.runDisposition).toEqual({ failed: true, lossReason: "provider_exit" });
});
});
describe("CLI-lane run-disposition seam", () => {
const CLEAN_RESULT = {
@@ -6243,27 +6073,6 @@ describe("CLI-lane run-disposition seam", () => {
expect(broker.runDisposition.failed).toBe(false);
});
it("cannot clear the loss latch with a later completion", async () => {
const broker = createFakeBridgeTransport();
// The loss latches before the CLI process exits.
broker.emitExit();
await flushMacrotasks();
// A later orderly completion cannot clear the latch.
broker.markOrderlyCompletion();
const runner = mockRunner({ ...CLEAN_RESULT });
const result = await runAdapterExecutionTargetProcess("run-cli-latch", sandboxTarget(runner), "agent-cli", [], {
cwd: "/local",
env: {},
timeoutSec: 5,
graceSec: 1,
onLog: async () => {},
settleRunDisposition: () => broker.settleRunDisposition(),
});
expect(result.exitCode).toBe(1);
expect(result.errorCode).toBe(DUPLEX_CHANNEL_LOST_ERROR_CODE);
});
it("leaves an already-failed CLI result unchanged and never settles the disposition", async () => {
const broker = createFakeBridgeTransport();
@@ -24,6 +24,7 @@ import {
startSandboxCallbackBridgeWorker,
} from "./sandbox-callback-bridge.js";
import type { SandboxCallbackBridgeQueueClient } from "./sandbox-callback-bridge.js";
import { DUPLEX_FRAME_VERSION, encodeDuplexFrame } from "./duplex-frame-codec.js";
import { createHttp2BridgeServer } from "./http2-bridge-server.js";
import type { Http2BridgeForwardRequest, Http2BridgeForwardResult } from "./http2-bridge-server.js";
import type { CommandManagedDuplexChannel } from "./command-managed-runtime.js";
@@ -34,6 +35,64 @@ const execFile = promisify(execFileCallback);
describe("sandbox callback bridge", () => {
const cleanupDirs: string[] = [];
function queueRuntimeInput(
runner: ReturnType<typeof createExecRunner>,
remoteWorkspaceDir: string,
localWorkspaceDir: string,
bridgeAsset: Awaited<ReturnType<typeof createSandboxCallbackBridgeAsset>>,
): Parameters<typeof prepareCommandManagedRuntime>[0] {
return {
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [
{
key: "bridge",
localDir: bridgeAsset.localDir,
},
],
};
}
function createWorkerWaiter() {
return async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
}
function virtualQueueReadClient(
directories: ReturnType<typeof sandboxCallbackBridgeDirectories>,
requestBodies: Map<string, string>,
): Pick<SandboxCallbackBridgeQueueClient, "makeDir" | "makeDirs" | "listJsonFiles" | "readTextFile" | "writeTextFile"> {
return {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
};
}
const cleanupFns: Array<() => Promise<void>> = [];
function createExecRunner() {
@@ -146,21 +205,7 @@ describe("sandbox callback bridge", () => {
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [
{
key: "bridge",
localDir: bridgeAsset.localDir,
},
],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -938,16 +983,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -1031,16 +1067,7 @@ describe("sandbox callback bridge", () => {
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -1129,16 +1156,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const bridgeToken = createSandboxCallbackBridgeToken();
@@ -1181,16 +1199,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: {
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
},
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -1953,16 +1962,7 @@ describe("sandbox callback bridge", () => {
// not write a 503 there; a 503 makes the caller retry while the original
// mutation still completes, applying it twice. The test proves no 503 lands
// and the handler's real response is delivered exactly once.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -1983,20 +1983,7 @@ describe("sandbox callback bridge", () => {
let handlerCompleted = false;
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
return { wrote: true };
@@ -2066,16 +2053,7 @@ describe("sandbox callback bridge", () => {
// operation started, so the handler finalizes a non-retryable 504, not a
// retryable 502; the caller then does not retry a mutation that may have
// committed.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2089,20 +2067,7 @@ describe("sandbox callback bridge", () => {
const requestRemovals: string[] = [];
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
return { wrote: true };
@@ -2163,16 +2128,7 @@ describe("sandbox callback bridge", () => {
// gets a terminal response even when the handler ignores the abort. The 504
// is non-retryable, so the caller does not retry a mutation that may have
// committed.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2186,20 +2142,7 @@ describe("sandbox callback bridge", () => {
const requestRemovals: string[] = [];
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
return { wrote: true };
@@ -2261,16 +2204,7 @@ describe("sandbox callback bridge", () => {
// host operation that is in flight. The handler completes late, after the
// abort. Its response must be a non-retryable 504, never a retryable 502 or
// 503; a retry of either would apply the mutation twice.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2284,20 +2218,7 @@ describe("sandbox callback bridge", () => {
const requestRemovals: string[] = [];
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
return { wrote: true };
@@ -2366,16 +2287,7 @@ describe("sandbox callback bridge", () => {
// handler, so the caller waits to its own deadline and gets a generic 502. The
// recovery path retries the write and removes the request file only after the
// write lands.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2390,20 +2302,7 @@ describe("sandbox callback bridge", () => {
let writeAttempts = 0;
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
writeAttempts += 1;
// The first backstop write fails; the retry then succeeds.
@@ -2464,16 +2363,7 @@ describe("sandbox callback bridge", () => {
// once. `finalize` must retry the write and deliver the real response. It
// must not fence out recovery and leave the request for a retryable 503,
// which would let the caller repeat a possibly-committed mutation.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2488,20 +2378,7 @@ describe("sandbox callback bridge", () => {
let writeAttempts = 0;
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
writeAttempts += 1;
// The first handler-response write fails; the retry then succeeds.
@@ -2555,16 +2432,7 @@ describe("sandbox callback bridge", () => {
// recovery still delivers a non-retryable terminal response. Without the
// re-arm, a rejected write leaves the request for a retryable 503 and a hung
// write strands the caller until its own deadline.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2579,20 +2447,7 @@ describe("sandbox callback bridge", () => {
let writeAttempts = 0;
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
writeAttempts += 1;
// Fail every `finalize` write attempt. The re-armed 504 backstop then
@@ -2657,16 +2512,7 @@ describe("sandbox callback bridge", () => {
// unclaimed, so its host mutation never ran; the 503 stays retry-safe. (A
// hung poll no longer triggers this pass — the loop backs off and retries
// the poll instead, and a sustained hang is the watchdog's job.)
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2753,16 +2599,7 @@ describe("sandbox callback bridge", () => {
// of dropping it. Without the fix, the recovery removed the request before its
// single write attempt, so a failed write left no queue state; a later
// recovery pass found nothing and the caller waited until its own deadline.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2838,33 +2675,12 @@ describe("sandbox callback bridge", () => {
await worker.stop({ drainTimeoutMs: 10 });
});
it("maps an indeterminate host outcome to a non-retryable 409 in the in-sandbox bridge server", () => {
// The in-sandbox server returns the host response to the sandbox caller. A 5xx
// status is retryable by convention, so the server must not forward the
// indeterminate 504 as a retry-safe status. It maps the indeterminate outcome
// to a non-retryable 409, so a caller that retries 5xx does not repeat a
// possibly-committed mutation. The outcome header and body still forward, so a
// caller that reads them still sees the indeterminate result.
const source = getSandboxCallbackBridgeServerSource();
expect(source).toContain("x-paperclip-bridge-outcome");
expect(source).toContain('=== "indeterminate"');
expect(source).toContain("res.statusCode = 409");
});
it("abandons a request before its handler starts, so the mutation never runs", async () => {
// Prove the reverse race. When the recovery path claims a request before the
// handler starts its host operation, the handler must bail without running
// the mutation. A retry after the 503 then applies the mutation once.
const waitFor = async (predicate: () => boolean, timeoutMs: number) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error("waitFor timed out");
};
const waitFor = createWorkerWaiter();
const queueDir = "/virtual-bridge/queue";
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -2877,20 +2693,7 @@ describe("sandbox callback bridge", () => {
const responseWrites: Array<{ path: string; status: number }> = [];
const client: SandboxCallbackBridgeQueueClient = {
makeDir: async () => {},
makeDirs: async () => {},
listJsonFiles: async (dir) =>
dir === directories.requestsDir
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
: [],
readTextFile: async (remotePath) => {
const body = requestBodies.get(remotePath);
if (body === undefined) {
throw new Error(`missing request ${remotePath}`);
}
return body;
},
writeTextFile: async () => {},
...virtualQueueReadClient(directories, requestBodies),
writeResponseFile: async (remotePath, body) => {
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
return { wrote: true };
@@ -3044,13 +2847,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
return {
runner,
@@ -3405,6 +3202,7 @@ describe("sandbox callback bridge", () => {
// would change the bytes on the wire without failing a looser assertion.
const readyLine = firstBytes.subarray(0, newlineIndex).toString("utf8");
expect(readyLine).toBe(`{"version":2,"type":"ready","nonce":"${nonce}"}`);
expect(encodeDuplexFrame({ version: DUPLEX_FRAME_VERSION, type: "ready", nonce })).toBe(`${readyLine}\n`);
const afterReady = firstBytes.subarray(newlineIndex + 1, newlineIndex + 1 + preface.length);
expect(afterReady).toEqual(preface);
}, 15_000);
@@ -3861,13 +3659,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const directories = sandboxCallbackBridgeDirectories(queueDir);
@@ -3917,13 +3709,7 @@ describe("sandbox callback bridge", () => {
const runner = createExecRunner();
const bridgeAsset = await createSandboxCallbackBridgeAsset();
cleanupFns.push(bridgeAsset.cleanup);
const prepared = await prepareCommandManagedRuntime({
runner,
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
adapterKey: "codex",
workspaceLocalDir: localWorkspaceDir,
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
});
const prepared = await prepareCommandManagedRuntime(queueRuntimeInput(runner, remoteWorkspaceDir, localWorkspaceDir, bridgeAsset));
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
const bridgeToken = createSandboxCallbackBridgeToken();
File diff suppressed because it is too large. Load diff
@@ -398,29 +398,6 @@ describe("a rejected capability resolution fails closed for persistent-session b
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
});
it("omits the snapshot when the resolution rejects", async () => {
// A rejected resolution carries no snapshot; the target never publishes a
// guessed capability set.
const { target } = await buildSandboxTarget({
snapshot: null,
supportsSync: false,
rejectResolution: true,
});
expect(target.effectiveCapabilities).toBeUndefined();
});
it("carries no snapshot for session-output streaming when the resolution rejects", async () => {
// A rejected resolution must not read as an open grant that enables
// streaming. The target carries no snapshot, so the bridge keeps the host
// output-file poll path.
const { target } = await buildSandboxTarget({
snapshot: null,
supportsSync: false,
rejectResolution: true,
});
expect(target.effectiveCapabilities).toBeUndefined();
});
it("never forces the persistent session when the resolution rejects", async () => {
// The bridge asks for the session with `useSession`, but a rejected
// resolution fails closed, so the seam runs the command one-shot instead.
@@ -429,6 +406,7 @@ describe("a rejected capability resolution fails closed for persistent-session b
supportsSync: false,
rejectResolution: true,
});
expect(target.effectiveCapabilities).toBeUndefined();
await target.runner?.execute({ command: "node", args: ["agent.js"], useSession: true });
const call = execute.mock.calls[0]![0] as { forceSession?: boolean };
expect(call.forceSession).toBe(false);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+31 -57
View File
@@ -475,6 +475,31 @@ function renderProperties(container: HTMLDivElement, props: ComponentProps<typeo
return root;
}
function seniorProductEngineer() {
return {
id: "agent-1",
name: "Senior Product Engineer",
role: "engineer",
title: null,
status: "active",
adapterType: "codex_local",
icon: null,
};
}
function reviewPolicyStages(): Pick<IssueExecutionPolicy, "stages"> {
return {
stages: [
{
id: "00000000-0000-4000-8000-000000000010",
type: "review",
approvalsNeeded: 1,
participants: [{ id: "00000000-0000-4000-8000-000000000012", type: "agent", agentId: "00000000-0000-4000-8000-000000000013", userId: null }],
},
],
};
}
describe("IssueProperties", () => {
let container: HTMLDivElement;
@@ -2239,15 +2264,7 @@ describe("IssueProperties", () => {
it("hides model options when the issue uses the responsible default", async () => {
mockAgentsApi.list.mockResolvedValue([
{
id: "agent-1",
name: "Senior Product Engineer",
role: "engineer",
title: null,
status: "active",
adapterType: "codex_local",
icon: null,
},
seniorProductEngineer(),
]);
const root = renderProperties(container, {
@@ -2269,15 +2286,7 @@ describe("IssueProperties", () => {
it("edits existing custom assignee model options from the properties pane", async () => {
const onUpdate = vi.fn();
mockAgentsApi.list.mockResolvedValue([
{
id: "agent-1",
name: "Senior Product Engineer",
role: "engineer",
title: null,
status: "active",
adapterType: "codex_local",
icon: null,
},
seniorProductEngineer(),
]);
mockAgentsApi.adapterModels.mockResolvedValue([
{ id: "gpt-6-astra", label: "gpt-6-astra" },
@@ -2391,15 +2400,7 @@ describe("IssueProperties", () => {
it("clears existing assignee adapter overrides from the properties pane", async () => {
const onUpdate = vi.fn();
mockAgentsApi.list.mockResolvedValue([
{
id: "agent-1",
name: "Senior Product Engineer",
role: "engineer",
title: null,
status: "active",
adapterType: "codex_local",
icon: null,
},
seniorProductEngineer(),
]);
const root = renderProperties(container, {
@@ -2663,16 +2664,7 @@ describe("IssueProperties", () => {
const onUpdate = vi.fn();
const root = renderProperties(container, {
issue: createIssue({
executionPolicy: createExecutionPolicy({
stages: [
{
id: "00000000-0000-4000-8000-000000000010",
type: "review",
approvalsNeeded: 1,
participants: [{ id: "00000000-0000-4000-8000-000000000012", type: "agent", agentId: "00000000-0000-4000-8000-000000000013", userId: null }],
},
],
}),
executionPolicy: createExecutionPolicy(reviewPolicyStages()),
}),
childIssues: [],
onUpdate,
@@ -2721,16 +2713,7 @@ describe("IssueProperties", () => {
const root = renderProperties(container, {
issue: createIssue({
status: "in_progress",
executionPolicy: createExecutionPolicy({
stages: [
{
id: "00000000-0000-4000-8000-000000000010",
type: "review",
approvalsNeeded: 1,
participants: [{ id: "00000000-0000-4000-8000-000000000012", type: "agent", agentId: "00000000-0000-4000-8000-000000000013", userId: null }],
},
],
}),
executionPolicy: createExecutionPolicy(reviewPolicyStages()),
executionState: createExecutionState(),
}),
childIssues: [],
@@ -2747,16 +2730,7 @@ describe("IssueProperties", () => {
const root = renderProperties(container, {
issue: createIssue({
status: "in_review",
executionPolicy: createExecutionPolicy({
stages: [
{
id: "00000000-0000-4000-8000-000000000010",
type: "review",
approvalsNeeded: 1,
participants: [{ id: "00000000-0000-4000-8000-000000000012", type: "agent", agentId: "00000000-0000-4000-8000-000000000013", userId: null }],
},
],
}),
executionPolicy: createExecutionPolicy(reviewPolicyStages()),
executionState: createExecutionState({
status: "pending",
currentStageType: "review",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+132 -326
View File
@@ -283,6 +283,65 @@ async function gotoLinkFrame(container: HTMLDivElement, url: string) {
await passAccessStep();
}
function notionConnection(id: string, credentialPolicy: string, status: string) {
return {
id: id,
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: credentialPolicy,
status: status,
config: { sourceTemplateKey: "notion" },
transportConfig: {},
};
}
function notionApplication(status: string) {
return {
id: "app-notion",
status: status,
metadata: { sourceTemplateKey: "notion" },
};
}
function oauthStartResponse(connectionId: string, provider: string, authorizationUrl: string) {
return {
connectionId: connectionId,
provider: provider,
authorizationUrl: authorizationUrl,
expiresAt: "2099-01-01T00:00:00.000Z",
};
}
function activeConnectorEnrollment() {
return {
configured: true,
status: "active",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: "inst-test",
environment: "staging",
origins: ["https://paperclip.example.test"],
};
}
function unconfiguredConnectorEnrollment() {
return {
configured: false,
status: "not_configured",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: null,
environment: "staging",
origins: [],
};
}
function customerOwnedApp(app: typeof GMAIL) {
return {
...app,
methods: app.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
};
}
describe("AppsConnect — Connect with a link (M4 frame)", () => {
let container: HTMLDivElement;
let mountedRoot: Root | null;
@@ -315,22 +374,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
listApplicationsMock.mockResolvedValue({ applications: [] });
listConnectionsMock.mockResolvedValue({ connections: [] });
getConnectionInstallsMock.mockResolvedValue({ installs: [{ targetType: "company", targetId: "company-1" }] });
startOAuthMock.mockResolvedValue({
connectionId: "conn-notion",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=resumed",
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValue(oauthStartResponse("conn-notion", "notion", "https://mcp.notion.com/authorize?state=resumed"));
finishAppMock.mockResolvedValue({});
putConnectionInstallsMock.mockResolvedValue({ connectionId: "conn-1", installs: [] });
getCloudConnectorEnrollmentMock.mockResolvedValue({
configured: true,
status: "active",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: "inst-test",
environment: "staging",
origins: ["https://paperclip.example.test"],
});
getCloudConnectorEnrollmentMock.mockResolvedValue(activeConnectorEnrollment());
startCloudConnectorEnrollmentMock.mockResolvedValue({
configured: false,
status: "pending",
@@ -1406,20 +1453,9 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("prompts for one-time Cloud registration inside Gmail when managed methods are not yet advertised", async () => {
mockParams.appKey = "gmail";
listGalleryMock.mockResolvedValue({
apps: [{
...GMAIL,
methods: GMAIL.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
});
getCloudConnectorEnrollmentMock.mockResolvedValueOnce({
configured: false,
status: "not_configured",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: null,
environment: "staging",
origins: [],
apps: [customerOwnedApp(GMAIL)],
});
getCloudConnectorEnrollmentMock.mockResolvedValueOnce(unconfiguredConnectorEnrollment());
await render();
await passAccessStep();
@@ -1451,10 +1487,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const popup = { closed: false, location: { assign: vi.fn() }, focus: vi.fn(), close: vi.fn() };
const open = vi.spyOn(window, "open").mockReturnValue(popupBlocked ? null : popup as unknown as Window);
listGalleryMock.mockResolvedValue({ apps: [{
...GMAIL, methods: GMAIL.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}] });
listGalleryMock.mockResolvedValue({ apps: [customerOwnedApp(GMAIL)] });
getCloudConnectorEnrollmentMock.mockResolvedValue({ status: "not_configured" });
await render(client, false, <ConnectionSetupFlow host="dialog" serviceSlug="gmail" interactionId="intent-1" requestedAgentId="agent-1" />);
await passAccessStep();
@@ -1491,10 +1524,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it.each(["request", "missing-url", "invalid-url"])("closes the reserved enrollment popup after %s failure", async (failure) => {
const popup = { closed: false, location: { assign: vi.fn() }, focus: vi.fn(), close: vi.fn() };
vi.spyOn(window, "open").mockReturnValue(popup as unknown as Window);
listGalleryMock.mockResolvedValue({ apps: [{
...GMAIL, methods: GMAIL.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}] });
listGalleryMock.mockResolvedValue({ apps: [customerOwnedApp(GMAIL)] });
getCloudConnectorEnrollmentMock.mockResolvedValue({ status: "not_configured" });
if (failure === "request") {
startCloudConnectorEnrollmentMock.mockRejectedValue(new Error("Enrollment unavailable"));
@@ -1530,11 +1560,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
async (expiresAt) => {
mockSearch.value = "source=github&stage=setup";
listGalleryMock.mockResolvedValue({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
apps: [customerOwnedApp(GITHUB)],
});
getCloudConnectorEnrollmentMock.mockResolvedValue({
configured: false,
@@ -1598,20 +1624,9 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("keeps GitHub's personal identity defaults while its managed method awaits enrollment", async () => {
mockParams.appKey = "github";
listGalleryMock.mockResolvedValue({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
});
getCloudConnectorEnrollmentMock.mockResolvedValueOnce({
configured: false,
status: "not_configured",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: null,
environment: "staging",
origins: [],
apps: [customerOwnedApp(GITHUB)],
});
getCloudConnectorEnrollmentMock.mockResolvedValueOnce(unconfiguredConnectorEnrollment());
await render();
@@ -1644,11 +1659,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("explains unavailable GitHub sign-in without silently switching to a PAT", async () => {
mockSearch.value = "source=github&stage=setup&cloud_connector=enrolled";
listGalleryMock.mockResolvedValueOnce({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
apps: [customerOwnedApp(GITHUB)],
});
await render();
@@ -1670,22 +1681,11 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("keeps GitHub sign-in intent when enrollment recovery reveals an unavailable profile", async () => {
mockSearch.value = "source=github&stage=setup&cloud_connector=enrolled";
listGalleryMock.mockResolvedValueOnce({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
apps: [customerOwnedApp(GITHUB)],
});
getCloudConnectorEnrollmentMock
.mockRejectedValueOnce(new Error("Cloud unavailable"))
.mockResolvedValueOnce({
configured: true,
status: "active",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: "inst-test",
environment: "staging",
origins: ["https://paperclip.example.test"],
});
.mockResolvedValueOnce(activeConnectorEnrollment());
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await render(queryClient);
@@ -1706,11 +1706,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("preserves a dedicated agent identity across the full-page enrollment callback", async () => {
mockParams.appKey = "github";
listGalleryMock.mockResolvedValue({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
apps: [customerOwnedApp(GITHUB)],
capabilities: {
canCreateOrganizationGrant: true,
organizationGrantReason: null,
@@ -1718,14 +1714,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
companyInstallReason: null,
},
});
getCloudConnectorEnrollmentMock.mockResolvedValue({
configured: false,
status: "not_configured",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: null,
environment: "staging",
origins: [],
});
getCloudConnectorEnrollmentMock.mockResolvedValue(unconfiguredConnectorEnrollment());
await render();
await openAccessAdvanced();
@@ -1763,14 +1752,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=github&stage=setup&cloud_connector=enrolled";
mockCompany.value = { selectedCompanyId: undefined, selectedCompany: null };
listGalleryMock.mockResolvedValue({ apps: [GITHUB_MANAGED] });
getCloudConnectorEnrollmentMock.mockResolvedValue({
configured: true,
status: "active",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: "inst-test",
environment: "staging",
origins: ["https://paperclip.example.test"],
});
getCloudConnectorEnrollmentMock.mockResolvedValue(activeConnectorEnrollment());
const coldLoadClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await render(coldLoadClient);
@@ -1877,11 +1859,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
it("never renders self-host enrollment when the connector identity is already active", async () => {
mockSearch.value = "source=gmail&stage=setup";
listGalleryMock.mockResolvedValueOnce({
apps: [{
...GMAIL,
methods: GMAIL.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
apps: [customerOwnedApp(GMAIL)],
});
await render();
@@ -2307,17 +2285,34 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
});
it("renders the same PostHog method choices in page and task-dialog hosts", async () => {
const methodState = () => ({
oauth: buttonByText("Sign in with PostHog")?.getAttribute("aria-pressed"),
apiKey: buttonByText("Use a personal API key")?.getAttribute("aria-pressed"),
connectDisabled: buttonByText("Connect")?.disabled,
});
const checkMethodChoices = async () => {
await openAccessAdvanced();
expect(radioContaining("Sign in with PostHog")?.getAttribute("aria-checked")).toBe("true");
expect(radioContaining("Use a personal API key")?.getAttribute("aria-checked")).toBe("false");
expect(buttonByText("Continue to sign in")?.disabled).toBe(false);
await act(async () => {
radioContaining("Use a personal API key")!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(radioContaining("Sign in with PostHog")?.getAttribute("aria-checked")).toBe("false");
expect(radioContaining("Use a personal API key")?.getAttribute("aria-checked")).toBe("true");
expect(buttonByText("Connect")?.disabled).toBe(true);
const keyInput = container.querySelector<HTMLInputElement>('input[type="password"]');
expect(keyInput).toBeTruthy();
await act(async () => setInputValue(keyInput!, "phx_host-test-key"));
await flushReact();
expect(buttonByText("Connect")?.disabled).toBe(false);
await act(async () => {
radioContaining("Sign in with PostHog")!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(buttonByText("Continue to sign in")?.disabled).toBe(false);
};
mockParams.appKey = "posthog";
listGalleryMock.mockResolvedValue({ apps: [POSTHOG] });
const pageRoot = await render();
await passAccessStep();
const pageMethods = methodState();
await checkMethodChoices();
await act(async () => pageRoot.unmount());
container.innerHTML = "";
@@ -2335,12 +2330,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
);
expect(continueButton).toBeTruthy();
expect(continueButton?.disabled).toBe(false);
await checkMethodChoices();
await act(async () => {
continueButton?.click();
buttonByText("Continue to sign in")!.click();
});
await flushReact();
expect(methodState()).toEqual(pageMethods);
expect(connectAppMock).toHaveBeenCalledTimes(1);
await act(async () => dialogRoot.unmount());
});
@@ -2512,21 +2507,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
);
});
it("shows an in-flight state while Paperclip prepares Notion sign-in", async () => {
mockSearch.value = "source=notion";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
connectAppMock.mockReturnValueOnce(new Promise(() => {}));
await render();
await passAccessStep();
await submitCuratedOAuthSetup();
expect(container.textContent).toContain("Connect Notion");
expect(container.textContent).toContain("Preparing secure sign-in");
expect(container.textContent).toContain("Preparing…");
expect(connectAppMock).toHaveBeenCalledTimes(1);
});
it("keeps the same steps when Notion sign-in takes over the wizard", async () => {
mockSearch.value = "source=notion";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
@@ -2543,6 +2523,9 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
await submitCuratedOAuthSetup();
expect(container.textContent).toContain("Preparing secure sign-in");
expect(container.textContent).toContain("Connect Notion");
expect(container.textContent).toContain("Preparing…");
expect(connectAppMock).toHaveBeenCalledTimes(1);
expect(stepLabelsOnScreen()).toEqual([]);
expect(stepDotCount()).toBe(0);
});
@@ -2569,29 +2552,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = `source=notion&intent=${interactionId}`;
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "draft",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("draft")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
id: "conn-existing",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "per_user",
status: "draft",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
});
startOAuthMock.mockResolvedValueOnce({
connectionId: "conn-existing",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=existing",
expiresAt: "2099-01-01T00:00:00.000Z",
connections: [notionConnection("conn-existing", "per_user", "draft")],
});
startOAuthMock.mockResolvedValueOnce(oauthStartResponse("conn-existing", "notion", "https://mcp.notion.com/authorize?state=existing"));
await render();
expect(container.textContent).not.toContain("Reconnect keeps this identity type");
@@ -2614,32 +2580,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&resume=22222222-2222-4222-8222-222222222222";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "active",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("active")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [
{
id: "11111111-1111-4111-8111-111111111111",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "shared",
status: "draft",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
},
{
id: "22222222-2222-4222-8222-222222222222",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "per_user",
status: "draft",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
},
notionConnection("11111111-1111-4111-8111-111111111111", "shared", "draft"),
notionConnection("22222222-2222-4222-8222-222222222222", "per_user", "draft"),
],
});
@@ -2688,12 +2634,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
transportConfig: {},
}],
});
startOAuthMock.mockResolvedValueOnce({
connectionId,
provider: "clickup",
authorizationUrl: "https://app.clickup.com/api?state=resumed",
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValueOnce(oauthStartResponse(connectionId, "clickup", "https://app.clickup.com/api?state=resumed"));
await render();
await vi.waitFor(() => {
@@ -2971,11 +2912,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&resume=22222222-2222-4222-8222-222222222222&oauth=denied&code=oauth_authorization_denied";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "draft",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("draft")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
@@ -3016,29 +2953,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "active",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("active")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
id: "conn-existing",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy,
status: "active",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
});
startOAuthMock.mockResolvedValueOnce({
connectionId: "conn-existing",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=existing",
expiresAt: "2099-01-01T00:00:00.000Z",
connections: [notionConnection("conn-existing", credentialPolicy, "active")],
});
startOAuthMock.mockResolvedValueOnce(oauthStartResponse("conn-existing", "notion", "https://mcp.notion.com/authorize?state=existing"));
await render();
expect(container.textContent).toContain(
@@ -3057,22 +2977,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&applicationId=app-notion&new=1&reconnect=conn-archived&identity=organization";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "archived",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("archived")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
id: "conn-archived",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "shared",
status: "archived",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
connections: [notionConnection("conn-archived", "shared", "archived")],
});
connectAppMock.mockResolvedValueOnce({
connectionId: "conn-archived",
@@ -3087,12 +2995,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
suggestedDefaults: {},
auth: { kind: "oauth", startUrl: null },
});
startOAuthMock.mockResolvedValueOnce({
connectionId: "conn-archived",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=revived",
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValueOnce(oauthStartResponse("conn-archived", "notion", "https://mcp.notion.com/authorize?state=revived"));
await render();
expect(container.textContent).toContain("Connects for everyone in your organization");
@@ -3113,22 +3016,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&applicationId=app-notion&new=1&reconnect=conn-archived&identity=organization";
listGalleryMock.mockRejectedValueOnce(new Error("Gallery unavailable"));
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "archived",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("archived")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
id: "conn-archived",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "shared",
status: "archived",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
connections: [notionConnection("conn-archived", "shared", "archived")],
});
await render();
@@ -3152,22 +3043,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&applicationId=app-notion&new=1&reconnect=conn-archived&identity=organization";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
const applications = {
applications: [{
id: "app-notion",
status: "archived",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("archived")],
};
const connections = {
connections: [{
id: "conn-archived",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "shared",
status: "archived",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
connections: [notionConnection("conn-archived", "shared", "archived")],
};
if (failedLookup === "connection") {
listConnectionsMock
@@ -3204,11 +3083,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
mockSearch.value = "source=notion&applicationId=app-notion&new=1";
listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] });
listApplicationsMock.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "active",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("active")],
});
listConnectionsMock.mockResolvedValueOnce({
connections: [{
@@ -3279,22 +3154,10 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
expect(startOAuthMock).not.toHaveBeenCalled();
resolveApplications({
applications: [{
id: "app-notion",
status: "draft",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("draft")],
});
resolveConnections({
connections: [{
id: "conn-refreshed",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "per_user",
status: "draft",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
connections: [notionConnection("conn-refreshed", "per_user", "draft")],
});
await flushReact();
await flushReact();
@@ -3316,24 +3179,12 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
listApplicationsMock
.mockRejectedValueOnce(new Error("Lookup unavailable"))
.mockResolvedValueOnce({
applications: [{
id: "app-notion",
status: "draft",
metadata: { sourceTemplateKey: "notion" },
}],
applications: [notionApplication("draft")],
});
listConnectionsMock
.mockResolvedValueOnce({ connections: [] })
.mockResolvedValueOnce({
connections: [{
id: "conn-after-retry",
applicationId: "app-notion",
authKind: "oauth",
credentialPolicy: "per_user",
status: "draft",
config: { sourceTemplateKey: "notion" },
transportConfig: {},
}],
connections: [notionConnection("conn-after-retry", "per_user", "draft")],
});
await render();
@@ -3392,12 +3243,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
});
startOAuthMock
.mockRejectedValueOnce(new Error("Provider unavailable"))
.mockResolvedValueOnce({
connectionId: "conn-prepared",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=retry",
expiresAt: "2099-01-01T00:00:00.000Z",
});
.mockResolvedValueOnce(oauthStartResponse("conn-prepared", "notion", "https://mcp.notion.com/authorize?state=retry"));
await render();
await passAccessStep();
@@ -3445,12 +3291,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
}],
});
connectAppMock.mockRejectedValueOnce(new Error("Response lost"));
startOAuthMock.mockResolvedValueOnce({
connectionId: "conn-response-lost",
provider: "notion",
authorizationUrl: "https://mcp.notion.com/authorize?state=recovered",
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValueOnce(oauthStartResponse("conn-response-lost", "notion", "https://mcp.notion.com/authorize?state=recovered"));
await render();
await passAccessStep();
@@ -3921,31 +3762,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
});
});
it("uses the gallery app name automatically", async () => {
await render();
await act(async () => {
buttonContaining("GitHub")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
await passAccessStep();
const keyField = container.querySelector<HTMLInputElement>("input[type=password]");
await act(async () => setInputValue(keyField!, "secret-key"));
await flushReact();
await act(async () => {
buttonByText("Connect")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(connectAppMock).toHaveBeenCalledTimes(1);
const [, input] = connectAppMock.mock.calls[0];
expect(input).toMatchObject({
galleryKey: "github",
name: "GitHub for the organization",
});
});
it("uses the Google Sheets app name automatically", async () => {
listGalleryMock.mockResolvedValueOnce({
apps: [
@@ -4045,12 +3861,7 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => {
listAgentsMock.mockResolvedValue([]);
finishAppMock.mockResolvedValue({});
putConnectionInstallsMock.mockResolvedValue({ connectionId: "conn-1", installs: [] });
startOAuthMock.mockResolvedValue({
connectionId: "conn-1",
provider: "mcp_example_test",
authorizationUrl: "https://auth.example.test/authorize?state=abc",
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValue(oauthStartResponse("conn-1", "mcp_example_test", "https://auth.example.test/authorize?state=abc"));
connectAppMock.mockResolvedValue({
connectionId: "conn-1",
application: { id: "app-1", name: "mcp.example.test" },
@@ -4410,12 +4221,7 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => {
suggestedDefaults: {},
auth: { kind: "oauth", startUrl: null },
});
startOAuthMock.mockResolvedValue({
connectionId: "conn-1",
provider: "mcp_example_test",
authorizationUrl,
expiresAt: "2099-01-01T00:00:00.000Z",
});
startOAuthMock.mockResolvedValue(oauthStartResponse("conn-1", "mcp_example_test", authorizationUrl));
await render();
await gotoLinkFrame(container, "https://mcp.example.test/mcp");
await act(async () => {