Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

probe/tests/matrix_launch_shape.rs

//! End-to-end tests of the probe **as the install matrix actually launches it**.
//!
//! The unit tests inside `skidbladnir-probe.rs` cover the pure predicates
//! (`exec_matches`, `is_self_or_ancestor`, `running_verdict`) in isolation. They
//! could all have passed while the shipped binary still lied, because the defect
//! was not in any one predicate — it was in the *composition* of three of them
//! under one specific launch shape:
//!
//! ```text
//! matrix.rs:206  sh -c "sleep 4; skidbladnir-probe … --exec /usr/local/bin/skidbladnir-testsvc …"
//! ```
//!
//! The `--exec` path appears on the **launching shell's** command line. The old
//! `proc_scan` matched any cmdline *containing* that string and skipped only its
//! own pid, so it found the shell, in every cell, always — and the verdict then
//! let that hit override an init system that had said "stopped".
//!
//! So these tests run the REAL binary, from a REAL `sh -c`, and read the JSON the
//! matrix actually parses. They are hermetic: no systemd, no root, no KVM. Both
//! use a backend slug the probe cannot query (`init_status` → `(None, None)`),
//! which is the *most permissive* configuration — the one where the scan is
//! allowed to decide. If the scan is honest here, it is honest everywhere.

use std::net::{TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

const PROBE: &str = env!("CARGO_BIN_EXE_skidbladnir-probe");
const TESTSVC: &str = env!("CARGO_BIN_EXE_skidbladnir-testsvc");

/// A backend slug `init_status` has no branch for, so it returns `(None, None)`
/// and the `/proc` scan is the only evidence — deliberately the weakest case.
const UNQUERYABLE: &str = "sysvinit-unknown";

/// Pull a top-level field out of the probe's one-line JSON.
///
/// The probe emits with `format!` (std-only, no serde), so the shape is fixed and
/// a positional read is faithful. Kept dumb on purpose: a JSON parser in the test
/// would be a second implementation to get wrong.
fn field<'a>(json: &'a str, key: &str) -> &'a str {
    let pat = format!("\"{key}\":");
    let start = json
        .find(&pat)
        .unwrap_or_else(|| panic!("field {key:?} missing from probe output: {json}"))
        + pat.len();
    let rest = &json[start..];
    let end = rest.find([',', '}']).unwrap_or(rest.len());
    rest[..end].trim().trim_matches('"')
}

/// Run the probe THE WAY THE MATRIX DOES — through `sh -c`, so the parent shell's
/// cmdline contains the `--exec` path — and return the JSON line it prints.
fn probe_via_sh(exec: &str, port: Option<u16>) -> String {
    let mut cmd = format!(
        "{PROBE} --service skidbladnir-testsvc --backend {UNQUERYABLE} \
         --os test-host --exec {exec}"
    );
    if let Some(p) = port {
        cmd.push_str(&format!(" --port {p} --health /health"));
    }
    let out = Command::new("sh")
        .arg("-c")
        .arg(&cmd)
        .output()
        .expect("run the probe under sh -c");
    assert!(out.status.success(), "probe exited {:?}", out.status);
    let s = String::from_utf8(out.stdout).expect("probe emits utf-8");
    let line = s.lines().next().unwrap_or_default().to_string();
    assert!(line.starts_with('{'), "probe must emit one JSON line, got: {s:?}");
    line
}

/// A free localhost port. Bound then dropped, so the service can take it.
fn free_port() -> u16 {
    TcpListener::bind("127.0.0.1:0").expect("bind ephemeral").local_addr().unwrap().port()
}

fn wait_until_listening(port: u16) {
    let deadline = Instant::now() + Duration::from_secs(10);
    while Instant::now() < deadline {
        if TcpStream::connect_timeout(
            &format!("127.0.0.1:{port}").parse().unwrap(),
            Duration::from_millis(200),
        )
        .is_ok()
        {
            return;
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    panic!("skidbladnir-testsvc never listened on {port}");
}

/// Kills the child on drop so a failing assertion cannot leak a listener that
/// then poisons the next run of the suite.
struct Reaped(Child);
impl Drop for Reaped {
    fn drop(&mut self) {
        let _ = self.0.kill();
        let _ = self.0.wait();
    }
}

// ─────────────────────────── the regression ──────────────────────────────────

/// **THE RED-PROVE, automated.** Nothing on this machine is running
/// `/nonexistent/skidbladnir-ghost` — it does not even exist. The only processes
/// whose command lines mention it are the probe itself and the `sh -c` that
/// launched it.
///
/// Pre-fix, this exact invocation printed `"running":true` with the *shell's*
/// pid. That is the constant-green: `--exec` appearing as an ARGUMENT of an
/// ancestor was read as proof the service was up.
#[test]
fn an_exec_that_only_the_launching_shell_mentions_is_not_running() {
    let json = probe_via_sh("/nonexistent/skidbladnir-ghost", None);
    assert_eq!(
        field(&json, "running"),
        "false",
        "no process runs this exec — only our own launching shell names it. \
         Pre-fix this said `true` (the constant green). Output: {json}"
    );
    assert_eq!(
        field(&json, "pid"),
        "null",
        "and it must not report the shell's pid as the service's. Output: {json}"
    );
}

/// The other half of the same defect: even when a service IS genuinely up, the old
/// scan returned the FIRST cmdline match in `/proc` readdir order — which was the
/// launching shell, not the service. So the pid the matrix recorded as ground
/// truth belonged to the wrong process, and any pid-based follow-up (logs, kill,
/// stale-binary check) was chasing a shell.
///
/// This also guards the opposite failure mode: a predicate tightened so far that
/// it matches nothing would turn every cell red, which is a different lie.
#[test]
fn a_genuinely_running_service_is_found_by_its_own_pid_not_the_shells() {
    let port = free_port();
    // Canonicalised so the comparison against the kernel's resolved
    // `/proc/<pid>/exe` is not defeated by a symlink in the target dir path.
    let exec = std::fs::canonicalize(TESTSVC).expect("canonicalize testsvc");
    let exec = exec.to_str().expect("utf-8 path");

    let child = Reaped(
        Command::new(exec)
            .args(["--port", &port.to_string(), "--name", "skidtest"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .expect("spawn testsvc"),
    );
    let real_pid = child.0.id();
    wait_until_listening(port);

    let json = probe_via_sh(exec, Some(port));
    assert_eq!(field(&json, "running"), "true", "the service IS up: {json}");
    assert_eq!(
        field(&json, "pid"),
        real_pid.to_string(),
        "the reported pid must be the SERVICE ({real_pid}), not the `sh -c` that \
         launched the probe. Output: {json}"
    );
    assert_eq!(field(&json, "ok"), "true", "health must be green: {json}");
}

// ─────────────────────────── contract guards ─────────────────────────────────

/// An absent `--exec` must not degrade into "match anything". The old scan built
/// its basename from an empty string, so `cmdline.contains("")` was true for the
/// very first pid it read.
#[test]
fn an_absent_exec_reports_not_running_rather_than_matching_everything() {
    let out = Command::new("sh")
        .arg("-c")
        .arg(format!("{PROBE} --service s --backend {UNQUERYABLE} --os test-host"))
        .output()
        .expect("run probe without --exec");
    let s = String::from_utf8(out.stdout).unwrap();
    let json = s.lines().next().unwrap_or_default();
    assert_eq!(field(json, "running"), "false", "no --exec => no evidence => red: {json}");
    assert_eq!(field(json, "pid"), "null");
}

/// `--version` must answer in a bare guest AND must not emit a JSON status line —
/// the matrix parses stdout, so a stray `{...}` here would be read as a cell
/// report. Guards the artifact-identity law (a probe that is trusted as ground
/// truth must be able to say which commit built it).
#[test]
fn version_answers_without_emitting_a_report_line() {
    for flag in ["--version", "-V"] {
        let out = Command::new(PROBE).arg(flag).output().expect("run --version");
        assert!(out.status.success());
        let s = String::from_utf8(out.stdout).unwrap();
        assert!(s.contains("skidbladnir-probe"), "{flag} must name the binary, got {s:?}");
        assert!(!s.contains("\"running\""), "{flag} must NOT emit a report line, got {s:?}");
        assert!(s.contains('('), "{flag} must carry the git hash, got {s:?}");
    }
}

/// The report the matrix parses must carry every field `ProbeReport` deserialises
/// — a silently renamed/dropped field would surface as `NoReport`, i.e. a red the
/// matrix blames on the guest rather than on the probe.
#[test]
fn the_report_line_carries_the_whole_contract() {
    let json = probe_via_sh("/nonexistent/skidbladnir-ghost", None);
    for key in
        ["service", "backend", "os", "running", "pid", "ports", "health", "logs_tail", "asserted_at"]
    {
        assert!(json.contains(&format!("\"{key}\":")), "field {key:?} missing from {json}");
    }
}