pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! `inputs = "machine"` — the floor row whose verdict depends on what else the
//! machine is doing.
//!
//! F76's addendum: `make floor` went RED on `bench` (836/1) with cargo builds
//! running concurrently and green (837/0) on the same tree quiet, warm `p99`
//! moving 109.7ms to 47.5ms between the two. `[floor]` already had the
//! vocabulary for exactly this — `inputs = repo | toolchain | network` plus a
//! disclosure printed only when it applies (F69's rider, built for `cargo
//! deny`'s `RustSec` dependency) — and `bench` declared `toolchain`, so the one
//! row that depends on machine state was the row claiming to depend only on the
//! toolchain.
//!
//! **The two disclosures must not share wording**, which is why a test pins it.
//! The network sentence explains time-variance — *"an unchanged commit can
//! newly fail when upstream data changes"* — and that reason is simply false for
//! load. Two causes reading as one line is how a reader learns to skip both.
//!
//! **A committed suite already depends on the separation.**
//! `floor_verb.rs:359` asserts a toolchain-only floor prints nothing containing
//! "unchanged commit". Distinct wording keeps that assertion and this file
//! independent instead of coupling them through a shared string.
//!
//! A NEW file per N10; no committed suite is touched. Fixtures use `git
//! --version` for the same reason `floor_verb.rs` does — portable, already a
//! test dependency, and nothing here shells out to cargo.

use assert_cmd::Command;
use std::fs;
use std::path::Path;

type TestResult = Result<(), Box<dyn std::error::Error>>;

const BASE: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"

[gates]
"#;

const MACHINE_ROW: &str = r#"
[[floor.commands]]
name = "latency"
run = ["git", "--version"]
scope = "whole_repo"
inputs = "machine"
"#;

const TOOLCHAIN_ONLY: &str = r#"
[[floor.commands]]
name = "version"
run = ["git", "--version"]
scope = "whole_repo"
inputs = "toolchain"
"#;

const NETWORK_ROW: &str = r#"
[[floor.commands]]
name = "advisories"
run = ["git", "--version"]
scope = "whole_repo"
inputs = "network"
"#;

fn repo(floor: &str) -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
    let dir = tempfile::tempdir()?;
    fs::write(dir.path().join("pushkin.toml"), format!("{BASE}{floor}"))?;
    Ok(dir)
}

struct Run {
    code: i32,
    out: String,
}

fn floor(dir: &Path) -> Result<Run, Box<dyn std::error::Error>> {
    let output = Command::cargo_bin("pushkin")?
        .current_dir(dir)
        .arg("floor")
        .output()?;
    Ok(Run {
        code: output.status.code().unwrap_or(-1),
        out: format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        ),
    })
}

/// The line carrying a disclosure, if the run printed one.
fn line_containing<'a>(out: &'a str, needle: &str) -> Option<&'a str> {
    out.lines().find(|line| line.contains(needle))
}

#[test]
fn the_machine_class_parses_and_is_reported_per_command() -> TestResult {
    let dir = repo(MACHINE_ROW)?;
    let run = floor(dir.path())?;
    assert_eq!(run.code, 0, "output:\n{}", run.out);
    assert!(
        run.out.contains("machine"),
        "the per-command line must report the inputs class, got:\n{}",
        run.out
    );
    Ok(())
}

#[test]
fn a_machine_inputs_command_prints_the_load_variance_disclosure() -> TestResult {
    let dir = repo(MACHINE_ROW)?;
    let run = floor(dir.path())?;
    let disclosure = line_containing(&run.out, "quiet").unwrap_or_default();
    assert!(
        disclosure.contains("latency"),
        "the disclosure must name the load-dependent command, got:\n{}",
        run.out
    );
    assert!(
        disclosure.to_lowercase().contains("machine"),
        "the disclosure must say what the dependency IS, got:\n{}",
        run.out
    );
    Ok(())
}

#[test]
fn a_floor_with_no_machine_command_prints_no_load_disclosure() -> TestResult {
    // A disclosure that always prints is noise, and noise is what gets ignored.
    let dir = repo(TOOLCHAIN_ONLY)?;
    let run = floor(dir.path())?;
    assert!(
        line_containing(&run.out, "quiet").is_none(),
        "no machine inputs means no load disclosure, got:\n{}",
        run.out
    );
    Ok(())
}

#[test]
fn the_load_and_network_disclosures_are_separate_lines() -> TestResult {
    // Two different causes of a non-reproducible verdict. Collapsing them into
    // one sentence would make the network reason ("upstream data changed") read
    // as the explanation for a latency flake, which it is not.
    let dir = repo(&format!("{MACHINE_ROW}{NETWORK_ROW}"))?;
    let run = floor(dir.path())?;
    let load = line_containing(&run.out, "quiet");
    let network = line_containing(&run.out, "upstream data");
    assert!(
        load.is_some(),
        "the load disclosure is missing:\n{}",
        run.out
    );
    assert!(
        network.is_some(),
        "the network disclosure is missing:\n{}",
        run.out
    );
    assert_ne!(
        load, network,
        "the two disclosures must be distinct lines, got:\n{}",
        run.out
    );
    Ok(())
}

#[test]
fn the_repo_declares_bench_as_machine_dependent() -> TestResult {
    // The fix is only real if this repo's own table uses it. `bench` is the row
    // that measures wall-clock latency; it is the reason the class exists.
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../pushkin.toml");
    let text = fs::read_to_string(&manifest)?;
    let bench = text
        .split("[[floor.commands]]")
        .find(|block| block.contains("name = \"bench\""))
        .unwrap_or_default()
        .to_owned();
    assert!(
        !bench.is_empty(),
        "the repo manifest has no `bench` floor command"
    );
    assert!(
        bench.contains("inputs = \"machine\""),
        "bench measures wall-clock latency and must declare it, got:\n{bench}"
    );
    Ok(())
}