Turbo / Nx test-suite survey (2026-09-10)
The owner’s ask: “go through all tests of nx and turbo, see if we miss
any test or any core functionality, e.g. prevention of circular task
deps.” Two survey passes over fresh sparse clones (Turborepo at
crates/turborepo/tests, turborepo-lib, turborepo-cache,
turborepo-scm; Nx at packages/nx/src, task-runner / hasher /
project-graph / command-line / native), every upstream test classified
against vx’s suites with grep evidence: COVERED, GAP-TEST (behaviour
present, unpinned), GAP-CORE (behaviour absent), DIVERGENT (documented
choice), REJECTED (owner’s list). This is the record of what came out
and what was decided; docs/parity.md stays the user-facing map.
Counts. Turbo: 568 tests over 74 files — ~355 rejected (Cargo/Go/uv
workspaces, prune, query, the daemon, TUI, futureFlags, globals, env
modes, // tasks…), ~155 covered, ~28 divergent, 10 core gaps, 5 test
gaps. Nx: ~380 cases in 146 rows — 78 covered, 41 rejected, 17
divergent, 7 core gaps, 3 test gaps. Circular task dependencies
specifically: every cycle shape both suites pin is covered, and vx
refuses at graph build rather than breaking edges (NX_IGNORE_CYCLES
has no vx spelling, on purpose).
Fixed the same day (STATUS 102–105)
Section titled “Fixed the same day (STATUS 102–105)”- A literal directory in
cache.inputs.files/outputs.files(src/,dist) matched nothing — the most common turbo.json shape migrated into a config that folded zero inputs and cached an empty artifact. A literal is now the file or its whole tree (Turboglobs_test.rs). - A negated workspace package glob (
!packages/fixtures) inverted discovery: every manifest in the tree became a member. Negations subtract now, in discovery and the root-claim walk (Nxproject-configuration-utils.spec.ts). --affecteddiffed from the ref, not the merge base of ref and HEAD, so a branch off a moved trunk selected everyone else’s changes (Turboaffected_test.rs, Nxcommand-line-utils.spec.ts).- Path filters take a glob (
./packages/*,{apps/**}), matched against the project’s own dir (Turboinfer_pkg_test.rs). - A
--cachespec naming a remote axis with no remote layer says so (Turborun_caching.rs). - Pins: the cycle message names the path; an unknown
pkg#tasktarget is named as such;--concurrency <n>%;pkg#taskruns under--filter '!pkg'. Comparison rows: no bare-task cross-product;parallelism: falsemaps to a whole-budget reservation (thenexec.resources; a schedule-plugin reservation since 2026-09-12).
Fixed (STATUS 106, same night)
Section titled “Fixed (STATUS 106, same night)”- SIGINT/SIGTERM: SIGTERM then
process.exitwith no grace and no SIGKILL escalation, so a child that trapped TERM was orphaned by a CI cancellation (Turbograceful_shutdown_test.rs). Now: SIGTERM, aVX_KILL_GRACE_MSwait (2 s, the persistent shutdown’s grace), SIGKILL, exit 130/143; a second signal skips the grace. - A task whose command re-entered
vx runforked without bound (Turborecursive_turbo_test.rs). Now every child carriesVX_RUN_WORKSPACE/VX_RUN_TASKandrun()refuses its own root, naming the task — the terminating shape too, since a nested run is invisible to the outer graph; a different workspace is still fine.
Deferred, with the reasoning
Section titled “Deferred, with the reasoning”- Project edges outside
package.json(NximplicitDependencies; an e2e package that testsappwithout depending on it was invisible to...[ref]). DECIDED 2026-09-10 (STATUS 113): no new field — a cross-projectdependsOnedge (e2e’sdependsOn: ['app#build']) now counts for.../^...selection, since the task graph already knows it; every surface agrees without a second declaration. A workspace-level default base stays rejected:--affected=<ref>is one flag and theorigin/HEADfallback covers the rest. - Project-selector
dependsOn(lib*#build,!lib1). vx materialises concrete edges and rejects a pattern in thepkg#taskform; task-name patterns (^build.*) cover the common case. Revisit when a real workspace needs it. - Default base configuration (
defaultBase,NX_BASE). vx falls backorigin/HEAD→HEAD~1; a workspace-levelaffectedBaseis a small addition when someone asks. FORCE_COLORsynthesized for children (Nx defaults it on). vx forwards it only when set; a default would put ANSI into cached logs and every renderer. Explicit over magical.- Structured log stream (
--log-file,--jsonNDJSON). The telemetry seam carries every record; a sink plugin is the vx spelling. Core will not grow a second stream. - Richer
--dry=json/--summarize(per-file input hashes, command, env).vx showgives the command,vx whythe input diff; a pre-execution file listing is a candidate, not a gap. - watch + persistent divergence (documented, unpinned): a pin needs a watch harness that is not flaky; on the list with M7/M8.
- Scoped vs whole-repo enumeration equivalence: DONE (STATUS 108),
tests/enumeration-equivalence.test.ts— twelve seeded trees, every worktree state, exact partitions and trusted OIDs in both modes.