Run the Harness end-to-end integration target on every PR
Problem
effects::effect_request_and_result_bodies_never_enter_stream (Harness integration target main) failed on main from 7a4414bc6 (#513) until #851 landed the root-cause fix. That fix hashes length-framed binary fields for the effect root, replacing decimal JSON digest serialization whose byte work depended on the digest digits. It passes on current main 2ecd4f21a (bb1, 1 passed, 0.07 s).
Why CI missed it:
- PR CI (
SDK Static) compiles no Harness code by design (#374). It checks the lockfile and lane artifacts, and the aarch64 job type-checksacyclic-fs. - The only workflow that runs Harness tests is release qualification (
nextest --workspace), which is reachable only throughrelease-acyclic.yml. Its last run was 2026-09-25, three weeks before #513 merged on 2026-10-16. - Lanes run focused tests for the code they change. Nothing ran the end-to-end
maintarget, which aggregates 21 cross-subsystem scenarios.
Fix
- A new
SDK Harness integrationjob in theSDK Staticworkflow runs the wholemaintarget on every PR and main push:cargo test --locked -p acyclic-harness --features filesystem-local,test-support --test main. - It runs on the free GitHub-hosted
ubuntu-24.04runner (the repo is public), so there's no paid CI. TheSDK Staticjob itself is unchanged, with the same 1-minute, no-compile assertions. - The job isn't a required check. Main's only required check is
readme-guard. scripts/tiny-ci.test.mjspins the job's exact command, its free hosted runner, and the absence of a job condition orcontinue-on-error.scripts/check-workflow-runners.mjsrecords the new hosted runner.
Class closed: a commit breaking an end-to-end Harness scenario can no longer merge without a red check on its PR. The policy test also stops the gate from being silently narrowed (a test filter), softened (continue-on-error), limited to main, or moved to a paid runner.
Known red on landing
This job will be red on this PR and on main until a separate fix lands. native_media_boundary::live_original_options_and_actual_native_parts_reach_adapter_without_conversion in the same target fails on current main: Failed { message: "unauthorized: task has no owner-bound content writer" } at native_media_boundary_cases/live.rs:688 (bb1: 43 passed, 1 failed, 38.9 s). It's the same class of escape. First bad commit is 108fa5e52 (#711, "Publish task compaction summaries in authorized private files"); its parent bfbaaf2b3 (#773) passes (bb1 probes: 0 passed / 1 failed vs 1 passed). The fix will be its own PR.
Verification
- This PR's own
SDK Harness integrationrun (cold, no cache) took 12m10s: an 11m27s build, then 43 passed and 1 failed (the #711 native-media test) in 28.3 s. So the gate compiles on a bare free runner and fails on the real regression. - On bb1, on main 2ecd4f21a, canonical native features:
effects::passed (1 test, 0.07 s). The wholemaintarget ran 43 passed and 1 failed (the native-media test above), with a 38.9 s suite. - On bb1:
node --test scripts/tiny-ci.test.mjs scripts/check-workflows.test.mjs scripts/static-ci.test.mjs scripts/static-policy.test.mjspassed 23 of 23 in 1.4 s, andnode scripts/check-workflow-runners.mjspassed. - Negative controls in the new policy test: a test-name filter,
continue-on-error, a main-onlyif, and a Blacksmith runner each makeverifythrow. - No Rust changes, so no Windows check applies.
Review fixes (eeea93e7e)
- Synchronized output and the cursor are journaled.
RestoreGuardnow journals two undos before a frame's first byte: closing the 2026 update and showing a hidden cursor. When a write fails, the update is closed at once, bounded by the device deadline. A timeout, panic or exit restores it through the journal.lifecycle::restorenow recovers a poisoned lock; this is safe because the journal is always written before the bytes it covers.- Tests in
tests/terminal_frame_faults.rsinject faults at every byte of a three-frame session: a transient timeout, a persistent failure until teardown, and a writer panic. An alacritty oracle checks that no update is left open and the cursor is visible afterwards. A transient error must also leave the update closed immediately. - Negative control: the unjournaled device path must fail these checks.
lifecycle::poison_testscovers restoration after a panic while the lock is held.
- Tests in
- Teardown shows the cursor.
FrameBackend's destructor presents ratatui's queued cursor restoration. If the device is already closed, the journal has shown the cursor. - No spinning on macOS. Darwin now waits for writability with level-triggered kqueue, which has no descriptor limit. It falls back to select only when kqueue rejects the
/dev/ttyalias with EINVAL; after #869, which opens the real device, that path is unused. - Traced native write paths.
- The write loop is now
write_untilover aNonblockingWritetrait. A scripted trace for EAGAIN/EINTR must equal the expected one exactly. Negative control: the removed 1 ms sleep loop fails it. - A real PTY test writes a 256 KiB frame against a late reader, with the descriptor at its normal number and above 1024. Every readiness wait must be followed by a write that makes progress. Negative control: the removed
yield_nowwait fails it. - The Windows console now gets bounded calls of
CONSOLE_CHUNK= 256 KiB, each under its own 100 ms deadline. Measured on the local console, per call: 16 KiB 1 ms, 64 KiB 3 ms, 256 KiB 10-17 ms, 1 MiB 38 ms, 4 MiB 110 ms (timed out). Total time for 4 MiB by call size: 4 KiB 289 ms, 64 KiB 114 ms, 256 KiB 99 ms, 1 MiB 98 ms. - A plan test checks frames up to 16 MiB: ceil(len/chunk) calls, each within the bound, inside one update. Negative control: a single unbounded call fails it. A failed part is closed by the abort.
- On the real console, 1, 4 and 16 MiB frames present without error in 26, 98 and 380 ms.
- Structurally, the device decides synchronized output from the probed modes, so
FrameTerminal::newno longer takes a flag.
- The write loop is now
Real terminal at this head (release, every row repainted; old path → frame path). Brackets aren't counted because these harnesses don't negotiate:
- Linux bb1 under load (14 concurrent builds): 120x40 5104 → 1 writes, 26627 → 622 us. 240x70 9206 → 1 writes, 40851 → 1254 us.
- macOS ivar: 120x40 5104 → 1 writes, 2631 → 185 us. 240x70 9206 → 1 writes, 4702 → 374 us. Before kqueue, 240x70 was 906 us.
- Windows local: 120x40 4699 → 1 writes, 306403 → 407 us. 240x70 8499 → 1 writes, 558224 → 706 us.
Pre-existing failure, not caused by this PR: term::session::tests::delayed_old_state_drop_preserves_reopened_pty fails on macOS on main too, with flags 0x10000 added. Reproduced with main's files on ivar.
Decisions
- The job covers the
maintarget only, not--tests(37 targets, about 37 test-binary links).mainis the aggregated end-to-end target where this escape happened. - Features are
filesystem-local,test-support, the smallest set that compiles everymainmodule (local_conversation_forkneedsfilesystem-local). It avoids grpc, search-http and tui system dependencies on a bare runner. - There's no build cache. GitHub's 10 GB cache is shared with the qualification caches, and a cold build keeps the job self-contained. The job needs its first measured duration before caching is considered.
🤖 Generated with Claude Code