//! The cargo-shaped gates: test, clippy, fmt, supply chain and the hardening //! build, plus the machinery that runs one command over a list of targets and //! turns its output into a failure note. use super::GateCtx; use super::log::{append_to_log, stream_child_to_live_log}; use super::pg::clean_stale_test_dbs; use crate::classify; use crate::domain::{GateKind, GateRunId}; use crate::outcome::{GateFailure, GateOutcome, PassNote}; use anyhow::Result; use std::path::PathBuf; use tokio::process::Command; /// Run every configured `test_target`'s suite, in order, under one gate. /// /// Targets are configured (`[[test_target]]` in the daemon config), defaulting /// to a single `server` entry. A crate with no target ships ungated, `mnw-cli` /// included, which is built as a companion and installed onto prod-1 in the same /// promote. /// /// The whole set shares one `gate_runs` row and one log file: from the /// pipeline's point of view "the tests" either pass or don't. The first failing /// target ends the gate, since a red suite blocks the promote regardless of what /// the remaining crates would have said, and running them would only delay the /// operator's answer. Its name is carried in the failure so the summary points /// at the crate, not just the test. pub(super) async fn cargo_test(ctx: &GateCtx, run_id: GateRunId) -> Result { let log_path = ctx.log_path(GateKind::CargoTest); let log_ref = ctx.log_ref(GateKind::CargoTest); // Best-effort: drop our own role's stale `mnw_test_*` databases (the // template + any per-test clones orphaned by a previously-killed run) // before the suite, so they can't accumulate or collide. Foreign-owned // leftovers are left alone — the harness now namespaces its template per // role, so they no longer wedge the gate. if let Some(scratch_url) = ctx.cfg.scratch_db_url.as_deref() { clean_stale_test_dbs(scratch_url).await; } let started = std::time::Instant::now(); // One ceiling for the whole gate, not per target: the point is to bound how // long a hung suite can block the pipeline, and N targets each allowed the // full timeout would multiply that by N. let deadline = started + std::time::Duration::from_secs(ctx.cfg.gate_timeout_secs); let mut ran = 0usize; for target in &ctx.cfg.test_targets { let label = target.label(); // A target absent from this sha is skipped, not fatal: sando has to be // able to build older shas (bisect, rollback rebuild) from a config that // describes the tip. The zero-targets-ran check below is what stops this // from quietly turning the gate into a no-op. let Some(dir) = ctx .target_dir(target) .filter(|d| d.join("Cargo.toml").is_file()) else { tracing::warn!( target = %label, version = %ctx.version, "test_target has no Cargo.toml in this run; skipping", ); continue; }; let features: Vec<&str> = target.features.iter().map(String::as_str).collect(); // Fast pre-gate: compile the test targets WITHOUT running them. This // builds the exact artifacts the full run needs (so the subsequent run // reuses the cache — no wasted work), but fails in ~minutes with the // real `error[Ennnn]: ...` on a test-only-target compile break. That // class (a field missing in a `#[cfg(test)]`-only binary like `load`) // otherwise compiles fine under the build step and only blows up here, // after a full build, as an opaque mass test failure. let banner = format!("\n==== test_target: {label} ====\n"); append_to_log(&log_path, banner.as_bytes()).await; let mut pre = match cargo_test_command(ctx, &dir, target, &features, &["--no-run"]).spawn() { Ok(c) => c, Err(e) => { return Ok(GateOutcome::failed(GateFailure::SpawnFailed { message: format!("{label}: {e}"), }) .with_log_ref(log_ref)); } }; let (pre_out, pre_err, pre_status) = match run_to_deadline_for( &mut pre, ctx, run_id, log_path.clone(), deadline, started, GateKind::CargoTest, ) .await? { Ok(v) => v, Err(timeout) => return Ok(timeout.with_log_ref(log_ref)), }; if !pre_status.success() { let failure = classify::classify_compile_error(&pre_out, &pre_err); return Ok(GateOutcome::failed(name_target(failure, &target.dir)).with_log_ref(log_ref)); } // Full run: the test binaries are already built above, so cargo's // up-to-date check skips compilation and this just runs the tests. // // That claim is only true when the crate's build script is up to date // too, and for a long time it was not. server and multithreaded both // watched `.git/HEAD`, a path that does not exist at either package // root, and cargo reads a missing watch as changed: the build script // re-ran and the crate recompiled here, every time. It cost 347s a // pipeline, 35% of this gate, while this comment said it cost nothing. // Fixed 2026-08-20 in both build scripts; see `git_hash` in either. // // If this gate's duration ever climbs back toward the pre-pass's, look // for a new phantom watch before looking anywhere else. let mut child = match cargo_test_command(ctx, &dir, target, &features, &[]).spawn() { Ok(c) => c, Err(e) => { return Ok(GateOutcome::failed(GateFailure::SpawnFailed { message: format!("{label}: {e}"), }) .with_log_ref(log_ref)); } }; let (stdout_buf, stderr_buf, status) = match run_to_deadline_for( &mut child, ctx, run_id, log_path.clone(), deadline, started, GateKind::CargoTest, ) .await? { Ok(v) => v, Err(timeout) => return Ok(timeout.with_log_ref(log_ref)), }; if !status.success() { let failure = classify::classify_cargo_test(&stdout_buf, &stderr_buf); return Ok(GateOutcome::failed(name_target(failure, &target.dir)).with_log_ref(log_ref)); } ran += 1; } // Every configured target was missing from the worktree. Exiting green here // would report "tests passed" having run none of them. if ran == 0 { return Ok(GateOutcome::failed(GateFailure::Unclassified { legacy_detail: Some(format!( "cargo_test ran no targets: none of the {} configured test_target dir(s) \ exist in this worktree", ctx.cfg.test_targets.len(), )), }) .with_log_ref(log_ref)); } let duration_s = started.elapsed().as_secs() as u32; Ok(GateOutcome::passed(PassNote::TestsPassed { duration_s }).with_log_ref(log_ref)) } /// Stream a child to the live log, bounded by the gate-wide `deadline`. `Ok(Err(_))` /// is the timeout outcome (child killed); `Err(_)` is an IO error on the stream. #[allow(clippy::type_complexity)] async fn run_to_deadline_for( child: &mut tokio::process::Child, ctx: &GateCtx, run_id: GateRunId, log_path: PathBuf, deadline: std::time::Instant, started: std::time::Instant, kind: GateKind, ) -> Result, Vec, std::process::ExitStatus), GateOutcome>> { let remaining = deadline.saturating_duration_since(std::time::Instant::now()); let stream = stream_child_to_live_log(child, ctx.events.clone(), run_id, log_path); match tokio::time::timeout(remaining, stream).await { Ok(res) => Ok(Ok(res?)), Err(_elapsed) => { child.start_kill().ok(); let _ = child.wait().await; Ok(Err(GateOutcome::failed(GateFailure::Timeout { gate: kind, after_s: started.elapsed().as_secs() as u32, }))) } } } /// Prefix a test/compile failure's headline with the crate it came from, so a /// red gate across many targets says *which* crate broke. Other failure kinds /// are single-target by construction and pass through untouched. fn name_target(failure: GateFailure, dir: &std::path::Path) -> GateFailure { let at = dir.display(); match failure { GateFailure::CargoTest { failed_count, first_failed, first_panic, } => GateFailure::CargoTest { failed_count, first_failed: Some(match first_failed { Some(name) => format!("{at}: {name}"), None => at.to_string(), }), first_panic, }, GateFailure::CompileError { error_count, first_error, } => GateFailure::CompileError { error_count, first_error: Some(match first_error { Some(e) => format!("{at}: {e}"), None => at.to_string(), }), }, other => other, } } /// `cargo clippy --all-targets -- -D warnings` over every configured /// `test_target`. /// /// The only thing standing between lint drift and prod. pub(super) async fn clippy(ctx: &GateCtx, run_id: GateRunId) -> Result { lint_over_targets(ctx, run_id, GateKind::Clippy, |target, features| { let mut args = vec!["clippy".to_string(), "--all-targets".to_string()]; if target.all_features { args.push("--all-features".to_string()); } else if !features.is_empty() { args.push("--features".to_string()); args.push(features.join(",")); } // Everything after `--` goes to rustc, which is where -D lives. args.push("--".to_string()); args.push("-D".to_string()); args.push("warnings".to_string()); args }) .await } /// `cargo fmt --check` over every configured `test_target`. /// /// No `rustfmt.toml` exists anywhere in the tree, so this is plain rustfmt /// defaults. Cheap: no compilation, just a parse. pub(super) async fn fmt_check(ctx: &GateCtx, run_id: GateRunId) -> Result { lint_over_targets(ctx, run_id, GateKind::Fmt, |_target, _features| { vec!["fmt".to_string(), "--check".to_string()] }) .await } /// `cargo audit` / `cargo deny check`, over the crates that carry the matching /// config file. /// /// Config-gated on purpose. Both tools are only meaningful against a triaged /// posture: four crates in this repo fail `cargo audit` today purely because /// they have no `.cargo/audit.toml` recording which transitive advisories have /// been reviewed and accepted. Running them everywhere would make the gate /// permanently and uninformatively red. Dropping the config file into a crate /// is what opts it in. pub(super) async fn supply_chain( ctx: &GateCtx, run_id: GateRunId, kind: GateKind, ) -> Result { let (config_rel, args): (&str, Vec) = match kind { GateKind::CargoAudit => (".cargo/audit.toml", vec!["audit".into()]), GateKind::CargoDeny => ("deny.toml", vec!["deny".into(), "check".into()]), other => unreachable!("supply_chain called for {other:?}"), }; run_over_targets(ctx, run_id, kind, |_target, target_dir| { target_dir.join(config_rel).is_file().then(|| args.clone()) }) .await } /// Shared driver for the lint gates: run `cargo ` in every configured /// `test_target` that exists in this worktree. async fn lint_over_targets( ctx: &GateCtx, run_id: GateRunId, kind: GateKind, build_args: impl Fn(&crate::config::TestTarget, &[String]) -> Vec, ) -> Result { // The target is handed in directly. This used to reverse-look-it-up by // comparing `worktree.join(dir)` against the resolved path, which silently // stopped matching for anything resolved anywhere else — an aux repo, say. run_over_targets(ctx, run_id, kind, move |t, _dir| { Some(build_args(t, &t.features)) }) .await } /// Run one cargo invocation per configured `test_target`, under a single /// gate-wide deadline. `args_for` returns `None` to skip a target (used by the /// supply-chain gates, which only apply where their config file lives). /// /// Shares `cargo_test`'s conventions: per-target log banners, stop at the first /// failure with the crate named, skip targets absent from the worktree, and fail /// closed if that leaves nothing to run. async fn run_over_targets( ctx: &GateCtx, run_id: GateRunId, kind: GateKind, args_for: impl Fn(&crate::config::TestTarget, &std::path::Path) -> Option>, ) -> Result { let log_path = ctx.log_path(kind); let log_ref = ctx.log_ref(kind); let started = std::time::Instant::now(); let deadline = started + std::time::Duration::from_secs(ctx.cfg.gate_timeout_secs); let mut ran = 0usize; for target in &ctx.cfg.test_targets { let label = target.label(); let Some(dir) = ctx .target_dir(target) .filter(|d| d.join("Cargo.toml").is_file()) else { tracing::warn!( gate = kind.as_str(), target = %label, "target has no Cargo.toml in this run; skipping", ); continue; }; let Some(args) = args_for(target, &dir) else { continue; }; append_to_log( &log_path, format!("\n==== {}: {label} ====\n", kind.as_str()).as_bytes(), ) .await; let mut cmd = Command::new("cargo"); cmd.args(&args) .current_dir(&dir) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); if let Some(t) = ctx.cfg.cargo_target_dir.as_deref() { cmd.env("CARGO_TARGET_DIR", t); } // clippy type-checks, so it needs the same sqlx online-mode env the // build and cargo_test steps get. fmt/audit/deny never touch the DB. if kind == GateKind::Clippy && let Some(url) = ctx .cfg .scratch_db_url .as_deref() .filter(|_| target.scratch_db) { cmd.env("DATABASE_URL", url); cmd.env( "TEST_DATABASE_URL", url.split_once('?').map_or(url, |(b, _)| b), ); } let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { return Ok(GateOutcome::failed(GateFailure::SpawnFailed { message: format!("{label}: {e}"), }) .with_log_ref(log_ref)); } }; let (stdout_buf, stderr_buf, status) = match run_to_deadline_for( &mut child, ctx, run_id, log_path.clone(), deadline, started, kind, ) .await? { Ok(v) => v, Err(timeout) => return Ok(timeout.with_log_ref(log_ref)), }; if !status.success() { let failure = match kind { // clippy speaks rustc diagnostics, so the compile-error // classifier extracts the real `error: ...` headline. GateKind::Clippy => classify::classify_compile_error(&stdout_buf, &stderr_buf), _ => GateFailure::Unclassified { legacy_detail: Some(first_meaningful_line(&stdout_buf, &stderr_buf)), }, }; return Ok(GateOutcome::failed(name_target(failure, &target.dir)).with_log_ref(log_ref)); } ran += 1; } if ran == 0 { return Ok(GateOutcome::failed(GateFailure::Unclassified { legacy_detail: Some(format!( "{} ran nothing: no configured target in this worktree qualified", kind.as_str(), )), }) .with_log_ref(log_ref)); } Ok(GateOutcome::passed(PassNote::TestsPassed { duration_s: started.elapsed().as_secs() as u32, }) .with_log_ref(log_ref)) } /// First line that looks like a diagnostic, for gates whose tools have no /// dedicated classifier (`cargo audit`, `cargo deny`). Falls back to a generic /// note rather than an empty string. fn first_meaningful_line(stdout: &[u8], stderr: &[u8]) -> String { for buf in [stderr, stdout] { let text = String::from_utf8_lossy(buf); if let Some(line) = text .lines() .map(str::trim) .find(|l| l.starts_with("error") || l.contains("vulnerabilit") || l.contains("FAILED")) { return line.chars().take(200).collect(); } } "tool reported failure; see the gate log".into() } /// The tests `cargo_test` cannot reach, run against production constants. /// /// `cargo_test` builds with `--features fast-tests`, which relaxes /// `AUTH_RATE_LIMIT_BURST` 5 → 20, `SANDBOX_RATE_LIMIT_MS` 30s → 10ms, and /// argon2 from 46 MiB/t=2 to 8 MiB/t=1 — and the rate-limiting suite is /// `#[cfg_attr(feature = "fast-tests", ignore)]`d on top of that, because a /// bucket refilling at 100/sec never depletes under parallel test threads. The /// net effect was that Sando's only code gate silently skipped every test of /// the auth hardening it most needs to protect. /// /// So: no features, `--test-threads=1` (these tests key on a shared per-IP /// bucket and must not interleave), and a name filter rather than the whole /// suite — the rest of the suite is tuned for `fast-tests` and would only go /// slow and flaky here. The filter is a substring match, so it catches /// `..._rate_limit_...` and `..._rate_limited` alike. /// /// This costs a second compile of the lib + integration binary (a different /// feature set is a different cfg, so no artifact sharing with `cargo_test`). /// That is the price of the coverage; the filter keeps the *run* to seconds. pub(super) async fn hardening_test(ctx: &GateCtx, run_id: GateRunId) -> Result { let server_dir = match ctx.worktree_for(GateKind::HardeningTest) { Ok(w) => w.join("server"), Err(outcome) => return Ok(outcome), }; // No features is the whole point; the scratch DB is needed because the // server's sqlx macros type-check against it. Unlike cargo_test, this gate // is deliberately not driven by `test_targets`: it targets one specific // suite in one specific crate, not "the repo's tests". let target = crate::config::TestTarget { dir: std::path::PathBuf::from("server"), aux_repo: None, features: Vec::new(), all_features: false, scratch_db: true, }; let log_path = ctx.log_path(GateKind::HardeningTest); let log_ref = ctx.log_ref(GateKind::HardeningTest); if let Some(scratch_url) = ctx.cfg.scratch_db_url.as_deref() { clean_stale_test_dbs(scratch_url).await; } let started = std::time::Instant::now(); // Same two-step shape as cargo_test: compile first so a test-target break // reports as a compile error rather than an opaque mass test failure. let mut pre = match cargo_test_command( ctx, &server_dir, &target, &[], &["--no-run", "--test", "integration"], ) .spawn() { Ok(c) => c, Err(e) => { return Ok(GateOutcome::failed(GateFailure::SpawnFailed { message: e.to_string(), }) .with_log_ref(log_ref)); } }; let (pre_out, pre_err, pre_status) = stream_child_to_live_log(&mut pre, ctx.events.clone(), run_id, log_path.clone()).await?; if !pre_status.success() { let failure = classify::classify_compile_error(&pre_out, &pre_err); return Ok(GateOutcome::failed(failure).with_log_ref(log_ref)); } let mut child = match cargo_test_command( ctx, &server_dir, &target, &[], &[ "--test", "integration", "--", "--test-threads=1", "rate_limit", ], ) .spawn() { Ok(c) => c, Err(e) => { return Ok(GateOutcome::failed(GateFailure::SpawnFailed { message: e.to_string(), }) .with_log_ref(log_ref)); } }; let ceiling = std::time::Duration::from_secs(ctx.cfg.gate_timeout_secs); let stream = stream_child_to_live_log(&mut child, ctx.events.clone(), run_id, log_path); let (stdout_buf, stderr_buf, status) = match tokio::time::timeout(ceiling, stream).await { Ok(res) => res?, Err(_elapsed) => { child.start_kill().ok(); let _ = child.wait().await; return Ok(GateOutcome::failed(GateFailure::Timeout { gate: GateKind::HardeningTest, after_s: started.elapsed().as_secs() as u32, }) .with_log_ref(log_ref)); } }; let duration_s = started.elapsed().as_secs() as u32; if status.success() { // A filter that matches nothing exits 0, which would make this gate a // green no-op the day someone renames the tests. Fail closed instead. if tests_run(&stdout_buf) == 0 { return Ok(GateOutcome::failed(GateFailure::Unclassified { legacy_detail: Some( "hardening_test ran 0 tests: the `rate_limit` filter matched nothing. \ The suite was renamed or moved — this gate is proving nothing." .into(), ), }) .with_log_ref(log_ref)); } Ok(GateOutcome::passed(PassNote::TestsPassed { duration_s }).with_log_ref(log_ref)) } else { let failure = classify::classify_cargo_test(&stdout_buf, &stderr_buf); Ok(GateOutcome::failed(failure).with_log_ref(log_ref)) } } /// Count the tests libtest reports as run, from its `test result: ok. N passed` /// summary line. Returns 0 when no summary is present, which is itself the /// "nothing ran" case the caller fails on. fn tests_run(stdout: &[u8]) -> u32 { String::from_utf8_lossy(stdout) .lines() .filter_map(|l| l.trim().strip_prefix("test result:")) .filter_map(|rest| rest.split_once(" passed")) .filter_map(|(head, _)| { head.rsplit(' ') .find(|t| !t.is_empty())? .parse::() .ok() }) .sum() } /// Configure (but don't spawn) `cargo test --release [--features ] /// ` in `dir`, wired to the scratch DB. Shared by the `--no-run` /// pre-gate compile and the full test run so both go through one env setup, /// and by both test gates so they can't drift apart on env. /// /// `cargo_test` passes `fast-tests`, matching what the retired astra CI did: /// it relaxes the auth rate-limit burst (5 → 20) and argon2 cost so /// signup-heavy + lockout workflow tests complete without hitting Governor /// before the hand-rolled lockout check (documented at /// `server/src/constants.rs:87`). `hardening_test` passes no features, which is /// the whole point of that gate — see `GateKind::HardeningTest`. fn cargo_test_command( ctx: &GateCtx, dir: &std::path::Path, target: &crate::config::TestTarget, features: &[&str], extra: &[&str], ) -> Command { let mut cmd = Command::new("cargo"); cmd.args(["test", "--release"]); if target.all_features { cmd.arg("--all-features"); } else if !features.is_empty() { cmd.args(["--features", &features.join(",")]); } cmd.args(extra) .current_dir(dir) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); // Share the build step's target dir so the test compile reuses its // artifacts (and the `--no-run` precompile reuses them again). Must match // `build.rs` or the gate would clean-compile the whole tree a second time. if let Some(target) = ctx.cfg.cargo_target_dir.as_deref() { cmd.env("CARGO_TARGET_DIR", target); } // Same online-mode rationale as the build step: sqlx query macros need a // live DB to type-check against. The scratch DB is left in migrated state // by the preceding build, so we can reuse it here. // // Opt-in per target: setting DATABASE_URL switches sqlx OUT of offline mode, // so a crate that ships `.sqlx` query data would stop using it and try to // type-check against a database that has none of its tables. if let Some(scratch_url) = ctx .cfg .scratch_db_url .as_deref() .filter(|_| target.scratch_db) { cmd.env("DATABASE_URL", scratch_url); // The server test harness (tests/harness/db.rs) parses TEST_DATABASE_URL // with rfind('/'), which mangles URLs whose query string contains '/' // (e.g. `?host=/var/run/postgresql`). Strip the query — libpq defaults // to /var/run/postgresql on Debian/Ubuntu when host is unspecified. let test_url = scratch_url .split_once('?') .map_or(scratch_url, |(base, _)| base); cmd.env("TEST_DATABASE_URL", test_url); } cmd } #[cfg(test)] mod tests { use super::*; use crate::domain::TierId; use crate::events; use crate::gates::testkit::target; use sqlx::sqlite::SqlitePoolOptions; use std::collections::HashMap; #[test] fn name_target_points_a_test_failure_at_its_crate() { // With one target the crate was implicit; with fifteen the operator // needs the summary to say which one broke. let f = name_target( GateFailure::CargoTest { failed_count: 3, first_failed: Some("workflows::sync::round_trip".into()), first_panic: None, }, std::path::Path::new("shared/synckit-client"), ); assert_eq!( f.summary(), "3 test(s) failed; first: shared/synckit-client: workflows::sync::round_trip", ); } #[test] fn name_target_points_a_compile_failure_at_its_crate() { let f = name_target( GateFailure::CompileError { error_count: 1, first_error: Some("error[E0063]".into()), }, std::path::Path::new("mnw-cli"), ); assert_eq!( f.summary(), "compile failed (1 error(s)); first: mnw-cli: error[E0063]" ); } #[test] fn name_target_names_the_crate_even_without_a_test_name() { let f = name_target( GateFailure::CargoTest { failed_count: 2, first_failed: None, first_panic: None, }, std::path::Path::new("pom"), ); assert_eq!(f.summary(), "2 test(s) failed; first: pom"); } #[test] fn name_target_leaves_unrelated_failures_alone() { let f = name_target( GateFailure::SpawnFailed { message: "no cargo".into(), }, std::path::Path::new("pom"), ); assert!(matches!(f, GateFailure::SpawnFailed { .. })); } #[tokio::test] async fn cargo_test_fails_closed_when_no_target_exists_in_the_worktree() { // A worktree missing every configured crate must not report "tests // passed" having run none. Uses an empty tempdir as the worktree, so // no cargo process is ever spawned. let tmp = tempfile::tempdir().unwrap(); let mut cfg = crate::config::AppConfig::for_tests(); cfg.test_targets = vec![target("server"), target("mnw-cli")]; cfg.logs_root = tmp.path().join("logs"); let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .unwrap(); let ctx = GateCtx { public_url: None, pool, cfg: std::sync::Arc::new(cfg), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(tmp.path().to_path_buf()), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), }; let out = cargo_test(&ctx, GateRunId(1)).await.unwrap(); assert_eq!( out.status_str(), "failed", "green here would be a silent no-op gate" ); let crate::outcome::GateStatus::Failed { failure } = &out.status else { panic!("expected a failure") }; assert!( failure.summary().contains("ran no targets"), "got: {}", failure.summary() ); } #[tokio::test] async fn scratch_db_env_is_opt_in_per_target() { // Exporting DATABASE_URL knocks sqlx out of offline mode, so a crate // shipping .sqlx data must not see it. let mut cfg = crate::config::AppConfig::for_tests(); cfg.scratch_db_url = Some("postgres://sando@127.0.0.1/sando_scratch".into()); let ctx = GateCtx { public_url: None, pool: SqlitePoolOptions::new() .max_connections(1) .connect_lazy("sqlite::memory:") .unwrap(), cfg: std::sync::Arc::new(cfg), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(std::path::PathBuf::from("/tmp/wt")), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), }; let dir = std::path::Path::new("/tmp/wt/x"); let off = cargo_test_command(&ctx, dir, &target("x"), &[], &[]); let has_db = |c: &Command| { c.as_std() .get_envs() .any(|(k, v)| k == "DATABASE_URL" && v.is_some()) }; assert!(!has_db(&off), "scratch_db defaults off"); let mut on_target = target("x"); on_target.scratch_db = true; assert!( has_db(&cargo_test_command(&ctx, dir, &on_target, &[], &[])), "opt-in exports it" ); } #[tokio::test] async fn all_features_replaces_the_feature_list() { let ctx = GateCtx { public_url: None, pool: SqlitePoolOptions::new() .max_connections(1) .connect_lazy("sqlite::memory:") .unwrap(), cfg: std::sync::Arc::new(crate::config::AppConfig::for_tests()), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(std::path::PathBuf::from("/tmp/wt")), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), }; let mut t = target("shared/ops-exec"); t.all_features = true; let cmd = cargo_test_command(&ctx, std::path::Path::new("/tmp/wt"), &t, &[], &[]); let args: Vec<_> = cmd .as_std() .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!(args.iter().any(|a| a == "--all-features"), "got: {args:?}"); assert!(!args.iter().any(|a| a == "--features"), "got: {args:?}"); } #[test] fn first_meaningful_line_prefers_the_diagnostic() { let stderr = b" Updating crates.io index\nerror: 1 vulnerability found!\n"; assert_eq!( first_meaningful_line(b"", stderr), "error: 1 vulnerability found!" ); } #[test] fn first_meaningful_line_finds_a_deny_verdict() { let out = b"advisories FAILED, bans ok, licenses FAILED, sources ok\n"; assert!(first_meaningful_line(out, b"").contains("FAILED")); } #[test] fn first_meaningful_line_falls_back_rather_than_returning_empty() { assert!(first_meaningful_line(b"", b"").contains("see the gate log")); } #[tokio::test] async fn supply_chain_gates_skip_crates_without_their_config() { // Four crates in this repo fail `cargo audit` purely for want of a // triaged .cargo/audit.toml. Running it there would make the gate // permanently red, so a target only qualifies once it carries the file. // The worktree here has a Cargo.toml but no audit config, so nothing // qualifies and the gate fails closed rather than passing over zero work. let tmp = tempfile::tempdir().unwrap(); let crate_dir = tmp.path().join("server"); std::fs::create_dir_all(&crate_dir).unwrap(); std::fs::write(crate_dir.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap(); let mut cfg = crate::config::AppConfig::for_tests(); cfg.test_targets = vec![target("server")]; cfg.logs_root = tmp.path().join("logs"); let ctx = GateCtx { public_url: None, pool: SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .unwrap(), cfg: std::sync::Arc::new(cfg), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(tmp.path().to_path_buf()), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), }; let out = supply_chain(&ctx, GateRunId(1), GateKind::CargoAudit) .await .unwrap(); assert_eq!(out.status_str(), "failed"); let crate::outcome::GateStatus::Failed { failure } = &out.status else { panic!("expected a failure") }; assert!( failure.summary().contains("ran nothing"), "got: {}", failure.summary() ); // Drop the config in and the same target now qualifies. std::fs::create_dir_all(crate_dir.join(".cargo")).unwrap(); std::fs::write(crate_dir.join(".cargo/audit.toml"), "[advisories]\n").unwrap(); // The fixture crate is not a real cargo project, so the tool itself // still errors — but on its own terms, not with "ran nothing". That // distinction is the thing under test: the target was attempted. let out = supply_chain(&ctx, GateRunId(2), GateKind::CargoAudit) .await .unwrap(); if let crate::outcome::GateStatus::Failed { failure } = &out.status { assert!( !failure.summary().contains("ran nothing"), "a target carrying the config must be attempted, not skipped; got: {}", failure.summary(), ); } } #[test] fn tests_run_reads_the_libtest_summary() { let out = b"running 6 tests\ntest auth_rate_limit_triggers_on_burst ... ok\n\n\ test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 118 filtered out\n"; assert_eq!(tests_run(out), 6); } #[test] fn tests_run_sums_across_test_binaries() { let out = b"test result: ok. 6 passed; 0 failed\ntest result: ok. 2 passed; 0 failed\n"; assert_eq!(tests_run(out), 8); } #[test] fn tests_run_is_zero_when_the_filter_matched_nothing() { // The case hardening_test fails closed on: a filter matching no tests // exits 0, so a rename would otherwise make the gate a green no-op. let out = b"running 0 tests\n\n\ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 124 filtered out\n"; assert_eq!(tests_run(out), 0); } #[test] fn tests_run_is_zero_without_a_summary_line() { assert_eq!(tests_run(b"error: could not compile `makenotwork`\n"), 0); } #[tokio::test] async fn hardening_test_command_carries_no_features() { // The entire point of the gate: production constants, which means no // `fast-tests`. A stray feature here silently restores the blind spot. let ctx = GateCtx { public_url: None, pool: SqlitePoolOptions::new() .max_connections(1) .connect_lazy("sqlite::memory:") .unwrap(), cfg: std::sync::Arc::new(crate::config::AppConfig::for_tests()), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(std::path::PathBuf::from("/tmp/wt")), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), }; let plain = crate::config::TestTarget { dir: std::path::PathBuf::from("server"), aux_repo: None, features: Vec::new(), all_features: false, scratch_db: true, }; let cmd = cargo_test_command( &ctx, std::path::Path::new("/tmp/wt/server"), &plain, &[], &["--test", "integration"], ); let args: Vec<_> = cmd .as_std() .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!( !args.iter().any(|a| a == "--features"), "hardening_test must pass no features: {args:?}" ); assert!( !args.iter().any(|a| a.contains("fast-tests")), "got: {args:?}" ); let fast_target = crate::config::TestTarget { dir: std::path::PathBuf::from("server"), aux_repo: None, features: vec!["fast-tests".into()], all_features: false, scratch_db: true, }; let fast = cargo_test_command( &ctx, std::path::Path::new("/tmp/wt/server"), &fast_target, &["fast-tests"], &[], ); let fast_args: Vec<_> = fast .as_std() .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!( fast_args .windows(2) .any(|w| w == ["--features", "fast-tests"]), "got: {fast_args:?}" ); } }