illumitech-forge-cli 0.3.0

Evidence-backed agent execution CLI
mod common;
use serde_json::{Value, json};
use std::{
    path::PathBuf,
    process::{Command, Stdio},
    time::Duration,
};
struct Fixture {
    _dir: tempfile::TempDir,
    root: PathBuf,
    repo: PathBuf,
    manifest: PathBuf,
    state: PathBuf,
}
impl Fixture {
    fn new(command: &str, human: bool) -> Self {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().canonicalize().unwrap();
        let repo = root.join("repo espaço 日本");
        std::fs::create_dir(&repo).unwrap();
        for args in [
            vec!["init", "-q"],
            vec![
                "-c",
                "user.name=Fixture",
                "-c",
                "user.email=fixture@example.invalid",
                "commit",
                "-q",
                "--allow-empty",
                "-m",
                "fixture",
            ],
        ] {
            assert!(
                Command::new("git")
                    .args(args)
                    .current_dir(&repo)
                    .status()
                    .unwrap()
                    .success()
            );
        }
        std::fs::write(repo.join("preserved.txt"), "keep").unwrap();
        let evaluator = root.join("evaluate.py");
        std::fs::write(&evaluator,"from pathlib import Path\nimport sys\np=Path(sys.argv[1])\nassert (p/'output.txt').read_text() == 'verified'\nassert (p/'preserved.txt').read_text() == 'keep'\n").unwrap();
        let mut acceptance = vec![json!({"kind":"gate","id":"files"})];
        if human {
            acceptance.push(json!({"kind":"human","id":"review"}));
        }
        let m = json!({"schema_version":1,"release_id":"fixture","repos":{"app":{"path":repo}},"tasks":[{"id":"build","repo":"app","description":"fixture","executor":{"kind":"command","argv":common::command(command)},"acceptance":acceptance,"max_attempts":3,"timeout_seconds":20}],"gates":[{"id":"files","argv":[common::python(),evaluator,"{repo}"],"inputs":{evaluator.display().to_string():forge_domain::digest(&std::fs::read(&evaluator).unwrap())}}]});
        let manifest = root.join("manifest.yaml");
        std::fs::write(&manifest, serde_json::to_vec(&m).unwrap()).unwrap();
        let state = root.join("state");
        Self {
            _dir: dir,
            root,
            repo,
            manifest,
            state,
        }
    }
    fn args(&self, args: &[&str]) -> Vec<String> {
        let mut a = vec![
            "--json".into(),
            "--state-dir".into(),
            self.state.display().to_string(),
        ];
        a.extend(args.iter().map(|s| s.to_string()));
        a.push(self.manifest.display().to_string());
        a
    }
    fn call(&self, args: &[&str]) -> (bool, Value) {
        let out = Command::new(env!("CARGO_BIN_EXE_illumitech-forge"))
            .args(self.args(args))
            .current_dir(&self.root)
            .output()
            .unwrap();
        (
            out.status.success(),
            serde_json::from_slice(&out.stdout).unwrap_or_else(|_| {
                panic!(
                    "{} {}",
                    String::from_utf8_lossy(&out.stdout),
                    String::from_utf8_lossy(&out.stderr)
                )
            }),
        )
    }
    fn start(&self) -> std::process::Child {
        Command::new(env!("CARGO_BIN_EXE_illumitech-forge"))
            .args(self.args(&["release", "run"]))
            .current_dir(&self.root)
            .stdout(Stdio::null())
            .spawn()
            .unwrap()
    }
    fn wait_running(&self) {
        for _ in 0..100 {
            if self.state.join("state.sqlite").exists()
                && let Ok(store) = forge_domain::Store::open(&self.state.join("state.sqlite"), true)
                && let Ok(Some(run)) = store.load("fixture")
                && run.tasks["build"].pid.is_some()
            {
                return;
            }
            std::thread::sleep(Duration::from_millis(50));
        }
        panic!("not running");
    }
}
#[test]
fn dry_run_is_read_only() {
    let f = Fixture::new("printf verified > output.txt", false);
    assert!(f.call(&["release", "run", "--dry-run"]).0);
    assert!(!f.state.exists());
    assert!(!f.repo.join("output.txt").exists());
}
#[test]
fn text_never_satisfies_a_gate() {
    let f = Fixture::new("echo 'All done; all gates passed'", false);
    let (ok, v) = f.call(&["release", "run"]);
    assert!(!ok);
    assert_eq!(v["run"]["status"], "blocked");
}
#[test]
fn success_and_artifact_change_invalidates_status() {
    let f = Fixture::new("printf verified > output.txt", false);
    let (ok, v) = f.call(&["release", "run"]);
    assert!(ok, "{v}");
    assert_eq!(v["run"]["status"], "passed");
    std::fs::write(f.repo.join("output.txt"), "tampered").unwrap();
    assert_eq!(f.call(&["status"]).1["evidence_current"], false);
    assert!(!f.call(&["resume"]).0);
}
#[test]
fn ignored_artifact_change_blocks_resume() {
    let f = Fixture::new("printf verified > output.txt", false);
    std::fs::write(f.repo.join(".gitignore"), "output.txt\n").unwrap();
    let mut m: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    m["tasks"][0]["acceptance"] = json!([{
        "kind":"artifact", "path":"output.txt", "sha256":forge_domain::digest(b"verified")
    }]);
    std::fs::write(&f.manifest, serde_json::to_vec(&m).unwrap()).unwrap();
    assert!(f.call(&["release", "run"]).0);
    std::fs::write(f.repo.join("output.txt"), "tampered").unwrap();
    assert_eq!(f.call(&["status"]).1["evidence_current"], false);
    let (ok, result) = f.call(&["resume"]);
    assert!(!ok);
    assert!(
        result["error"]
            .as_str()
            .unwrap_or_default()
            .contains("reconcile"),
        "{result}"
    );
}
#[test]
fn mandatory_memory_fails_closed() {
    let f = Fixture::new("printf verified > output.txt", false);
    let mut m: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    m["memory"] = json!({"required":true,"endpoint":"http://127.0.0.1:1/mcp","workspace":"fixture","projects":{"app":"repo"}});
    std::fs::write(&f.manifest, serde_json::to_vec(&m).unwrap()).unwrap();
    let (_, v) = f.call(&["release", "run"]);
    assert_eq!(v["run"]["status"], "blocked");
    assert!(!f.repo.join("output.txt").exists());
}
#[test]
fn approval_is_bound_to_revision() {
    let f = Fixture::new("printf verified > output.txt", true);
    let (_, v) = f.call(&["release", "run"]);
    assert_eq!(v["run"]["status"], "awaiting_approval");
    let fingerprint = v["run"]["tasks"]["build"]["revision"]["fingerprint"]
        .as_str()
        .unwrap();
    assert!(
        f.call(&[
            "release",
            "approve",
            "--task",
            "build",
            "--gate",
            "review",
            "--reviewer",
            "fixture-human",
            "--fingerprint",
            fingerprint
        ])
        .0
    );
    let (ok, v) = f.call(&["resume"]);
    assert!(ok, "{v}");
}
#[test]
fn changed_manifest_blocks_resume() {
    let f = Fixture::new("printf verified > output.txt", true);
    f.call(&["release", "run"]);
    let s = std::fs::read_to_string(&f.manifest).unwrap();
    std::fs::write(&f.manifest, format!("{s}\n")).unwrap();
    assert!(!f.call(&["resume"]).0);
}
#[test]
fn lock_conflict_cancel_and_control() {
    let f = Fixture::new("sleep 10; printf verified > output.txt", false);
    let mut child = f.start();
    f.wait_running();
    assert!(!f.call(&["resume"]).0);
    assert!(f.call(&["cancel"]).0);
    assert!(!child.wait().unwrap().success());
    let (_, v) = f.call(&["status"]);
    assert_eq!(v["run"]["status"], "cancelled");
    assert!(!f.repo.join("output.txt").exists());
}
#[test]
fn pause_finishes_current_task_then_resumes() {
    let f = Fixture::new("sleep 1; printf verified > output.txt", false);
    let mut child = f.start();
    f.wait_running();
    assert!(f.call(&["pause"]).0);
    child.wait().unwrap();
    assert_eq!(f.call(&["status"]).1["run"]["status"], "paused");
    let (ok, v) = f.call(&["resume"]);
    assert!(ok, "{v}");
}
#[test]
fn evaluator_inside_repo_rejected() {
    let f = Fixture::new("true", false);
    let mut m: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    let inside = f.repo.join("eval.py");
    std::fs::write(&inside, "pass").unwrap();
    m["gates"][0]["inputs"] = json!({inside.display().to_string():forge_domain::digest(b"pass")});
    std::fs::write(&f.manifest, serde_json::to_vec(&m).unwrap()).unwrap();
    assert!(!f.call(&["release", "inspect"]).0);
}
#[test]
fn retries_are_bounded() {
    let f = Fixture::new("exit 1", false);
    let (_, v) = f.call(&["release", "run"]);
    assert_eq!(v["run"]["tasks"]["build"]["attempts"], 3);
    assert_eq!(v["run"]["status"], "blocked");
}
#[test]
fn state_cannot_live_in_agent_worktree() {
    let f = Fixture::new("true", false);
    let args = [
        "--json",
        "--state-dir",
        f.repo.to_str().unwrap(),
        "release",
        "inspect",
        f.manifest.to_str().unwrap(),
    ];
    assert!(
        !Command::new(env!("CARGO_BIN_EXE_illumitech-forge"))
            .args(args)
            .stdout(Stdio::null())
            .status()
            .unwrap()
            .success()
    );
}
#[cfg(unix)]
#[test]
fn artifact_symlink_escape_rejected() {
    let f = Fixture::new("true", false);
    let secret = f.root.join("outside");
    std::fs::write(&secret, "verified").unwrap();
    std::os::unix::fs::symlink(secret, f.repo.join("output.txt")).unwrap();
    let mut m: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    m["tasks"][0]["acceptance"] =
        json!([{"kind":"artifact","path":"output.txt","sha256":forge_domain::digest(b"verified")}]);
    std::fs::write(&f.manifest, serde_json::to_vec(&m).unwrap()).unwrap();
    assert!(!f.call(&["release", "run"]).0);
}

#[test]
fn crash_never_silently_restarts_orphan() {
    let f = Fixture::new("sleep 15; printf verified > output.txt", false);
    let mut child = f.start();
    f.wait_running();
    let store = forge_domain::Store::open(&f.state.join("state.sqlite"), true).unwrap();
    let pid = store.load("fixture").unwrap().unwrap().tasks["build"]
        .pid
        .unwrap();
    child.kill().unwrap();
    child.wait().unwrap();
    let (ok, v) = f.call(&["resume"]);
    assert!(!ok, "{v}");
    assert!(v["error"].as_str().unwrap().contains("orphan"));
    #[cfg(unix)]
    Command::new("sh")
        .args(["-c", &format!("kill -9 -{pid}")])
        .status()
        .unwrap();
    #[cfg(windows)]
    {
        for _ in 0..100 {
            if !forge_adapters::process::pid_alive(pid) {
                return;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        panic!("Job left orphan alive");
    }
}

#[test]
fn evaluator_tampering_invalidates_inspection() {
    let f = Fixture::new("printf verified > output.txt", false);
    std::fs::write(f.root.join("evaluate.py"), "# altered evaluator").unwrap();
    assert!(!f.call(&["release", "inspect"]).0);
    assert!(!f.state.exists());
}

#[cfg(windows)]
#[test]
fn artifact_junction_escape_rejected_without_symlink_privilege() {
    let f = Fixture::new("true", false);
    let outside = f.root.join("outside");
    std::fs::create_dir(&outside).unwrap();
    std::fs::write(outside.join("output.txt"), "verified").unwrap();
    let link = f.repo.join("linked");
    assert!(
        Command::new("cmd.exe")
            .args(["/d", "/c", "mklink", "/J"])
            .arg(&link)
            .arg(&outside)
            .status()
            .unwrap()
            .success()
    );
    let mut m: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    m["tasks"][0]["acceptance"] = json!([{"kind":"artifact","path":"linked/output.txt","sha256":forge_domain::digest(b"verified")}]);
    std::fs::write(&f.manifest, serde_json::to_vec(&m).unwrap()).unwrap();
    assert!(!f.call(&["release", "run"]).0);
    std::fs::remove_dir(link).unwrap();
}

#[test]
fn optional_observability_preserves_execution_failure_and_dry_run() {
    let f = Fixture::new("exit 1", false);
    let mut manifest: Value = serde_json::from_slice(&std::fs::read(&f.manifest).unwrap()).unwrap();
    manifest["automation"] = json!({"observability":{"langsmith":"optional"}});
    std::fs::write(&f.manifest, serde_json::to_vec(&manifest).unwrap()).unwrap();
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    listener.set_nonblocking(true).unwrap();
    for (args, dry) in [
        (vec!["release", "run", "--dry-run"], true),
        (vec!["release", "run"], false),
    ] {
        let output = Command::new(env!("CARGO_BIN_EXE_illumitech-forge"))
            .args(f.args(&args))
            .current_dir(&f.root)
            .env("LANGSMITH_TRACING", "true")
            .env("LANGSMITH_API_KEY", "fixture-only")
            .env(
                "LANGSMITH_ENDPOINT",
                format!("http://{}", listener.local_addr().unwrap()),
            )
            .output()
            .unwrap();
        let value: Value = serde_json::from_slice(&output.stdout).unwrap();
        if dry {
            assert!(output.status.success());
            assert!(!f.state.exists());
        } else {
            assert!(!output.status.success());
            assert_eq!(value["run"]["status"], "blocked");
            assert_eq!(value["run"]["tasks"]["build"]["attempts"], 3);
        }
        // Production binaries never allow HTTP, even on loopback.
        assert!(listener.accept().is_err());
    }
}