gy 1.2.0

good,yes — the ledger of what work rests on before a requirement is confirmed
#![allow(dead_code)]
//! Shared helpers: a repository root with a gy.toml, its XDG data directory,
//! and running the gy binary against them.
use gy_ledger::{
    DecisionScope, FileStore, FormatVersion, Node, NodeId, NodeKind, Repository, format, location,
};
use std::path::PathBuf;
use std::process::{Command, Output};

pub const DATE: &str = "2026-09-15T00:00:00Z";

pub fn node_id(kind: NodeKind, hash: &str) -> NodeId {
    NodeId::from_hash(kind, hash).unwrap()
}

pub fn need(hash: &str, title: &str) -> Node {
    Node::need(node_id(NodeKind::Need, hash), "a", DATE, title).unwrap()
}

pub fn criterion(hash: &str, title: &str) -> Node {
    Node::criterion(node_id(NodeKind::Criterion, hash), "a", DATE, title).unwrap()
}

pub fn question(hash: &str, title: &str) -> Node {
    Node::question(node_id(NodeKind::Question, hash), "a", DATE, title).unwrap()
}

pub fn decision(hash: &str, title: &str) -> Node {
    Node::decision(
        node_id(NodeKind::Decision, hash),
        "a",
        DATE,
        title,
        DecisionScope::recorded("scope").unwrap(),
    )
    .unwrap()
}

pub struct Fixture {
    _temp: tempfile::TempDir,
    pub root: PathBuf,
    pub data: PathBuf,
}

pub fn fixture() -> Fixture {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("repo");
    std::fs::create_dir_all(&root).unwrap();
    std::fs::write(root.join("gy.toml"), "[scopes.a]\n").unwrap();
    let data = temp.path().join("data");
    std::fs::create_dir_all(&data).unwrap();
    Fixture {
        _temp: temp,
        root,
        data,
    }
}

pub fn stdout(output: &Output) -> String {
    String::from_utf8(output.stdout.clone()).unwrap()
}

pub fn stderr(output: &Output) -> String {
    String::from_utf8(output.stderr.clone()).unwrap()
}

/// The id a write printed on its first line: `id: <ID>` for a creating write
/// (n-16c8, r-045a), the bare id for the others.
pub fn id_of(output: &Output) -> String {
    let text = stdout(output);
    let line = text.lines().next().unwrap_or_default();
    let id = line.strip_prefix("id: ").unwrap_or(line);
    id.split_whitespace().next().unwrap_or_default().to_string()
}

/// Write a need and an approved requirement that target `criterion`, so the
/// criterion's satisfy is admitted (n-f921). Returns the requirement's id.
pub fn cover(fx: &Fixture, criterion: &str) -> String {
    let need = id_of(&fx.run(&["need", "add", "a need", "--targets", criterion]));
    let request = id_of(&fx.run(&[
        "req",
        "add",
        "a requirement",
        "--need",
        &need,
        "--targets",
        criterion,
    ]));
    let approved = fx.run(&[
        "req",
        "approve",
        &request,
        "--design",
        "d",
        "--heard-by",
        "master",
        "--evidence",
        "e",
    ]);
    assert!(approved.status.success(), "{}", stderr(&approved));
    request
}

impl Fixture {
    pub fn ledger(&self) -> PathBuf {
        location::dir_in(&self.data, &self.root)
    }

    /// Create the ledger and write nodes, as an earlier write would have.
    pub fn seed(&self, nodes: &[Node]) {
        let ledger = self.ledger();
        format::write(&ledger, FormatVersion::CURRENT).unwrap();
        let store = FileStore::open_with(&ledger, |_| Some("piko".into())).unwrap();
        let mut repository = Repository::new(store);
        repository
            .transaction("seed", "test", |repository| {
                for node in nodes {
                    repository.put(node)?;
                }
                Ok(())
            })
            .unwrap();
    }

    pub fn run(&self, args: &[&str]) -> Output {
        self.command(args).output().unwrap()
    }

    pub fn run_without_actor(&self, args: &[&str]) -> Output {
        self.command(args).env_remove("GY_ACTOR").output().unwrap()
    }

    fn command(&self, args: &[&str]) -> Command {
        let mut command = Command::new(env!("CARGO_BIN_EXE_gy"));
        command
            .current_dir(&self.root)
            .env("XDG_DATA_HOME", &self.data)
            .env("GY_ACTOR", "piko")
            .args(args);
        command
    }
}

/// The identity the child git works under, and the only git config it can see.
/// A write to a shared copy reads `git config user.name`, and git itself needs
/// a name it can put on a commit; a developer's machine supplies both and a CI
/// runner supplies neither, so the test carries its own (n-8f60).
pub fn ident() {
    static ONCE: std::sync::Once = std::sync::Once::new();
    ONCE.call_once(|| unsafe {
        let path = std::env::temp_dir().join(format!("gy-ident-{}", std::process::id()));
        std::fs::write(&path, "[user]\n\tname = piko\n\temail = piko@example.com\n").unwrap();
        for (key, value) in [
            ("GIT_CONFIG_GLOBAL", path.display().to_string()),
            ("GIT_CONFIG_SYSTEM", "/dev/null".to_string()),
            ("GIT_CONFIG_NOSYSTEM", "1".to_string()),
            ("GIT_AUTHOR_NAME", "piko".to_string()),
            ("GIT_AUTHOR_EMAIL", "piko@example.com".to_string()),
            ("GIT_COMMITTER_NAME", "piko".to_string()),
            ("GIT_COMMITTER_EMAIL", "piko@example.com".to_string()),
        ] {
            std::env::set_var(key, value);
        }
    });
}