Skip to content

feat(affected): narrow package script chains; check flags entry targets it cannot read - #227

Merged
calvin-archastro merged 4 commits into
mainfrom
fix/narrow-script-chains
Oct 30, 2026
Merged

calvin-archastro merged 4 commits into
mainfrom
fix/narrow-script-chains

Conversation

@calvin-archastro

@calvin-archastro calvin-archastro commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Problem and author intent

aster affected <target> --related ran many CI test targets in full even when a change reached only a few tests, and aster check said nothing about it.

Observed on a sweep of 39 recent firstlanding PRs (aster affected test-ci --related --dry-run): "not a test command Aster can narrow; running as written" on //infra:test-ci (14 PRs), //services/go/archdev/web:test-ci (8), //infra/services/platform, //deploy/controller, //src/ts/developer-platform-cli, and others.

Mechanism:

target command: pnpm test-ci
  -> package.json: "test-ci": "pnpm a && pnpm b && tsx --test x.ts y.ts && tsc --noEmit"
  -> node plugin narrows a script only if its body is nothing but the runner (Argv::parse refuses `&&`)
  -> related_tests() = None -> Narrowed::NotRunner -> "running as written"

aster check: test_targets() `continue`s when no plugin recognises a runner in the written words,
so `pnpm test-ci` or `./test-ci.sh` was never a test target -> 0 findings.

Goal: narrow chains of package scripts (generic, any repo), and make aster check report every entry target the planner cannot narrow, using the same classification.

What changed (before -> after)

Narrowing (src/plugins/related_tests.rs):

  • Before: pnpm test-ci with a chained body -> run as written.
  • After: for npm/pnpm the body is split on &&, nested script calls are expanded (depth 8, cycle guard), and each command is narrowed or kept:
    • test runner (Vitest, Jest, Mocha, bun test, node --test, tsx --test, node --import tsx --test): narrowed to the selected files it covers; dropped if none.
    • any other command (tsc --noEmit, node gen.mjs --check): kept as written.
    • no runner left -> the target is skipped like any narrowed test target. Non-test checks in a test target therefore run only when a test in that target runs; they belong in lint/build.
  • Each kept command becomes one command of the plan (run in order, fail at first failure, same as &&), started with pnpm exec <cmd> / npm exec -c '<cmd>' so node_modules/.bin is on the path as in a script. NAME=value stays in front. One command per step (not one a && b string) so configured wrappers and bash -c one-liners apply to each step.
  • node --test with a shell glob (test/*.test.ts) is now matched against the selection (globset, * stops at /). A node --test file that does not exist (built by an earlier step) leaves that command as written.
  • Vitest/Jest steps get --passWithNoTests (a selected file outside that config's include would otherwise fail the run).
  • Unchanged: a script that is only the runner still becomes pnpm test <files>; ;, ||, pipes, cd, interpreter scripts, and pre/post hooks still stop narrowing (a called script with those stays a call). yarn/bun chains are not expanded (no equivalent of exec that finds package binaries).

Check (src/check.rs, src/cli/affected_files.rs):

  • Before: a command with no recognisable runner was not a test target.
  • After: an entry target (--entry, [check] entries, a lane target) whose command is not inert and not narrowable is reported by unnarrowable-test-command with a specific reason (script file / package script with no runner / unsupported form). A package script that contains a runner Aster cannot narrow is reported for any target. Same narrowable computation as the planner.

Docs: README (narrowing section and rule table), src/cli/skills.md.

Scope

Backend/CLI only (Rust).

Risk

Medium. Changes what --related runs for package-script chains, which previously ran in full.

  • Unsound drops were the main concern: a runner that cannot be narrowed is kept as written (never dropped), hooks/cd/operators leave the script alone.
  • Known limitation: narrowing vitest run --coverage runs a subset, which can trip coverage thresholds.
  • aster check gains findings on repos that have entry targets running script files; --strict users will see new failures (intended).

User impact

CI with --related runs fewer tests for chain targets. New aster check findings for script-file and unreadable entry targets.

Testing

  • Unit: package_script_chain_tests and updated node_narrows_direct_runners_and_plain_scripts in src/plugins/related_tests.rs (nested chain, segment dropping, all-dropped skip, non-runner kept, cycle, cd, hooks, yarn, npm form, globs, declined runner kept).
  • End to end (canonical): tests/script_chains.rs::a_package_script_chain_narrows_and_what_aster_cannot_read_is_reported runs the real binary on a git workspace with five test-ci variants; asserts the narrowed commands and that the set of targets the planner reports as "not a test command Aster can narrow" equals the set aster check --entry test-ci reports. a_chain_with_no_selected_test_is_skipped_with_its_other_commands covers the skip.
  • cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test: pass locally. Automatic coverage: the existing Aster CI cargo test job picks up tests/script_chains.rs.
  • Firstlanding sweep (release binary, old v0.27.1 vs new):
PR target before after
21141, 21114, 21098 //services/go/archdev/web:test-ci as written vitest narrowed (+ node --test narrowed or dropped)
21098 //src/ts/developer-platform-cli:test-ci as written unit + e2e vitest narrowed, ensure-node-pty kept
21085 //infra/services/archdev:test-ci as written narrowed
21085 //infra/services/platform:test-ci as written skipped
21128 //specs/deploy:test-ci as written skipped
21096 //deploy/controller:test-ci as written generator check + narrowed node test + tsc
21021, 21047, 21096, 21133, ... //infra:test-ci as written tsc + narrowed tsx tests; node --test bin-test/*.js kept as written

aster check --strict --entry test-ci on firstlanding origin/main: 0 findings before, 21 after.

Added after review

  • A narrowed run disables coverage (thresholds measure a whole suite): Vitest drops --coverage* and gets --coverage.enabled=false; Jest --coverage=false; pytest drops --cov* and gets --no-cov (only if it used --cov); mix test drops --cover; bun and node --test drop their coverage flags. A package script with coverage in its body is read as a chain so the flag can be removed. Tests: coverage_tests, tests/script_chains.rs::a_coverage_gated_chain_runs_its_narrowed_tests_without_coverage.
  • orphan-test-target: a target is reached when a reached target's package script calls its script (bare pnpm <script>, npm run, yarn, nested), via the same call reader as the chain expansion; a script that only aliases reached scripts is reached too. Test: tests/check.rs::a_target_that_a_reached_package_script_calls_is_not_an_orphan.
  • New [check] allow_unnarrowable = ["//addr:target"] and error rule stale-allow-unnarrowable, mirroring allow_orphans. Test: tests/check.rs::allow_unnarrowable_accepts_a_fixed_command_and_stale_entries_are_errors.
  • Full 39-PR firstlanding sweep with the final build: no regressions, no failures.

Follow-ups

  • Not proven here: yarn/bun chains (not expanded), coverage-threshold behaviour of narrowed --coverage runs, and a real pnpm exec run of an expanded chain in a node_modules checkout.
  • A script file entry (./test-ci.sh) is only reported; Aster does not read it.

🤖 Generated with Claude Code

calvin-archastro and others added 2 commits October 29, 2026 21:09
…ts it cannot read

--related expands an npm or pnpm script whose body is an && chain, through the scripts it
calls, and narrows each test runner in it (including node --test and tsx --test with files or
globs). Runners that select nothing are dropped, other commands are kept, and a target left
with no runner is skipped.

aster check reports an entry target (--entry, [check] entries, a lane target) that runs a
script file or a package script Aster cannot narrow, and a package script that contains a
runner it cannot narrow, with the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ust name existing files

A package script called from a chain that cannot be read (a cd, a pre/post hook, a cycle) stays
a call instead of making the whole chain unnarrowable. node --test with a file that does not
exist yet (built by an earlier step) is left as written. aster check no longer calls
python -m or node --test a script file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@calvin-archastro
calvin-archastro requested a review from a team October 30, 2026 04:20
@calvin-archastro
calvin-archastro marked this pull request as draft October 30, 2026 04:30
calvin-archastro and others added 2 commits October 29, 2026 21:38
Coverage thresholds measure a whole suite, so a run narrowed to a few files fails thresholds the
full run passes. Narrowed Vitest drops --coverage/--coverage.* and gets --coverage.enabled=false,
Jest gets --coverage=false, pytest drops --cov* and gets --no-cov (when it used --cov), mix test
drops --cover, bun test and node --test drop their coverage flags. A package script that turns
coverage on in its body is read as a chain so the flag can be removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
orphan-test-target now treats a target as reached when a reached target's package script calls
its script (bare pnpm <script>, npm run, yarn, nested), through the same call reader the chain
expansion uses, and when its script only aliases such scripts.

[check] allow_unnarrowable accepts a command fixed on purpose; a stale entry is the error
stale-allow-unnarrowable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@calvin-archastro
calvin-archastro marked this pull request as ready for review October 30, 2026 05:00
@calvin-archastro
calvin-archastro merged commit c732d3d into main Oct 30, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant