diff --git a/CHANGELOG.md b/CHANGELOG.md index 519927c6b..41e008bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,22 +2,24 @@ ## [Unreleased] -**Pregate gives touched Rust files a test lane, and runs a nested pnpm or vitest package with its own runner (#3344).** In a mixed Cargo and JavaScript repo, every touched `.rs` file failed `lanes` with "has no test lane", so /ship could not get a receipt, and a nested package's vitest tests ran under `bun test` from the repo root (about 98 false failures in the reporter's repo). Ported from #3346 by vilano93, whose two commits carry the feature; our commits on top quote the crate name and make the new fixtures hermetic. +**Pregate gives touched Rust files a test lane, and runs a nested pnpm or vitest package with its own runner (#3344).** In a mixed Cargo and JavaScript repo, every touched `.rs` file failed `lanes` with "has no test lane", so /ship could not get a receipt, and a nested package's vitest tests ran under `bun test` from the repo root (about 98 false failures in the reporter's repo). Ported from #3346 by vilano93, whose two commits carry the feature; our commits on top close four findings from a security-focused review of the port and make the new fixtures hermetic. ### Fixed - **`lib/pregate/rust.ts`** (new, vilano93): a crate is the nearest Cargo.toml with `[package]`. Its `.rs` files under `tests/` or holding a `#[cfg(test)]` are tests, and every `.rs` file of a crate and its Cargo.toml select that crate's tests and those of every crate depending on it. Manifests are parsed as TOML, so renamed, inherited, dotted, table-form and target-specific dependencies all count, and a crate runs from the workspace it is a member of under Cargo's rules (members, exclude, `package.workspace`). Nothing runs cargo to build the graph. - **`lib/pregate/checks/lanes.ts`** (vilano93): selected Rust tests run per crate as `cargo test -p ` from the workspace root and are judged on the summed `test result:` lines; zero tests is ZERO-RUN and a missing cargo is incomplete. A JS/TS test owned by a nested package.json that declares vitest or jest runs from that package through the package manager its `packageManager` field or nearest lockfile names (`pnpm exec`, `yarn`, `bun x`, else `npx`). -- **`lib/pregate/checks/lanes.ts`**: the crate name comes from the repo's Cargo.toml and reaches the lane's shell, so it is now quoted like every file and cwd. Before, a crate named `evil;touch pwned` ran `touch pwned` in the workspace (the lane only runs with `--allow-repo-commands`, under which the repo's tests run anyway, but no other input reached the shell unquoted). +- **`lib/pregate/checks/lanes.ts`**: the crate name comes from the repo's Cargo.toml and reaches the lane's shell. Before, a crate named `evil;touch pwned` ran `touch pwned` in the workspace, and a name containing `{files}` spliced the file list into the command. A name outside Cargo's grammar (letters, digits, `_`, `-`) now gets no runner, so its files are unclaimed and the lane is incomplete, as Cargo would refuse the manifest anyway; a valid name is quoted like every file and cwd. The lane only runs with `--allow-repo-commands`, under which the repo's tests run anyway, but no other repo input reached the shell unquoted. +- **`lib/pregate/rust.ts`**: Cargo `members` globs are matched per path component by a small linear matcher. The first cut used the policy glob matcher, which matched `[ab]` literally (a member listed that way lost its inherited workspace dependencies, so its tests were never selected) and backtracked for seconds on a hostile glob. +- **`lib/pregate/rust.ts`**: a plain `#[test]` or `#[tokio::test]` function in `src/` makes the file a test, so a dependent crate tested that way runs when its dependency changes. The `#[cfg(...)]` scan is length-bounded, so a file of unclosed attributes no longer takes seconds. - **`lib/pregate/init-draft.ts`** (vilano93): `init` notes that Rust tests run per crate. ### Tests -- `test/pregate-rust.test.ts` (new, vilano93) covers the crate model, the dependency forms, workspace membership, the cargo summary parser and three end-to-end lane runs. A new case runs a crate named `evil;touch pwned-by-crate-name` and checks that nothing is created and cargo receives the name as one argument; it fails before the quoting fix. +- `test/pregate-rust.test.ts` (new, vilano93) covers the crate model, the dependency forms, workspace membership, the cargo summary parser and three end-to-end lane runs. New cases: crates named `evil;touch pwned-by-crate-name` and `evil{files}` never reach the shell (nothing is created, both are unclaimed) while `naïve_crate-2` still runs quoted; a `crates/[ab]` and a `nested/**/leaf` member inherit workspace dependencies; a plain `#[test]` dependent is selected; and a hostile members glob plus 20,000 unclosed `#[cfg(` finish quickly. Each failed (or hung) before its fix. - The cargo stand-in now wins PATH lookup everywhere. On windows-free-shard (5) two end-to-end cases failed deterministically because the shim PATH was joined with `:` (Git Bash never saw it) and the runner's real cargo ran against the fixture. On hosts where Bun and cargo share a directory, the lane puts Bun's directory first and the real cargo won the same way. The shim PATH now uses the platform delimiter, adds `cargo.cmd` for Windows lookup, and on POSIX the lane's bash puts the shim first again through `BASH_ENV`. - `test/pregate-selection.test.ts` (vilano93) covers nested pnpm/yarn/vitest/jest packages. Its `pnpm` stand-in gets the same `BASH_ENV` fix: on a host with a real pnpm beside Bun, the end-to-end case installed vitest from the network and came back incomplete. ### Gate integrity - No threshold, budget, timeout, trial count or case kind changed. Pregate's lanes now map and run tests for `.rs` files that previously failed closed with "no test lane", and nested vitest/jest packages run under their own runner instead of `bun test`. A `.rs` file outside any crate, a crate with no tests, and a missing cargo or package manager still fail closed or stay incomplete; the tests above cover each. -- No new dependency (Cargo.toml is parsed with Bun's built-in TOML parser), no network access, no workflow change. +- No new dependency (Cargo.toml is parsed with Bun's built-in TOML parser), no network call in pregate itself, no workflow change. As with the root runner detection on main (`npx vitest run`), a nested package's detected runner (`npx`, `bun x`) may fetch a missing tool when it runs under `--allow-repo-commands`. ## [1.91.186.0] - 2026-10-24 diff --git a/docs/pregate.md b/docs/pregate.md index 0e6b58f09..3bb406682 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -59,7 +59,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`). | Key | Meaning | |---|---| -| `runner` | the local test command with `{files}` (gstack: `bun test {files}`); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p ` per Rust crate, run from the nearest ancestor Cargo.toml that declares `[workspace]`, else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, or a `.rs` file outside any crate is `incomplete` (the runnable groups still run, so a real failure still fails) | +| `runner` | the local test command with `{files}` (gstack: `bun test {files}`); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p ` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) | | `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences) | | `empty_lanes` | extra globs for the docs/no-tests empty lane. Any Markdown file already qualifies with no pin file when no test depends on it (the same import graph as other files: a quoted path, a `join()` of path segments, an import, or a `dependencies` entry). reStructuredText and AsciiDoc count as Markdown here. README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`, and so does licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`, `licenses/*.txt`). Source code and files under a test directory never qualify. A test that reads the file still selects that test | | `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) | @@ -136,8 +136,8 @@ Two stages with separate clocks, both printed: `"**/test_driver/**": []`) under `dependencies` and let CI run them; Dart files, device tests included, are source code, so a `dependencies` `[]` declaration clears them and `empty_lanes` does not); in a Rust crate (the nearest Cargo.toml with a `[package]` name) a `.rs` file is a - test when it sits under `/tests/` or holds a `#[cfg(test)]` (any cfg predicate naming - `test`), and every `.rs` file of the crate and its Cargo.toml select the crate's tests and + test when it sits under `/tests/`, holds a `#[cfg(test)]` (any cfg predicate naming + `test`) or a test attribute (`#[test]`, `#[tokio::test]`), and every `.rs` file of the crate and its Cargo.toml select the crate's tests and those of every crate whose manifest depends on it, directly or through other crates (cargo runs a crate's tests, not a file, so the lane is the crate; a crate with no tests still has no lane, and a workspace-only Cargo.toml or Cargo.lock needs a `dependencies` entry),