car-server-core 0.47.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
//! Does this session's diff touch the files its own contract executes?
//!
//! The outcome contract is the automated trust boundary, and its *derivation* is
//! already well hardened: [`OutcomeContract::validate`] rejects assertion-less
//! checks, toolchain-only no-ops and placeholder names; `strip_exit_masking_pipes`
//! blocks pipelines that always exit 0; and the derivation prompt encodes a
//! dogfooded anti-hacking rule about `-k` filters and bespoke reproduction
//! snippets.
//!
//! What remains is that the agent can edit the test files the contract runs.
//! `coder::policy` addresses that deliberately and correctly:
//!
//! > "Deliberately scoped to what is mechanically decidable. `conftest.py`,
//! > `pyproject.toml`, `tox.ini`, and `setup.cfg` are NOT denied: editing them is
//! > often the actual task, and no matcher can separate 'add a fixture' from
//! > 'change how tests run'."
//!
//! **This module does not change that.** Denying test-adjacent edits would break
//! the many sessions where editing tests *is* the task. But denial is not the
//! only option: *disclosure* is mechanically decidable and was simply absent. If
//! check `run_failing_test` runs `pytest tests/test_x.py` and the diff modifies
//! `tests/test_x.py`, that is a fact computable from data already in hand — and
//! it was never computed, let alone surfaced at the approval gate.
//!
//! ## Why the extraction errs toward flagging
//!
//! Pulling paths out of a shell command is a heuristic; there is no parse that
//! is both general and exact. So it follows `policy.rs`'s established posture —
//! conservative token matching, biased toward reporting — because the costs are
//! wildly asymmetric: a false positive costs one line of disclosure that a human
//! dismisses in a second, while a false negative silently restores the status
//! quo this exists to fix.

use serde::{Deserialize, Serialize};

use super::contract::{ContractCheck, OutcomeContract};

/// One check whose command references paths the session's diff also touched.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CheckOverlap {
    /// The contract check that executes the path(s).
    pub check: String,
    /// Changed paths this check's command references, sorted, deduped.
    pub paths: Vec<String>,
}

/// Source-ish extensions worth treating as a path even without a `/`, so a
/// bare `pytest test_x.py` at the repo root is still matched.
const PATHY_EXTENSIONS: [&str; 12] = [
    ".py", ".rs", ".ts", ".tsx", ".js", ".jsx", ".go", ".java", ".rb", ".swift", ".kt", ".c",
];

/// Strip shell noise from one token so it can be compared to a repo path:
/// surrounding quotes, a pytest `::node_id` selector, a trailing `:line`, and
/// `./` prefixes.
fn normalize_token(raw: &str) -> Option<String> {
    let mut t = raw.trim();
    // Quotes may wrap the whole token (`"tests/test_x.py"`), including the
    // combined form `'tests/test_x.py::test_a'`.
    t = t.trim_matches(|c| c == '"' || c == '\'');
    // pytest node ids and `file:line` references both name a real file first.
    if let Some((head, _)) = t.split_once("::") {
        t = head;
    }
    // Only split a trailing `:<digits>` — a Windows-style `C:\...` or a URL
    // must not be mangled.
    if let Some((head, tail)) = t.rsplit_once(':') {
        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
            t = head;
        }
    }
    t = t.trim_start_matches("./");
    // Decide path-likeness BEFORE trimming the trailing slash: `tests/` is
    // obviously a path, but the trimmed `tests` has neither a separator nor an
    // extension, so testing after the trim silently dropped every bare
    // directory argument (`pytest tests/`, `ruff check src/`) — the exact
    // reference that most needs matching, since it covers a whole tree.
    let lower = t.to_ascii_lowercase();
    let pathy = t.contains('/') || PATHY_EXTENSIONS.iter().any(|e| lower.ends_with(e));
    t = t.trim_end_matches('/');
    if t.is_empty() || t.starts_with('-') {
        return None;
    }
    pathy.then(|| t.to_string())
}

/// Path-like tokens referenced by a check's command.
///
/// Deliberately dumb: split on shell word boundaries and keep what looks like a
/// path. It does not resolve variables, globs, or `&&`-chained subcommands
/// beyond treating them as separators — anything cleverer would be guessing.
pub fn referenced_paths(command: &str) -> Vec<String> {
    let mut out: Vec<String> = command
        .split(|c: char| c.is_whitespace() || matches!(c, '|' | ';' | '(' | ')' | '&'))
        .filter_map(normalize_token)
        .collect();
    out.sort();
    out.dedup();
    out
}

/// Whether `changed` is the same file as, or lives under, `referenced`.
///
/// Directory containment is what makes `pytest tests/` match a change to
/// `tests/test_x.py`. The `/` guard keeps `tests_helpers/x.py` from matching a
/// reference to `tests`.
fn covers(referenced: &str, changed: &str) -> bool {
    changed == referenced || changed.starts_with(&format!("{referenced}/"))
}

/// Which of `changed_paths` each contract check executes.
///
/// Returns only checks with at least one overlapping path, so an empty result
/// means "the diff touched nothing the contract runs".
pub fn contract_overlap(contract: &OutcomeContract, changed_paths: &[String]) -> Vec<CheckOverlap> {
    contract
        .checks
        .iter()
        .filter_map(|check| {
            let overlap = overlapping_paths(check, changed_paths);
            (!overlap.is_empty()).then(|| CheckOverlap {
                check: check.name.clone(),
                paths: overlap,
            })
        })
        .collect()
}

fn overlapping_paths(check: &ContractCheck, changed_paths: &[String]) -> Vec<String> {
    let referenced = referenced_paths(&check.command);
    let mut hits: Vec<String> = changed_paths
        .iter()
        .filter(|changed| referenced.iter().any(|r| covers(r, changed)))
        .cloned()
        .collect();
    hits.sort();
    hits.dedup();
    hits
}

/// One-line disclosure for the approval surface. `None` when nothing overlaps.
///
/// Phrased as a fact, not an accusation: editing the tests is frequently the
/// task, and the human at the gate is the one positioned to judge which case
/// this is.
pub fn disclosure(overlap: &[CheckOverlap]) -> Option<String> {
    if overlap.is_empty() {
        return None;
    }
    let parts: Vec<String> = overlap
        .iter()
        .map(|o| format!("{} (run by check `{}`)", o.paths.join(", "), o.check))
        .collect();
    Some(format!(
        "This session modified files that its own contract executes: {}. \
         That is often legitimate — editing tests is frequently the task — but it \
         means those checks are not an independent verdict on this change.",
        parts.join("; ")
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn check(name: &str, command: &str) -> ContractCheck {
        ContractCheck {
            name: name.into(),
            command: command.into(),
            expect_exit_zero: true,
            output_contains: None,
            timeout_secs: 120,
        }
    }

    fn contract(checks: Vec<ContractCheck>) -> OutcomeContract {
        OutcomeContract {
            description: "d".into(),
            checks,
        }
    }

    #[test]
    fn extracts_paths_and_ignores_flags_and_bare_words() {
        let paths = referenced_paths("python -m pytest -q tests/test_x.py --maxfail=1");
        assert_eq!(paths, vec!["tests/test_x.py"]);
    }

    #[test]
    fn strips_pytest_node_ids_quotes_and_line_suffixes() {
        assert_eq!(
            referenced_paths("pytest 'tests/test_x.py::test_a'"),
            vec!["tests/test_x.py"]
        );
        assert_eq!(
            referenced_paths("pytest \"tests/test_x.py\""),
            vec!["tests/test_x.py"]
        );
        assert_eq!(referenced_paths("vet src/main.rs:42"), vec!["src/main.rs"]);
    }

    #[test]
    fn a_bare_source_file_counts_even_without_a_separator() {
        assert_eq!(referenced_paths("pytest test_x.py"), vec!["test_x.py"]);
        // ...but a plain word is not a path, or every command would "reference"
        // its own program name.
        assert!(referenced_paths("cargo test").is_empty());
    }

    #[test]
    fn splits_on_shell_operators_so_chained_commands_are_covered() {
        let paths = referenced_paths("cargo build && pytest tests/test_x.py | tee out.log");
        assert!(paths.contains(&"tests/test_x.py".to_string()));
    }

    /// The issue's worked example: a check runs `tests/test_x.py` and the diff
    /// edits that file.
    #[test]
    fn a_diff_touching_a_contract_executed_file_is_flagged() {
        let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
        let changed = vec!["tests/test_x.py".to_string(), "src/lib.rs".to_string()];

        let overlap = contract_overlap(&c, &changed);
        assert_eq!(
            overlap,
            vec![CheckOverlap {
                check: "run_failing_test".into(),
                paths: vec!["tests/test_x.py".into()],
            }]
        );
        assert!(disclosure(&overlap).unwrap().contains("tests/test_x.py"));
    }

    /// ...and the other half of the acceptance criterion: a session that only
    /// edits `src/` is not flagged.
    #[test]
    fn a_diff_touching_only_source_is_not_flagged() {
        let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
        let changed = vec!["src/lib.rs".to_string(), "src/main.rs".to_string()];
        assert!(contract_overlap(&c, &changed).is_empty());
        assert!(disclosure(&[]).is_none());
    }

    /// A directory reference covers files beneath it — `pytest tests/` does run
    /// `tests/test_x.py`.
    #[test]
    fn a_directory_reference_covers_files_under_it() {
        let c = contract(vec![check("suite", "pytest tests/")]);
        let changed = vec!["tests/unit/test_x.py".to_string()];
        assert_eq!(
            contract_overlap(&c, &changed)[0].paths,
            vec!["tests/unit/test_x.py"]
        );
    }

    /// ...but only on a real segment boundary, or a sibling directory with a
    /// shared prefix would be a false positive.
    #[test]
    fn a_prefix_that_is_not_a_path_segment_does_not_match() {
        let c = contract(vec![check("suite", "pytest tests/")]);
        let changed = vec!["tests_helpers/util.py".to_string()];
        assert!(contract_overlap(&c, &changed).is_empty());
    }

    #[test]
    fn several_checks_each_report_their_own_paths() {
        let c = contract(vec![
            check("unit", "pytest tests/test_x.py"),
            check("lint", "ruff check src/"),
            check("unrelated", "cargo build"),
        ]);
        let changed = vec![
            "tests/test_x.py".to_string(),
            "src/app.py".to_string(),
            "README.md".to_string(),
        ];

        let overlap = contract_overlap(&c, &changed);
        assert_eq!(
            overlap.len(),
            2,
            "the `cargo build` check references no path"
        );
        assert_eq!(overlap[0].check, "unit");
        assert_eq!(overlap[0].paths, vec!["tests/test_x.py"]);
        assert_eq!(overlap[1].check, "lint");
        assert_eq!(overlap[1].paths, vec!["src/app.py"]);
    }

    #[test]
    fn disclosure_reads_as_a_fact_not_an_accusation() {
        let overlap = vec![CheckOverlap {
            check: "unit".into(),
            paths: vec!["tests/test_x.py".into()],
        }];
        let text = disclosure(&overlap).unwrap();
        assert!(text.contains("often legitimate"));
        assert!(text.contains("not an independent verdict"));
    }
}