CI: run every tests/*.rs integration binary on ubuntu, offline - #239
Merged
Conversation
No workflow ran the integration binaries under crates/*/tests. The test matrix selects --lib --bins, and clippy --all-targets only compiles them, so the guards CLAUDE.md treats as load-bearing (the privacy master-switch binaries, the keyless-daemon binaries, privacy_ar15_is_retired, every_test_binary_is_sandboxed) ran only when someone remembered to. From eb594de until PR #229 the AR-15 closure check read the wrong function and would have stayed green with the gate deleted. A new step in the test job, ubuntu only, runs every integration target in the workspace except an exclusion table that gives each entry's reason: - sandbox: the step before it already runs it, on every OS. - ui_example_apps: bundles with `npx --yes esbuild` when no local esbuild exists. Offline it passes vacuously: npx waits out the bundler's 60 s timeout and the type-stripper fallback runs instead. apps-smoke.yml runs it after `npm ci`. An exclusion list rather than an allowlist, so a new binary runs the day it lands; a stale table entry and a duplicate target name both fail the step. It never passes --ignored, so the live tests stay out while the rest of their binaries run. It selects with --workspace --test, not -p, because -p changes feature unification and rebuilds dependencies. The tests run in a loopback-only network namespace (unshare --net, then setpriv back to the runner user), so a test that dials out fails there instead of passing while the third party is up. Measured on hosted runners, one binary at a time (runs 34628149105, 34629544896): on ubuntu, offline, all 119 binaries pass (687 passed, 0 failed, 49 ignored); the build adds 135 s and the selected tests about 135 s. macOS passed except one log-capture test that failed once in about 70 runs. windows-latest fails 31 tests in 9 binaries that had never run there, hence ubuntu only. The step needs CARGO_PROFILE_TEST_DEBUG=0 on ubuntu too. With debug=2 the integration build took the runner from 70 GB free to 1.4 GB and died with `No space left on device`: on Linux every test binary embeds the DWARF of everything it links. Setting it only for the new step would rebuild every dependency, since cargo test builds dependencies with the test profile (measured). So it moves to the job-level step windows and macOS already use. The first ubuntu runs build cold until main saves a new cache (lib + bins 3.7 -> 7.5 min).
The job filtered by name to the Chromium smoke harness, so the binary's other five tests, including every_example_bundles_against_the_real_sdk, ran in no workflow at all. They bundle with esbuild, which is hermetic only where `npm ci` has put one in ui/desktop/node_modules. This job is the one place that is true, and rust.yml's offline integration step excludes the binary for that reason.
rust.yml runs the integration binaries on ubuntu, offline, bar an exclusion table. A future contributor should know a new binary runs by default, and what to do when one cannot run offline.
This was referenced Sep 11, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
No workflow ran the integration binaries under
crates/*/tests/*.rs.rust.yml'stestjob selects--lib --bins, andclippy --all-targetsonly compiles them. So the guards CLAUDE.md treats as load-bearing (the privacy master-switch binaries,approval_no_user_keyandknowledge_tier_no_user_key,privacy_ar15_is_retired,every_test_binary_is_sandboxed) ran only when someone remembered to run them. From eb594de until #229,the_documented_closure_is_the_one_the_code_performsscanned the wrong function and would have stayed green with the AR-15 gate deleted.This PR adds a step to the
testjob, ubuntu only, that runs every integration target in the workspace except an exclusion table. Each table entry gives its reason inline:sandboxui_example_appsnpx --yes esbuildwhen no local esbuild exists, and this job installs none. Offline it passes vacuously: npx waits out the bundler's 60 s timeout, then every example falls back to the type-stripper and esbuild never runs.apps-smoke.ymlnow runs the whole binary afternpm ci.How the step works:
new_chat_no_user_keyfrom fix(serve): a new chat starts on the host's configured private model (QA F1) #229 is picked up automatically when fix(serve): a new chat starts on the host's configured private model (QA F1) #229 merges, with no edit here. (Naming it explicitly today would fail:no test target named new_chat_no_user_key.) A table entry for a target that no longer exists fails the step, and so does a target name shared by two packages. I planted both failures against the extracted step to check they fire.--ignored. The live tests (real providers, vendor CLIs, llama.cpp downloads, GitHub fetches) are#[ignore]d inside binaries whose other tests do run, so those binaries stay in and their live tests stay out.sudo unshare --net, which leaves only loopback, thensetprivback to the runner user (as root, every "permission denied" assertion would pass for the wrong reason). A test that dials out fails immediately with a connection error. Before trusting it, I ran a positive control on the hosted runner: uid is non-zero inside,https://example.comis refused, and a127.0.0.1server answers.--workspace --test <name>, not-p <pkg>. A-pselection changes cargo's feature unification and rebuilt dependencies when I measured it.--workspacereuses what the lib+bins step built (0.6 s, nothing rebuilt).BIOROUTER_DISABLE_KEYRING=true), like the existing step, andCARGO_NET_OFFLINE=true.A required side change: ubuntu test debuginfo
With ubuntu's default debug=2, building the integration binaries ran the runner out of disk: 70 GB free fell to 1.4 GB, then
rustc-LLVM ERROR: IO failure on output stream: No space left on device(run 34628149105). On Linux, every test executable embeds the DWARF of everything it links. I measured thatcargo testbuilds dependencies with the test profile (flipping onlyCARGO_PROFILE_TEST_DEBUGset cargo recompiling the whole dependency graph: 515 crates in the first 25 s, when I stopped it). So the variable has to be job-wide; set on the new step alone, it would rebuild every dependency. It now applies to ubuntu as well as windows/macOS. With it, the integration executables total 14.5 GiB and the runner keeps 65 GB free.⚠ Cost: ubuntu's rust-cache key changes, so ubuntu builds cold until
mainsaves a new cache. That includes this PR's own runs. Measured: the lib+bins build goes from 3.7 to 7.5 min. The workflow comment that deliberately kept ubuntu out ("to fix a problem ubuntu does not have") is rewritten, because ubuntu now has the problem.Measurements
Each binary run on its own, with a per-binary timeout, keyring off. Scratch runs: 34628149105 (all OSes, then-current debuginfo settings) and 34629544896 (ubuntu, debuginfo 0, offline).
Flakiness: I ran the step's exact command locally 6 times (3 at default threads, 3 at
RUST_TEST_THREADS=32, the stressor that exposed this repo's shared-fake races before). All 117 binaries passed every time, 126–141 s per pass.On time: the step should add about 5 min to
test (ubuntu-latest)warm (135 s build plus 137 s of tests plus process startup), against today's 12–19 min and a 40-min cap. macOS (~20 min) sets the workflow's wall clock today.Why ubuntu only
session_store_dispatch_boundary(2): "a script read every conversation on this machine, with no inspector anywhere". The Feature request: privacy tiers — keep private conversations away from externally hosted models #56 session-store guard forexecute_codedoes not appear to hold on Windows. Worth a look on its own.code_execution_integration(6): a Windows path is spliced into JS unescaped (\bbecame a backspace).hooks_agent_loop_tests(9),hooks_integration_tests(7),global_memory_consent_agent_loop(2),loop_safety_observability_tests(1):Tool 'developer__shell' not found, the same shape as the already-known open Windows bridge failures.parallel_tool_batch_concurrency_cap,_serialization_lever,_stress(4): the tool-concurrency cap did not bound the batch (18 concurrent against a ceiling of 8, and 7 withBIOROUTER_TOOL_MAX_CONCURRENT=1).output_recovery_agent_loop::private_provider_error_logging_retry_keeps_details_out_of_diagnostics, which failed once on the CI runner. It passed 7 times locally, 60 more times when hammered (40 at 8/32 threads, 20 throttled withtaskpolicy -b), and on ubuntu and Windows. My unconfirmed guess is a log-capture race, since it scopes its tracing subscriber to one future with.with_subscriber(). It could still flake on ubuntu at a low rate.unshareis Linux-only. Widening to another OS needs its failures fixed, and its own way to stay offline (Seatbelt works on macOS; I used it for the local run).Also in this PR
apps-smoke.ymlruns the wholeui_example_appsbinary (--include-ignored) instead of filtering by name to the Chromium test. The binary's other five tests ran in no workflow before.rust.ymlcomments are fixed. One claimed the sandbox target was the only thing outside--lib/--bins. The other claimedtests/needed cassettes and credentials; in factmcp_integration_testreplays its cassettes offline, and credentialed tests are#[ignore]d.Not in this PR
--libstep'sbuild_app_bundles_with_available_toolchaingoes through the samenpx --yes esbuildfallback on runners, so it probably already downloads esbuild from npm on every run. I haven't verified this in a CI log.find_esbuild()walks six parent directories and finds the main checkout'sui/desktop/node_modules/.bin/esbuildfrom inside a.claude/worktrees/*worktree, soui_example_appspasses locally for a reason CI never has.Test plan
sudostubbed: selects 117 binaries (including all 10privacy_*binaries, both*_no_user_keyandevery_test_binary_is_sandboxed); a planted stale entry exits 1; a planted duplicate name exits 1test (ubuntu-latest)runs the new step green (run 34631673813). From the job log: both exclusions printed,running 117 integration binaries, build 2m08s, 117test result: ok, 0 failed, step total 4m33s. The whole ubuntu job took 22 min on this cold-cache first run (budget 40; lib+bins went from 9.1 to 11.7 min cold).reference-apps(apps-smoke) runs the wholeui_example_appsbinary greentest (windows-latest): the first attempt failed in the lib step onroutes::agent::knowledge_selection_tests::applying_a_workflow_hides_a_base_that_lands_mid_call(gitNotFoundunder the Windows temp dir). That's a known Windows flake: it hit renderer-only Chat history on the chat measure (760px) #175 the same way, and nothing in this diff runs on Windows beyond the unchanged debuginfo setting. The rerun passed.🤖 Generated with Claude Code