Conversation
Adds tests/e2e/: a Vitest suite that builds the real `odu` binary with `nix build`, materializes a throwaway git fixture, and drives `odu run` on a localhost lane — asserting on the `--progress json` NDJSON stream and the process exit code. Covers the seams the in-process loopback suite (src/odu.test.ts) stubs: just-DAG ingest → scheduling → local lane spawn → NDJSON projection → exit code. Each fixture is a flake that re-exports `odu-runner` from the checkout under test, since a local `odu run` resolves the lane runner via `nix eval <repo>#packages.<sys>.odu-runner.drvPath` even for localhost. Wired in as a new `e2e` CI step in ci/mod.just (and a top-level `just e2e`). See tests/e2e/README.md for the design tradeoffs and follow-ups.
Drop the module-level fixtures[] accumulator + afterAll sweep; register each fixture's cleanup with the running test so creation and teardown sit together.
cleanup() no longer swallows rm errors silently — a leaked fixture dir is now logged so it's visible in CI instead of accumulating unnoticed. Also records (No-op) why the harness ProgressEvent intentionally mirrors rather than imports src/coordinator/display.ts: importing the type would make the test white-box and hide the wire-format regressions the black-box suite exists to catch. Cross-validation (hickey+lowy) confirmed the duplication is load-bearing and surface.ts would be the wrong home for it.
- terminalStatuses builds the Map directly from events.map (drops the mutable accumulator loop). - Cache the substituted flake.nix once at module load instead of re-reading + replacing on every makeFixture call. - Export the BIG maxBuffer constant so call sites share one definition. - no-silent-error-swallowing (fact-check): the NDJSON parse catch now logs the offending line + error to stderr instead of discarding it, so a wire-format regression surfaces here rather than as a missing-event assertion downstream.
The 300_000 per-test timeout appeared three times; hoist it to a RUN_TIMEOUT const. The dump test reuses the harness's exported BIG maxBuffer instead of a second 64*1024*1024 literal.
Hickey/Lowy AnalysisBoth lenses ran in parallel on the feature commit, then a cross-validation pass audited each lens against the other's recommendations. The cross-validation reversed three first-pass findings — a good example of why it exists.
Why #1–#3 became No-ops (cross-validation)
Hickey rationaleThe one real structural defect was the Lowy rationaleThe big boundaries are right: |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 1s | git fetch ok; forge=github |
| research | ✓ | 6m 45s | Mapped odu run local-lane internals; key discovery: resolveDrvPath runs even for localhost, so each fixture must be a flake exposing odu-runner |
| branch | ✓ | ~0s | On feature branch e2e |
| implement | ✓ | ~0s* | tests/e2e/ suite + fixtures + vitest.e2e config + test:e2e + e2e CI step; 4/4 green |
| check | ✓ | 6s | pnpm typecheck clean (now covers tests/) |
| docs | ✓ | ~0s | README Developing block + top-level just e2e; tests/e2e/README.md |
| fmt | — | 0s | skipped — no format command configured |
| commit | ✓ | 12s | feature commit 2c8a041 pushed |
| hickey+lowy | ✓ | 9m 25s | parallel review + cross-validation; 3 first-pass Fixes overturned to No-op, 2 real Fixes committed |
| police | ✓ | ~0s* | 3 passes; dry-rule + no-silent-swallow fixed, 4 elegance fixes, 2 documented No-ops |
| test | ✓ | 28s | test:e2e 4/4, test:unit 80/80 |
| create-pr | ✓ | 48s | draft PR #20 + hickey/lowy ledger |
| ci | ✓ | ~14m* | GitHub Actions green at HEAD e9c074f on both platforms (ci::e2e@x86_64-linux + ci::e2e@aarch64-darwin) |
| evidence | — | 0s | skipped — not opted in |
| Total | ~43m |
*Some per-step durations are approximate — a few do-results start/end bookends were recorded back-to-back, and the ci wall-clock (~14m, cold nix on macOS) landed outside its recorded window.
Slowest step: hickey+lowy (9m 25s)
Optimization suggestions
hickey+lowy(9m 25s) dominated — the cross-validation pass (4 extra sub-agents) is what earned its keep here, reversing the headlineProgressEvent-dedup finding. Worth it on a new-abstraction PR; for a trivial diff,--minimalskips this entirely.research(6m 45s) — the whole design hinged on one fact: a localodu runstill doesnix eval <repo>#…odu-runner.drvPath. Pre-readingsrc/coordinator/run.ts(thestartLane/resolveDrvPathblock) and@kolu/surface-nix-host'sgetHostSessionbefore invoking/dowould cut this in half.cicold-cache (~14m, macOS) — the e2e step'snix buildis the cost; it's a cache hit on warm runners. For re-runs after a fix,--from ci-onlyskips straight to it.- The e2e suite's first run pays a cold
nix build .#odu; locally,just e2eis fast once the store is warm.
Workflow completed.
odu now has end-to-end tests that exercise the real CLI as a black box — building the binary with
nix build, running it against a throwaway fixture repo on a localhost lane, and asserting on its--progress jsonstream and process exit code. Until now the only integration coverage was the in-process loopback suite (src/odu.test.ts), which stubs the transport with an in-memory stream pair. These tests cover the seams that suite fakes.What it proves end-to-end
The suite spawns the actual
odubinary — nothing is imported fromsrc/, so the contract under test is the binary's observable behavior, not its internals. Two fixtures drive the two outcomes that matter:odu runexits0status: "failed"/exit_code: 1and the process exits1Plus a smoke test for
odu dump(resolves the pipeline without a live socket) and a shape check on the progress events.The fixture is a flake, on purpose
A local
odu runresolves its lane runner by evaluating<repo>#packages.<system>.odu-runner.drvPath— even for a localhost lane (the realise is a local no-op copy, but the drvPath lookup still happens). So any repo that runs odu locally must exposeodu-runnerin its flake. Each fixture is therefore a tiny flake that re-exportsodu-runnerfrom the checkout under test, so it runs the exact runner the harness just built (a Nix cache hit). The leaf recipes are pure shell — the fixture's own "CI" is trivial so the test exercises odu's machinery, not a toolchain.Deliberate tradeoffs (documented in
tests/e2e/README.md)--progress json(NDJSON), not the TTY dashboard--no-strictsrc/imports)Wired in as a new
e2estep inci/mod.just(and a top-leveljust e2e). Follow-ups — TTY dashboard /attach, a real ssh-to-localhost transport, the MCP face, andstatus/logsagainst a live socket — are listed in the README.Generated by
/doon Claude Code (modelclaude-opus-4-8).