Skip to content

test(ui-react): attribute initial share confirmation stalls before ownership proof #2401

Closed

Description

@schlessera

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; bring lo up; create proof-dummy, set addrgenmode none on that interface only before link-up, assign 198.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 zero CapEff/CapPrm before exec of the given command.
  • Document it in docs/process/testing.md as the supported way to put bun run test or 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 :95 wait with navigator.onLine === false. Restoring the step brings back 3/3. Both are recorded in the PR.
  • Guard mutations: removing addrgenmode none makes the IPv6 empty-main-table guard refuse before bun starts. Removing the capability drop makes the helper refuse before bun starts, not inside bwrap. Each is restored afterwards.
  • packages/ui-server/tests/inbox-runtime.test.ts passes under the helper, so the non-loopback requirement at :236 is met.
  • The caller's network namespace and host interfaces are unchanged afterwards.
  • docs/process/testing.md names the helper and the capability trap; bun run lint and 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/main

Contract impact

None. Test and tooling only; no CLI --json, MCP, schema or frontmatter change.

Related

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, candidate 13601e76668b5801b4cc0aa5259a2df5a50ae42b, parent 775eb17aab740fa759c2182f306c38fc17acbcbe, 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. Candidate fec494fe97bb84d61e67ddd743be89403ef2a980 on main 78cfba4ff443ca2ce380502f7875a4a4283525ee, 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, head 6bc63594c37d5df26347cca888d2fd9b9965b652, tree 3d0a0b92794bb6361df079ba55bccb4d09b1b2a7, base 90a95123cfceb169bd2ecf7c213544db742eaed7, Chromium 154.0.8037.97. Loopback only: 0/3 in 54.038 s; navigator.onLine false, review paused, no /api/share, no prompts admitted. The first interface setup was refused by the IPv6 guard: a fe80::/64 route was auto-generated, and the fix is addrgenmode none. With the corrected prerequisite: 3 pass / 0 fail / 63 assertions in 20.690 s, navigator.onLine true, POST /api/share returned 201, prompts odysseus-share-1..4 were admitted.
  • 2026-10-19, reproduction at 09eb1f78e (comment 6270981555): cause confirmed (table above); needs: repro removed.

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

  1. added this to the 0.44.0 milestone on Oct 18, 2026
  2. added
    type: testCoverage, harness or proof work with no behaviour change.
    area: ui-serverHono app factory: turn coordinator, auth, session catalog, routes, cron.
    area: ui-reactReact chat/files/voice components, stores, WebSocket transport.
    needs: reproBlocked on a reproduction.
    priority: p2Wanted, not scheduled. Pulled in when a milestone has room.
    on Oct 18, 2026
  3. schlessera commented on Oct 18, 2026

    @schlessera
    OwnerAuthor

    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.

  4. schlessera commented on Oct 18, 2026

    @schlessera
    OwnerAuthor

    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.ts

    Explicit 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.

  5. schlessera commented on Oct 18, 2026

    @schlessera
    OwnerAuthor

    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):

    unshare --user --map-current-user --keep-caps --mount --net sh /tmp/brain-repro-audit-3/offline-mounted.sh bun run test packages/ui-react/tests/connection-continuity-runtime.test.ts packages/ui-react/tests/share-draft-runtime.test.ts -t 'direct share announcement|late history read retains the settled queued reply'

    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.

  6. added
    priority: p0Drop everything: a security hole, data loss, or a broken release.
    and removed
    priority: p2Wanted, not scheduled. Pulled in when a milestone has room.
    on Oct 19, 2026
  7. self-assigned this
    on Oct 19, 2026
  8. schlessera commented on Oct 19, 2026

    @schlessera
    OwnerAuthor

    2026-10-19 coordinated qualification follow-up

    Coordinator claim release044-2401-r1-20261019 owns 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 on 90a95123cfceb169bd2ecf7c213544db742eaed7, 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.

  9. schlessera commented on Oct 19, 2026

    @schlessera
    OwnerAuthor

    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 via PUPPETEER_EXECUTABLE_PATH. CPU load came from taskset -c <cpus> yes > /dev/null busy loops.

    Reproduced; the cause is confirmed. Runs of ./packages/ui-react/tests/share-draft-runtime.test.ts:

    Topology Result
    Host network 3 pass / 0 fail
    Namespace, loopback only 0 pass / 3 fail. All three cases time out at the line 95 draft?.messages.length === 2 wait (about 9.4 s).
    Namespace, loopback plus proof-dummy (198.18.0.1/32, addrgenmode none, noprefixroute) 3 pass / 0 fail

    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-caps without setpriv makes bwrap refuse with "Unexpected capabilities but not setuid". The test then fails early at line 95 with Received value does not have a length property, which looks like a different bug. Capabilities have to be dropped before bun.

    Direct probe, Chrome for Testing 153, about:blank, then reading navigator.onLine:

    Topology navigator.onLine
    Host true
    Loopback only false
    Loopback plus dummy true

    Product code also reads navigator.onLine outside share intake:

    • activity-report.tsx:63
    • composer.tsx:183
    • page-boundary.tsx:114
    • use-share-intake.ts:69 and :324

    So any offline proof harness that has only loopback skews every surface that consults it, not only share intake.

  10. added
    needs: decisionBlocked on a maintainer or designer ruling, not on code.
    area: ciCI workflows, the gates they run, release scripts, packaging.
    and removed
    needs: reproBlocked on a reproduction.
    area: ui-serverHono app factory: turn coordinator, auth, session catalog, routes, cron.
    on Oct 19, 2026
  11. schlessera commented on Oct 19, 2026

    @schlessera
    OwnerAuthor

    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 at use-share-intake.ts:69/173-181 correctly 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), and check:pr only wraps its measurement probes (scripts/check-pr.ts:38-42). It is an operator convention, so hosted CI is unaffected. Areas: removed ui-server (no server involvement) and added ci. 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:pr passed 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.

    • A. Repo-owned offline-namespace helper under scripts/. It adds the proof-dummy 198.18.0.1/32 interface with addrgenmode none and noprefixroute, guards both main route tables, drops capabilities and checks they are zero, and is documented in docs/process/testing.md. Consequences: the real browser signal stays intact; it also meets the existing non-loopback requirement in inbox-runtime.test.ts:236; there is one more script to maintain; and it only helps runs that go through it.
    • B. Fixture-owned online state in share-draft-runtime.test.ts, using a scoped, restored navigator.onLine descriptor 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 override navigator.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.
    • C. Declare a loopback-only namespace unsupported for real-browser proof (docs only). Run those tests on the host network, as hosted workers do, and rely on the test's own egress abort (share-draft-runtime.test.ts:82-85). Consequences: no code change, but OS-level offline enforcement for browser children is lost.
    • (Changing the product's navigator.onLine gating 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 every navigator.onLine reader 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.

  12. added
    agent-readyContext and acceptance criteria are complete enough for an agent to take this unattended.
    and removed
    needs: decisionBlocked on a maintainer or designer ruling, not on code.
    on Oct 22, 2026
  13. schlessera commented on Oct 22, 2026

    @schlessera
    OwnerAuthor

    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: decision and add agent-ready; preserve existing priority and release attribution. No implementation or new runtime qualification is claimed.

  14. schlessera commented on Oct 22, 2026

    @schlessera
    OwnerAuthor

    Claim bk044-2401-offline-20261022-r1: implementing the 2026-10-22 Option A ruling on branch test/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.

  15. schlessera commented on Oct 22, 2026

    @schlessera
    OwnerAuthor

    Implementation handoff for claim bk044-2401-offline-20261022-r1: draft PR #2546 on test/2401-offline-test-launcher-044, head fd10c6fc362083378dca4129103dde15f8fc8fde, starting/tested base 7b8a755ea329bd48a11b790e916c417b6f608eee.

    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 online false. 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, changeset and draft gates pass; heavier jobs are excluded by draft cadence. PR and issue milestones both read back 0.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

agent-readyContext and acceptance criteria are complete enough for an agent to take this unattended.area: ciCI workflows, the gates they run, release scripts, packaging.area: ui-reactReact chat/files/voice components, stores, WebSocket transport.priority: p0Drop everything: a security hole, data loss, or a broken release.type: testCoverage, harness or proof work with no behaviour change.

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions