From 8a74f7beffd215f2074855d965782ce4065458d3 Mon Sep 17 00:00:00 2001 From: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Date: Fri, 9 Oct 2026 11:05:23 -0400 Subject: [PATCH] ci: guard test-unit clauses and the rust-cache workspace root Fail just test-unit when any combined-filter clause selects no tests, so a renamed module or target cannot silently drop out of the union. Add a cargo-metadata check that no dependency manifest sits under the workspace root, run in Unit Tests before rust-cache can save, and select desktop jobs for bin/hermit.hcl changes. Refresh comments that described removed flags. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> --- .github/workflows/_ci-rust.yml | 6 ++ .github/workflows/ci.yml | 3 +- Justfile | 30 ++++++--- bin/hermit.hcl | 2 + scripts/check-rust-cache-workspace-root.mjs | 39 ++++++++++++ .../check-rust-cache-workspace-root.test.mjs | 63 +++++++++++++++++++ scripts/ci-selection.test.mjs | 1 + 7 files changed, 134 insertions(+), 10 deletions(-) create mode 100755 scripts/check-rust-cache-workspace-root.mjs create mode 100644 scripts/check-rust-cache-workspace-root.test.mjs diff --git a/.github/workflows/_ci-rust.yml b/.github/workflows/_ci-rust.yml index e25639d3ac..142e708984 100644 --- a/.github/workflows/_ci-rust.yml +++ b/.github/workflows/_ci-rust.yml @@ -64,6 +64,12 @@ jobs: uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 with: tool: cargo-nextest@0.9.136 + # Every Hermit + rust-cache job resolves the same CARGO_HOME (Hermit + # re-applies bin/hermit.hcl on each exec), so checking once here covers + # them. Runs on PRs too, and on main fails before rust-cache's + # success()-gated post-save can store an empty root-workspace cache. + - name: Rust cache workspace-root contract + run: cargo metadata --all-features --format-version 1 | node scripts/check-rust-cache-workspace-root.mjs - name: Unit tests run: just test-unit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3e541846d..6321307826 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,7 @@ jobs: desktop: - '.github/workflows/ci.yml' - '.github/workflows/_ci-desktop.yml' + - 'bin/hermit.hcl' - 'scripts/ci-apt-retry.sh' - 'Justfile' - 'scripts/generate-macos-icon.py' @@ -135,7 +136,7 @@ jobs: - name: CI required-context isolation contract run: scripts/test-ci-required-context-isolation.sh - name: CI path selection regression tests - run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs scripts/ci-apt-retry.test.mjs + run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs scripts/ci-apt-retry.test.mjs scripts/check-rust-cache-workspace-root.test.mjs - name: File size policy run: just file-size-check diff --git a/Justfile b/Justfile index 9990fa606d..145f89fc10 100644 --- a/Justfile +++ b/Justfile @@ -416,7 +416,7 @@ test-unit: 'package(buzz-cli)' # buzz-sdk builder/validation unit tests: pure event-builder and input # validation (e.g. the canvas writer-discipline/skew guard and the - # canvas_write_survived predicate), no infra. `--lib` runs all unit + # canvas_write_survived predicate), no infra. kind(lib) runs all unit # tests without the rustdoc dependency-resolution flake the full-package # invocation hits. Enumerated explicitly because nothing in CI runs # `cargo test --workspace` — membership buys clippy/check, not tests. @@ -433,13 +433,13 @@ test-unit: # additive migration set; legacy cutover/backfill remains an operator # script, not startup state) and the tenant-scoping lints. The # Postgres-backed buzz-db tests are - # #[ignore]d, so --lib runs only the infra-free set. Without this gate a + # #[ignore]d, so kind(lib) runs only the infra-free set. Without this gate a # stray file in migrations/ or a broken lint ships green. 'package(buzz-db) & kind(lib)' # buzz-db source-policy tests: infra-free scans of the crate sources # that enforce the event-write admission chokepoint, metric/provenance # contracts, and their fixtures. They live in an integration-test - # binary, so `--lib` above does not run them. + # binary, so kind(lib) above does not run them. 'package(buzz-db) & binary_id(buzz-db::observability_source)' # Storage accounting crosses three crates whose focused regression # suites are otherwise absent from the infra-free unit lane. @@ -450,7 +450,7 @@ test-unit: # Multi-tenant conformance gate (buzz-conformance): the independent # replay checker + golden fixtures. No infra — pure in-process trace # replay — so it belongs in the unit job. Run all targets (lib + the - # tests/replay_fixtures.rs integration test), not just --lib. + # tests/replay_fixtures.rs integration test), not just kind(lib). 'package(buzz-conformance)' # Gateway unit and black-box HTTP tests are infra-free. Postgres-backed # contract/race tests run in the dedicated CI job below. @@ -486,7 +486,7 @@ test-unit: # #[ignore]d Postgres suites — so these non-ignored tests ran in no lane # and a red one could ship green (exactly how a broken admin test slipped # past every gate once). Scoped to api::admin, not the whole buzz-relay - # --lib, because api::media has non-ignored tests that require Postgres. + # kind(lib), because api::media has non-ignored tests that require Postgres. # DB-backed api::admin tests are #[ignore]d and run in the PostgreSQL # lane; the non-ignored ones reject before touching the database. Any # new non-ignored test here must stay DB-free: without a database, a @@ -510,9 +510,10 @@ test-unit: # neighbours: nip_fi_http, nip_fi_config, router, api::parse_query_tests, # and the Git transport off_mode_precedence_tests. All are infra-free # and finish in well under a second with no DATABASE_URL, so none wait - # out the sqlx acquire timeout. `--bin buzz-relay` adds main.rs's - # `tests::` module (JWKS refresh cadence) and `composition_tests::` - # (JWKS source + refresh-loop composition), which `--lib` cannot reach + # out the sqlx acquire timeout. binary_id(buzz-relay::bin/buzz-relay) + # adds main.rs's `tests::` module (JWKS refresh cadence) and + # `composition_tests::` (JWKS source + refresh-loop composition), + # which kind(lib) cannot reach # because they live in the binary target; the nested # `tests::postgres_tests::` stays in the PostgreSQL lane. # The fourth clause adds the startup step timers (startup_steps) and the @@ -593,13 +594,24 @@ test-unit: package=${clause#package(} unit_packages+=(-p "${package%%)*}") done + # Each old per-package command failed when its selection went empty + # (nextest exits 4 on zero tests, cargo on a missing --test target). + # In the union an empty clause would be absorbed silently, so check + # every clause still selects a test. Same -p list: no rebuild. + for clause in "${unit_filters[@]}"; do + if ! cargo nextest list "${unit_packages[@]}" -E "$clause" \ + --message-format oneline --cargo-quiet | grep -q .; then + echo "test-unit: clause selects no tests: $clause" >&2 + exit 1 + fi + done unit_expr=$(printf ' | (%s)' "${unit_filters[@]}") cargo nextest run "${unit_packages[@]}" -E "${unit_expr# | }" # buzz-auth NIP-FI verifier doctests. The sealed-authority # `compile_fail` doctests prove the default-feature public API alone # cannot forge the issuer→JWKS authority; nextest does not run # doctests, hence this separate step. The verifier's regression suite - # lives in the in-crate `#[cfg(test)] mod tests`, so `--lib` above + # lives in the in-crate `#[cfg(test)] mod tests`, so kind(lib) above # already runs it. cargo test -p buzz-auth --doc # buzz-db `AdmittedTx` doctests. The `compile_fail` cases prove code diff --git a/bin/hermit.hcl b/bin/hermit.hcl index 691f6d7d02..dadc57cea2 100644 --- a/bin/hermit.hcl +++ b/bin/hermit.hcl @@ -4,6 +4,8 @@ manage-git = true # which made Swatinem/rust-cache treat every registry crate as a workspace # member and save an empty target/ for the root workspace. A shared home also # lets worktrees reuse one registry. scripts/test-rust-cache-contract.sh pins this. +# ~/.cargo-hermit/bin comes first on PATH, so a `cargo install` from any buzz +# checkout shadows same-named Hermit-pinned tools in every buzz checkout. env = { "CARGO_HOME": "${HOME}/.cargo-hermit", "PATH": "${HOME}/.cargo-hermit/bin:${PATH}", diff --git a/scripts/check-rust-cache-workspace-root.mjs b/scripts/check-rust-cache-workspace-root.mjs new file mode 100755 index 0000000000..def2905afe --- /dev/null +++ b/scripts/check-rust-cache-workspace-root.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +// Fails when Swatinem/rust-cache would mistake dependencies for workspace +// members. rust-cache keeps target/ outputs only for packages whose +// manifest_path is outside the workspace root; a dependency whose manifest +// sits under the root (a registry under the checkout, a vendored source, a +// path dependency into a nested directory) is treated as workspace code and +// cleaned away. When that covers every dependency the root-workspace cache +// saves an empty target/, and the exact-key hit blocks every later save. +// +// Usage: cargo metadata --all-features --format-version 1 | node scripts/check-rust-cache-workspace-root.mjs +import { readFileSync } from "node:fs"; +import { sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +export function dependenciesUnderWorkspaceRoot(metadata) { + const members = new Set(metadata.workspace_members); + const root = metadata.workspace_root.endsWith(sep) + ? metadata.workspace_root + : metadata.workspace_root + sep; + return metadata.packages + .filter((pkg) => !members.has(pkg.id) && pkg.manifest_path.startsWith(root)) + .map((pkg) => `${pkg.name} ${pkg.version} (${pkg.manifest_path})`); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const metadata = JSON.parse(readFileSync(0, "utf8")); + const inside = dependenciesUnderWorkspaceRoot(metadata); + if (inside.length > 0) { + console.error( + `${inside.length} non-member package(s) have a manifest under ${metadata.workspace_root}; ` + + "rust-cache would treat them as workspace code and save an empty target/. " + + "Keep CARGO_HOME and vendored sources outside the checkout (see bin/hermit.hcl).", + ); + for (const line of inside.slice(0, 10)) console.error(` ${line}`); + process.exit(1); + } + const outside = metadata.packages.length - metadata.workspace_members.length; + console.log(`rust-cache workspace root ok: ${outside} dependency package(s) outside ${metadata.workspace_root}`); +} diff --git a/scripts/check-rust-cache-workspace-root.test.mjs b/scripts/check-rust-cache-workspace-root.test.mjs new file mode 100644 index 0000000000..ad1522a1a4 --- /dev/null +++ b/scripts/check-rust-cache-workspace-root.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +import { dependenciesUnderWorkspaceRoot } from "./check-rust-cache-workspace-root.mjs"; + +const script = fileURLToPath( + new URL("./check-rust-cache-workspace-root.mjs", import.meta.url), +); + +function metadata(registry) { + return { + workspace_root: "/work/buzz", + workspace_members: ["path+file:///work/buzz/crates/buzz-core#0.1.0"], + packages: [ + { + id: "path+file:///work/buzz/crates/buzz-core#0.1.0", + name: "buzz-core", + version: "0.1.0", + manifest_path: "/work/buzz/crates/buzz-core/Cargo.toml", + }, + { + id: "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.0", + name: "serde", + version: "1.0.0", + manifest_path: `${registry}/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.0/Cargo.toml`, + }, + ], + }; +} + +function check(meta) { + return spawnSync("node", [script], { + input: JSON.stringify(meta), + encoding: "utf8", + }); +} + +test("a registry outside the checkout passes", () => { + const meta = metadata("/home/runner/.cargo-hermit"); + assert.deepEqual(dependenciesUnderWorkspaceRoot(meta), []); + const result = check(meta); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /1 dependency package\(s\) outside/); +}); + +test("Hermit's default in-checkout registry fails", () => { + const meta = metadata("/work/buzz/.hermit/rust"); + assert.deepEqual(dependenciesUnderWorkspaceRoot(meta), [ + "serde 1.0.0 (/work/buzz/.hermit/rust/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.0/Cargo.toml)", + ]); + const result = check(meta); + assert.equal(result.status, 1); + assert.match(result.stderr, /1 non-member package\(s\) have a manifest under \/work\/buzz/); +}); + +test("a sibling directory sharing the root prefix is outside the root", () => { + assert.deepEqual( + dependenciesUnderWorkspaceRoot(metadata("/work/buzz-cargo")), + [], + ); +}); diff --git a/scripts/ci-selection.test.mjs b/scripts/ci-selection.test.mjs index 0ce88ef067..a67951675a 100644 --- a/scripts/ci-selection.test.mjs +++ b/scripts/ci-selection.test.mjs @@ -233,6 +233,7 @@ const scenarios = [ ["desktop", "mobile", "rust"], ], ["Justfile", ["Justfile"], ["desktop", "rust"]], + ["Hermit environment", ["bin/hermit.hcl"], ["desktop", "rust"]], ["CI apt retry", ["scripts/ci-apt-retry.sh"], ["desktop", "rust"]], ["integration services", ["docker-compose.yml"], ["rust"]], ["CI integration images", ["docker-compose.ci.yml"], ["rust"]],