Repository navigation
test(ui-react): attribute initial share confirmation stalls before ownership proof #2401
Description
Context
Three real-browser cases in packages/ui-react/tests/share-draft-runtime.test.ts (direct share announcement preserves ownership: same draft | new chat B | existing conversation, loop at :79) time out at the initial post-confirmation wait (until, packages/ui-react/tests/share-draft-runtime.test.ts:67-69; wait at :95, originally :91) when the test process runs inside a private network namespace that has only loopback.
Cause (settled, reproduced 2026-10-19 at main 09eb1f78e): in a loopback-only namespace Chrome reports navigator.onLine === false. Share intake requires both a connected WebSocket and navigator.onLine !== false (canUpload, packages/ui-react/src/hooks/use-share-intake.ts:69). A confirmed review therefore pauses before upload (if (!canUpload()), packages/ui-react/src/hooks/use-share-intake.ts:173-181) with "Waiting for connection…", and no /api/share request is made. The reconnect effect reads the same signal (packages/ui-react/src/hooks/use-share-intake.ts:324).
| Network namespace | Result | navigator.onLine (Chrome for Testing 153) |
|---|---|---|
| Loopback only | 0/3 | false |
Loopback plus non-loopback dummy (198.18.0.1/32, addrgenmode none, noprefixroute) |
3/3 | true |
| Host network | 3/3 | true |
Other product readers of the same signal are equally affected by a loopback-only harness: isOffline (packages/ui-react/src/components/activity/activity-report.tsx:62-64), Composer (packages/ui-react/src/components/chat/composer.tsx:183), decide (packages/ui-react/src/components/layout/page-boundary.tsx:114).
Who places tests in a loopback-only namespace: no repository code does this for bun run test. Hosted unit workers unshare user+mount only, without --net (scripts/unit-workers.ts:202), and drop capabilities in scripts/unit-worker-entry.sh:18. check:pr uses unshare --net only around its measurement probes and two native tests (localChecks, scripts/check-pr.ts:38-42). The loopback-only wrapper around whole-suite or real-browser runs is an operator/agent convention, recorded in evidence such as docs/process/evidence/test-wait-child-observations.json:7, and is not a documented or scripted workflow. Hosted CI is not affected.
Related existing requirement: externalSocket already refuses without a non-loopback local interface (packages/ui-server/tests/inbox-runtime.test.ts:234-236). Related precedent going the other way: #867/#887 and #1184/#1315 isolated an inherited offline state inside the fixture with a scoped navigator.onLine descriptor (withNavigatorOnline, packages/ui-kit/tests/visual/track-intake.visual.tsx:15-27).
Trap for whoever builds the harness: unshare --keep-caps without dropping capabilities (setpriv --inh-caps=-all --ambient-caps=-all --bounding-set=-all) makes bwrap refuse ("Unexpected capabilities but not setuid"). The test then fails early at :95 with Received value does not have a length property, which looks like a different bug.
The product behaviour is correct: a browser that reports offline must not upload. This is a proof-environment defect, not a share regression. Closed #2003 (ownership correlation) is not implicated.
Decision (maintainer, 2026-10-22)
Option A selected: provide one repository-owned Linux launcher for deliberate offline test commands. Create a fresh isolated user/network namespace containing loopback and a route-free dummy interface, so the real Chrome online signal supports local fixture traffic while external networking remains unavailable. Verify namespace separation, empty IPv4/IPv6 main route tables and dropped capabilities before running the command. Preserve all current browser/network guards and caller-owned host state.
Use the concrete setup and causal/guard controls below. Document this supported manual test-launch path and its rationale in the existing testing/CI guidance and decision record during implementation. No navigator.onLine override, product upload-policy change, host-level network change, generic test-runner modification or hosted-worker change is selected.
Scope
- Add one repository-owned bootstrap under
scripts/for deliberately running a command in a private offline network namespace. It must: create a fresh user+net namespace (plus mount when the caller needs one) and verify that it differs from the caller's; bringloup; createproof-dummy, setaddrgenmode noneon that interface only before link-up, assign198.18.0.1/32 noprefixroute, bring it up; record link/address/route inventory; refuse unless the IPv4 and IPv6 main tables have no routes; drop inherited, ambient and bounding capabilities and verify zeroCapEff/CapPrmbeforeexecof the given command. - Document it in
docs/process/testing.mdas the supported way to putbun run testor real-browser proof in an offline namespace, including the capability trap above. - Do not change the generic test runner, hosted unit workers or
check:pr's existing probe namespaces.
Out of scope
Changing the product's navigator.onLine gating; faking browser online state; larger timeouts, retries, removed cases or weakened ownership/egress assertions; host-level interface, sysctl, IPv6 or DNS changes; #2404's planner selection gap; historical attribution of every older same-stage failure (they stay recorded below as failed and are not relabelled green).
Acceptance criteria
- With the helper, all three share cases pass (63 assertions) with original assertions and deadlines unchanged, and the browser reports
navigator.onLine === true. - Failing-first receipt: the same invocation with the dummy-interface step removed fails all three cases at the
:95wait withnavigator.onLine === false. Restoring the step brings back 3/3. Both are recorded in the PR. - Guard mutations: removing
addrgenmode nonemakes the IPv6 empty-main-table guard refuse beforebunstarts. Removing the capability drop makes the helper refuse beforebunstarts, not inside bwrap. Each is restored afterwards. packages/ui-server/tests/inbox-runtime.test.tspasses under the helper, so the non-loopback requirement at:236is met.- The caller's network namespace and host interfaces are unchanged afterwards.
docs/process/testing.mdnames the helper and the capability trap;bun run lintand the decision-citation test pass.
Verification
export BRAIN_REQUIRE_CHROME=1 PUPPETEER_EXECUTABLE_PATH=<chrome>
# Red control (current behaviour)
unshare --user --map-current-user --keep-caps --net sh -c 'ip link set lo up; exec setpriv --inh-caps=-all --ambient-caps=-all --bounding-set=-all bun run test ./packages/ui-react/tests/share-draft-runtime.test.ts'
# Green through the new helper
<helper> bun run test ./packages/ui-react/tests/share-draft-runtime.test.ts ./packages/ui-server/tests/inbox-runtime.test.ts
bun run lint
bun run check:pr --base origin/mainContract impact
None. Test and tooling only; no CLI --json, MCP, schema or frontmatter change.
Related
- ui-sdk: give governed and strict workers a private network with a recorded, limitable egress proxy #1665 (p0, 0.44.0) encountered the loopback-only preflight failure recorded here. A 2026-10-21 live read of both
issues/1665/dependencies/blocked_byandissues/2401/dependencies/blockingreturns empty lists; ui-sdk: give governed and strict workers a private network with a recorded, limitable egress proxy #1665’s current body has no canonical blocker declaration for test(ui-react): attribute initial share confirmation stalls before ownership proof #2401. The earlier claim of a current native blocking edge is therefore unverified and is removed. This does not claim ui-sdk: give governed and strict workers a private network with a recorded, limitable egress proxy #1665’s remaining proof is complete. - ui-react: attribute workspace export resolution failure in share draft runtime setup #2421 (closed) removed its dependency on this issue on 2026-10-19 after
check:prpassed in the namespace plus dummy-interface environment (draft test(ui-react): build SDK before share draft browser bundling #2422). - ci: reconcile cross-workspace test consumers with selected proof inventories #2404: the hosted planner does not select this test for ui-server-only changes, which is separate from this issue.
History (condensed; full receipts in comments)
- 2026-10-18, ui-sdk: give governed and strict workers a private network with a recorded, limitable egress proxy #1665 preflight: tree
fe815d2d2268d948550f4d762bafc34e27cc32f7, candidate13601e76668b5801b4cc0aa5259a2df5a50ae42b, parent775eb17aab740fa759c2182f306c38fc17acbcbe, Bun 1.4.2, loopback-only private network. The three cases failed at line 91 and ownership assertions were never reached. These original failures remain failed. - 2026-10-18, ui-sdk: fresh sandbox import reads worker XDG constants before initialization #2148 candidate
98ffbbb98: 2,037 pass / one skip / nine fail in 169 files, including the same three. An isolated same-draft control with a route-free fictional interface passed. Candidatefec494fe97bb84d61e67ddd743be89403ef2a980on main78cfba4ff443ca2ce380502f7875a4a4283525ee, with loopback plus a route-free interface: 2,046 pass / 0 fail / 1 skip, 17,876 assertions. The ui-sdk: fresh sandbox import reads worker XDG constants before initialization #2148 edge was removed. - 2026-10-18, comments 6247275227 and 6248359694: three-case controls with the interface passed (3 tests / 63 assertions; 7 pass / 151 expects).
- 2026-10-19, coordinator claim
release044-2401-r1-20261019, head6bc63594c37d5df26347cca888d2fd9b9965b652, tree3d0a0b92794bb6361df079ba55bccb4d09b1b2a7, base90a95123cfceb169bd2ecf7c213544db742eaed7, Chromium 154.0.8037.97. Loopback only: 0/3 in 54.038 s;navigator.onLinefalse, review paused, no/api/share, no prompts admitted. The first interface setup was refused by the IPv6 guard: afe80::/64route was auto-generated, and the fix isaddrgenmode none. With the corrected prerequisite: 3 pass / 0 fail / 63 assertions in 20.690 s,navigator.onLinetrue,POST /api/sharereturned 201, promptsodysseus-share-1..4were admitted. - 2026-10-19, reproduction at
09eb1f78e(comment 6270981555): cause confirmed (table above);needs: reproremoved.
Triage 2026-10-19: rewrote as a current spec around the confirmed cause; cited the actual namespace launchers (none run this test loopback-only); added needs: decision on how offline proof supplies browser online state; moved areas from ui-server to ui-react+ci; kept p0 on the then-recorded #1665 blocking-edge rationale (the #2421 propagation had ended); proposed parent #736. The current dependency readback below supersedes that edge claim without changing the existing priority.
2026-10-21 triage at main b7f6360a86be68f7da8cefa93c316ecc0bf9bf42: repaired the active hosted-worker namespace pointer after its move to scripts/unit-workers.ts:202, and checked the actual native dependency APIs and #1665 body rather than inferring an edge from shared preflight history. No current #1665↔#2401 dependency is present in those sources. Preserve explicit p0 and milestone 0.44.0 pending the already raised priority/dependency reconciliation; no new security, data-loss or release-urgency evidence is inferred. The confirmed browser-online cause, unapproved A/B/C harness choice and needs: decision remain. #736 still has 100 direct children, so no alternate parent or hierarchy change is forced. No product change, new browser run or offline-helper approval was performed.
2026-10-22 decision processing: selected A and folded the isolated offline launcher into the approved implementation scope; existing causal and namespace/capability guard controls remain required. Remove needs: decision and establish agent-ready. Preserve existing priority/release attribution and historical failures; no launcher implementation, new browser proof or host-network modification was performed.
Activity
schlessera commentedon Oct 18, 2026
Additional #2148 receipt at head 98ffbbb, base b7a0885: required full feedback in a real 4 GiB fixture TMPDIR (UID/GID 1000, zero capabilities, private loopback-only network, original /tmp visible) completes 2,037 pass / one skip / nine fail across 169 files. Three failures match the initial-confirmation timeouts here at line 91, all original ownership assertions unreached. Other failure classes are separately attributed. After integrating main fcb7d5c, an isolated same-draft control with a route-free fictional non-loopback interface passes; a temporary observer around the original wait does not fire. That is a scoped changed-environment/ordering control, not a correction or complete qualification. No original failure is retried unchanged for green. This is now a canonical #2148 proof blocker; priority p2 inherits its blocked issue under the blocker-priority rule. The full combined cause remains unestablished.
schlessera commentedon Oct 18, 2026
2026-10-18 — second reproduction pass at
775eb17aab740fa759c2182f306c38fc17acbcbe.Different approach: Record actual root/store state, API response status/body, WS sent/received frames, backend prompts and native turn_scope immediately after the unchanged eight-second initial confirmation wait in every original ownership scenario, including successful waits. Unlike prior failure-only observation or an isolated same-draft control, all three scenarios execute with valid-capacity TMPDIR and a route-free local fixture interface.
Observed: All three original ownership scenarios pass, 3 tests / 63 assertions. Every unchanged initial eight-second confirmation wait completes, and the downstream ownership assertions execute. Store/HTTP/WS/backend-admission/scope observations are retained at each confirmation. Outer invocation exit
0, elapsed 6.332 s; outer watchdog fired: false.Command (working directory: repository root; any diagnostic source changes are described above):
unshare --user --map-current-user --keep-caps --net sh /tmp/brain-repro-audit-2/offline.sh bun run test packages/ui-react/tests/share-draft-runtime.test.tsExplicit environment:
BRAIN_REQUIRE_CHROME=1,PUPPETEER_EXECUTABLE_PATH=$PLAYWRIGHT_CHROME,DEBUG=pw:browser,TMPDIR=/dev/shm/odysseus-repro2.This is the complete three-scenario file with passive boundary observations, valid actual TMPDIR and the required route-free fixture interface; it is not the old combined 169-file process or an explanation of its historical timeouts. The previously missing failure-time state is still unavailable because no stall recurs.
This is focused local evidence, not historical-run equivalence, a repair or full candidate proof. Existing assertions, deadlines and offline guards were retained. Temporary source instrumentation was restored byte for byte. Keep
needs: repro; no readiness or closure change. Full log, command/exit receipt and diagnostic patch are retained in task-owned scratch.schlessera commentedon Oct 18, 2026
2026-10-18 — third reproduction pass at
775eb17aab740fa759c2182f306c38fc17acbcbe.Different approach: Execute the complete three direct-share scenarios together with all four settled queued late-history guards in one Bun invocation, recording browser NetLog from process launch. This adds an actual continuity-file interaction and global native admission evidence beyond earlier isolated HTTP/WS observers, while retaining original initial-confirmation assertions.
Observed: 7 pass; 0 fail; 151 expect() calls Outer invocation exit
0, elapsed 92.922 s; outer watchdog fired: false.Command (working directory: repository root; any diagnostic source changes are described above):
Explicit environment:
BRAIN_REQUIRE_CHROME=1,PUPPETEER_EXECUTABLE_PATH=$PLAYWRIGHT_CHROME,DEBUG=pw:browser,TMPDIR=/tmp/brain-repro-audit-3/native-state.Original historical failure and broad qualification remain unresolved.
This is focused local evidence, not historical-run equivalence, a repair or full candidate proof. Existing assertions, deadlines and offline guards were retained. Temporary source instrumentation was restored byte for byte. Keep
needs: repro; no readiness or closure change. Full log, command/exit receipt and diagnostic patch are retained in task-owned scratch.schlessera commentedon Oct 19, 2026
2026-10-19 coordinated qualification follow-up
Coordinator claim
release044-2401-r1-20261019owns the next bounded diagnosis. The open recorder PR #2399 references this proof gap; it does not implement a share-confirmation repair and remains under the same coordinator's custody.The #2421 worker reproduced a separate cold browser-bundle setup failure and restored bundling using the supported SDK build. On candidate
6bc63594c37d5df26347cca888d2fd9b9965b652, based on90a95123cfceb169bd2ecf7c213544db742eaed7, all three original share cases then reached the unchanged post-confirmation wait and failed before ownership assertions. This is a new same-stage observation, not proof of common cause or historical runtime equivalence. Original #2421 distribution state remains unknown. Setup-only bundling proof does not establish passing share behavior.The current #2421 qualification now waits on this confirmation diagnosis. Its P0 priority therefore propagates here; no timeout or ownership assertion is relaxed. Next evidence must bind the actual failing wait to page/store state, HTTP/WS and backend admission, native scope/refusal and supported fixture/network/storage inputs. No unchanged retry, speculative store/routing fix or broad feedback replay is authorized.
schlessera commentedon Oct 19, 2026
Repro attempt, 2026-10-19, at main
09eb1f78e. Local Linux (WSL2): 32 CPUs, 47 GiB, Bun 1.4.2, 121 mountinfo entries. Chrome for Testing 153.0.8010.12 viaPUPPETEER_EXECUTABLE_PATH. CPU load came fromtaskset -c <cpus> yes > /dev/nullbusy loops.Reproduced; the cause is confirmed. Runs of
./packages/ui-react/tests/share-draft-runtime.test.ts:draft?.messages.length === 2wait (about 9.4 s).proof-dummy(198.18.0.1/32,addrgenmode none,noprefixroute)Both namespace arms ran as
unshare --user --map-current-user --keep-caps --net sh -c "ip link set lo up; [dummy setup;] exec setpriv --inh-caps=-all --ambient-caps=-all --bounding-set=-all bun run test …".One trap for whoever writes the fixture:
--keep-capswithoutsetprivmakes bwrap refuse with "Unexpected capabilities but not setuid". The test then fails early at line 95 withReceived value does not have a length property, which looks like a different bug. Capabilities have to be dropped beforebun.Direct probe, Chrome for Testing 153,
about:blank, then readingnavigator.onLine:navigator.onLinetruefalsetrueProduct code also reads
navigator.onLineoutside share intake:activity-report.tsx:63composer.tsx:183page-boundary.tsx:114use-share-intake.ts:69and:324So any offline proof harness that has only loopback skews every surface that consults it, not only share intake.
schlessera commentedon Oct 19, 2026
Triage 2026-10-19 at
e16b8407f.What changed: the body is now a spec built on the confirmed cause. A loopback-only namespace makes Chrome report
navigator.onLine === false, so the share gate atuse-share-intake.ts:69/173-181correctly pauses. I checked who puts tests in that namespace: no repo script does. Hosted unit workers don't use--net(scripts/unit-workers.ts:74), andcheck:pronly wraps its measurement probes (scripts/check-pr.ts:38-42). It is an operator convention, so hosted CI is unaffected. Areas: removedui-server(no server involvement) and addedci. I propose attaching to epic #736, which already lists "browser connectivity-state inheritance".Priority: stays p0. The #2421 propagation ended when #2421 closed and dropped its edge, and the 0.44.0 milestone does not name this work. However, #1665 (p0, 0.44.0) is still natively blocked by this issue. Maintainer: please confirm whether that edge is still needed. #2421's
check:prpassed in the namespace plus dummy-interface environment. If #1665 accepts that environment, the edge can be removed and this falls to p1 on its milestone.Open decision (
needs: decision): how should local offline proof supply browser online state? The proposal in the body ("guarded bootstrap … not implemented") was never ruled on, and it conflicts with delivered precedent.scripts/. It adds theproof-dummy198.18.0.1/32 interface withaddrgenmode noneandnoprefixroute, guards both main route tables, drops capabilities and checks they are zero, and is documented indocs/process/testing.md. Consequences: the real browser signal stays intact; it also meets the existing non-loopback requirement ininbox-runtime.test.ts:236; there is one more script to maintain; and it only helps runs that go through it.share-draft-runtime.test.ts, using a scoped, restorednavigator.onLinedescriptor as ci: track-intake browser fixture inherits ambient offline state #867/test(ui-kit): isolate track picker connectivity #887 did (track-intake.visual.tsx:15-27). Consequences: the test no longer depends on topology, but it masks the real browser signal. That contradicts the coordinator's "do not overridenavigator.onLine" constraint, fixes one file only, and leaves the other readers (activity-report.tsx:63,composer.tsx:183,page-boundary.tsx:114) dependent on topology.share-draft-runtime.test.ts:82-85). Consequences: no code change, but OS-level offline enforcement for browser children is lost.navigator.onLinegating is rejected: a browser that reports offline really is offline.)Recommendation (not approval): A, plus a line in
docs/process/testing.md. The repo's evidence workflow already depends on explicit offline namespaces, one existing test already needs a non-loopback interface, and A keeps everynavigator.onLinereader honest. If B is ruled instead, the scope and acceptance criteria need rewriting around a fixture descriptor with a restoration sentinel.Related: #2404 (planner misses this test on ui-server changes) is separate and does not block this.
schlessera commentedon Oct 22, 2026
Decision (maintainer, 2026-10-22): Option A.
Add the repository-owned Linux offline-test launcher specified in the body: a fresh user/network namespace with loopback plus a route-free dummy interface, verified route tables and capability dropping. Preserve the real browser online signal, external-network isolation, host interfaces and original test assertions/deadlines. Document the supported launch path and its rationale in existing testing/CI guidance. No browser-signal override or product/host-network policy change is approved.
The implementation brief and existing failing-first/guard controls are complete. Remove
needs: decisionand addagent-ready; preserve existing priority and release attribution. No implementation or new runtime qualification is claimed.schlessera commentedon Oct 22, 2026
Claim
bk044-2401-offline-20261022-r1: implementing the 2026-10-22 Option A ruling on branchtest/2401-offline-test-launcher-044. Keeping the existing assignment. Scope ends at a draft PR with passing local preflight and current-head draft gates; coordinator owns confirmation and merge.schlessera commentedon Oct 22, 2026
Implementation handoff for claim
bk044-2401-offline-20261022-r1: draft PR #2546 ontest/2401-offline-test-launcher-044, headfd10c6fc362083378dca4129103dde15f8fc8fde, starting/tested base7b8a755ea329bd48a11b790e916c417b6f608eee.Option A is implemented with the reusable launcher and optional mount isolation. All three real-browser share cases pass with 63 assertions and real online
true; removing only dummy setup yields 0/3 at the original line-95 wait with onlinefalse. Inbox passes 18/18 with 151 assertions. Omitted addrgenmode and capability-drop controls both refuse before Bun starts. Named IPv6/driver guard-deletion tests fail for expected exit 1 / received 0 and pass after restoration. Explicit caller-owned 4 GiB tmpfs composition passes. Final launcher suite: 9/9, 91 assertions.Final-head local preflight passes (970 fast tests plus 9 affected tests), as do typecheck, lint and citation checks. Current-head
contract,changesetanddraft gatespass; heavier jobs are excluded by draft cadence. PR and issue milestones both read back0.44.0. Receipts and the unchanged original failure attribution are in the PR.One setup edge is explicit: the initial cold-driver caller-state control detected a new host
dummy0; that interface remains present. The final launcher refuses uninitialized dummy-driver autoload, and subsequent caller-state controls pass. Hosted driver provisioning is outside this PR. The tmpfs recipe is ready for #2394 to reuse.Stopping at the coordinator's draft-only boundary. No ready transition, integration reservation/confirmation or merge was requested. The implementation claim is handed back to the coordinator; the existing assignment is retained as instructed in the worker brief. No follow-up issue was filed.