use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
fn mati_bin() -> PathBuf {
let env_key = "CARGO_BIN_EXE_MATI";
if let Ok(p) = std::env::var(env_key) {
return PathBuf::from(p);
}
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
PathBuf::from(manifest)
.join("target")
.join("debug")
.join("mati")
}
fn run(bin: &Path, repo: &Path, home: &Path, args: &[&str]) -> (String, String, bool) {
let out = Command::new(bin)
.args(args)
.current_dir(repo)
.env("HOME", home)
.env("NO_COLOR", "1") .output()
.expect("failed to run mati");
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
(stdout, stderr, out.status.success())
}
fn setup_repo() -> (TempDir, TempDir) {
let repo_dir = TempDir::new().expect("create repo dir");
let home_dir = TempDir::new().expect("create home dir");
let repo = repo_dir.path();
Command::new("git")
.args(["init"])
.current_dir(repo)
.output()
.expect("git init");
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo)
.output()
.expect("git config email");
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo)
.output()
.expect("git config name");
std::fs::create_dir_all(repo.join("src")).expect("mkdir src");
std::fs::write(
repo.join("src/main.rs"),
r#"fn main() {
println!("hello");
}
fn helper() -> Result<(), Box<dyn std::error::Error>> {
// TODO: handle error properly
let x = std::fs::read_to_string("config.toml")?;
Ok(())
}
"#,
)
.expect("write main.rs");
std::fs::write(
repo.join("src/lib.rs"),
r#"pub fn add(a: i32, b: i32) -> i32 {
a + b
}
"#,
)
.expect("write lib.rs");
std::fs::write(
repo.join("Cargo.toml"),
r#"[package]
name = "test-project"
version = "0.1.0"
edition = "2021"
"#,
)
.expect("write Cargo.toml");
Command::new("git")
.args(["add", "-A"])
.current_dir(repo)
.output()
.expect("git add");
Command::new("git")
.args(["commit", "-m", "initial commit"])
.current_dir(repo)
.output()
.expect("git commit");
(repo_dir, home_dir)
}
fn init_repo(bin: &Path, repo: &Path, home: &Path) -> String {
let (stdout, stderr, ok) = run(bin, repo, home, &["init", "--no-hooks"]);
if !ok {
panic!("mati init failed:\nstdout: {stdout}\nstderr: {stderr}");
}
stdout
}
fn init_repo_codex(bin: &Path, repo: &Path, home: &Path) -> String {
std::fs::create_dir_all(repo.join(".codex")).expect("mkdir .codex");
let (stdout, stderr, ok) = run(bin, repo, home, &["init", "--codex"]);
if !ok {
panic!("mati init --codex failed:\nstdout: {stdout}\nstderr: {stderr}");
}
stdout
}
fn init_repo_autodetect_codex(bin: &Path, repo: &Path, home: &Path) -> String {
std::fs::create_dir_all(repo.join(".codex")).expect("mkdir .codex");
let (stdout, stderr, ok) = run(bin, repo, home, &["init"]);
if !ok {
panic!("mati init failed in codex repo:\nstdout: {stdout}\nstderr: {stderr}");
}
stdout
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut in_escape = false;
for c in s.chars() {
if c == '\x1b' {
in_escape = true;
continue;
}
if in_escape {
if c.is_ascii_alphabetic() {
in_escape = false;
}
continue;
}
out.push(c);
}
out
}
fn assert_contains(haystack: &str, needle: &str) {
let clean = strip_ansi(haystack);
assert!(
clean.contains(needle),
"Expected output to contain: {needle:?}\n\n--- Actual output ---\n{clean}"
);
}
#[test]
fn help_workflow_framing() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(&bin, Path::new("."), Path::new("/tmp"), &["--help"]);
assert!(ok, "mati --help should succeed");
let out = strip_ansi(&stdout);
assert_contains(&out, "persistent, queryable knowledge store");
assert_contains(&out, "mati init");
assert_contains(&out, "mati explain <file>");
assert_contains(&out, "mati diff <range>");
assert_contains(&out, "mati status");
assert_contains(&out, "build project memory");
assert_contains(&out, "file briefing");
assert_contains(&out, "pre-merge check");
assert_contains(&out, "project memory dashboard");
}
#[test]
fn help_subcommands_present() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(&bin, Path::new("."), Path::new("/tmp"), &["--help"]);
assert!(ok);
let out = strip_ansi(&stdout);
for cmd in &["init", "explain", "diff", "status"] {
assert_contains(&out, cmd);
}
for cmd in &["gotcha", "show", "gaps", "stats"] {
assert_contains(&out, cmd);
}
for cmd in &["review", "repair", "stale"] {
assert_contains(&out, cmd);
}
for cmd in &["serve", "daemon", "ping"] {
assert_contains(&out, cmd);
}
}
#[test]
fn init_next_steps_guidance() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let stdout = init_repo(&bin, repo_dir.path(), home_dir.path());
assert_contains(&stdout, "mati");
assert_contains(&stdout, "file records:");
assert_contains(&stdout, "graph edges:");
assert_contains(&stdout, "0 tokens");
assert_contains(&stdout, "0 Claude calls");
assert_contains(&stdout, "Next steps");
assert_contains(&stdout, "mati explain");
assert_contains(&stdout, "mati review");
assert_contains(&stdout, "mati status");
assert_contains(&stdout, "start here");
assert_contains(&stdout, "candidates for hook enforcement");
assert_contains(&stdout, "project knowledge dashboard");
}
#[test]
fn init_summary_has_candidate_categories() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let stdout = init_repo(&bin, repo_dir.path(), home_dir.path());
assert_contains(&stdout, "gotcha candidates:");
assert_contains(&stdout, "dep records:");
assert_contains(&stdout, "hotspot files:");
}
#[test]
fn init_codex_reports_platform_capability() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let stdout = init_repo_codex(&bin, repo_dir.path(), home_dir.path());
assert_contains(&stdout, "integration:");
assert_contains(&stdout, "Codex");
assert_contains(&stdout, "Enforcement");
assert_contains(&stdout, "Bash reads blocked");
assert_contains(&stdout, "gotchas injected on prompt submit");
}
#[test]
fn init_autodetects_codex_without_forcing_claude() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let stdout = init_repo_autodetect_codex(&bin, repo_dir.path(), home_dir.path());
assert_contains(&stdout, "integration:");
assert_contains(&stdout, "Codex");
assert!(
!strip_ansi(&stdout).contains("Claude + Codex"),
"plain init in a codex-only repo should not force Claude installation\n--- stdout ---\n{}",
strip_ansi(&stdout)
);
}
#[test]
fn explain_output_structure() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["explain", "src/main.rs"],
);
assert!(ok, "mati explain should succeed");
assert_contains(&stdout, "main.rs");
assert_contains(&stdout, "confidence");
assert_contains(&stdout, "quality");
assert_contains(&stdout, "source:");
assert_contains(&stdout, "mati gotcha add");
}
#[test]
fn explain_todo_section() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["explain", "src/main.rs"],
);
assert!(ok);
assert_contains(&stdout, "TODOs");
assert_contains(&stdout, "handle error");
}
#[test]
fn explain_missing_file_suggests_init() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, stderr, _ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["explain", "nonexistent/file.rs"],
);
let combined = format!("{stdout}{stderr}");
assert_contains(&combined, "mati init");
}
#[test]
fn diff_output_structure() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let repo = repo_dir.path();
init_repo(&bin, repo, home_dir.path());
std::fs::write(
repo.join("src/main.rs"),
r#"fn main() {
println!("updated");
}
"#,
)
.expect("update main.rs");
Command::new("git")
.args(["add", "src/main.rs"])
.current_dir(repo)
.output()
.expect("git add");
Command::new("git")
.args(["commit", "-m", "update main"])
.current_dir(repo)
.output()
.expect("git commit");
let (stdout, _stderr, ok) = run(&bin, repo, home_dir.path(), &["diff", "HEAD~1"]);
assert!(ok, "mati diff should succeed");
assert_contains(&stdout, "PRE-MERGE CHECK");
assert_contains(&stdout, "changed");
let has_symbol = stdout.contains("documented")
|| stdout.contains("no file record")
|| stdout.contains("gotcha");
assert!(
has_symbol,
"diff output should classify files\n--- stdout ---\n{stdout}"
);
let has_severity = stdout.contains("CRITICAL")
|| stdout.contains("HIGH")
|| stdout.contains("NORMAL")
|| stdout.contains("UNKNOWN");
assert!(
has_severity,
"diff output should show a severity marker per file\n--- stdout ---\n{stdout}"
);
}
#[test]
fn diff_summary_line_format() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let repo = repo_dir.path();
init_repo(&bin, repo, home_dir.path());
std::fs::write(
repo.join("src/lib.rs"),
"pub fn sub(a: i32, b: i32) -> i32 { a - b }\n",
)
.expect("update lib.rs");
Command::new("git")
.args(["add", "src/lib.rs"])
.current_dir(repo)
.output()
.expect("git add");
Command::new("git")
.args(["commit", "-m", "update lib"])
.current_dir(repo)
.output()
.expect("git commit");
let (stdout, _stderr, ok) = run(&bin, repo, home_dir.path(), &["diff", "HEAD~1"]);
assert!(ok);
assert_contains(&stdout, "Summary:");
assert_contains(&stdout, "warned");
assert_contains(&stdout, "documented");
assert_contains(&stdout, "unknown");
}
#[test]
fn status_dashboard_sections() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(&bin, repo_dir.path(), home_dir.path(), &["status"]);
assert!(ok, "mati status should succeed");
assert_contains(&stdout, "mati status");
assert_contains(&stdout, "Records");
assert_contains(&stdout, "Confirmed");
assert_contains(&stdout, "Confidence");
assert_contains(&stdout, "Hotspots");
assert_contains(&stdout, "files");
assert_contains(&stdout, "gotchas");
}
#[test]
fn status_trust_section_with_unconfirmed() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(&bin, repo_dir.path(), home_dir.path(), &["status"]);
assert!(ok);
let has_trust_guidance = stdout.contains("mati review") || stdout.contains("No gotchas yet");
assert!(
has_trust_guidance,
"status should show trust guidance or no-gotchas hint\n--- stdout ---\n{stdout}"
);
}
#[test]
fn repair_check_clean_state() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["repair", "--check"],
);
assert!(ok, "repair --check should succeed on clean state");
assert_contains(&stdout, "mati repair --check");
assert_contains(&stdout, "gotchas");
assert_contains(&stdout, "files");
assert_contains(&stdout, "No drift detected");
assert_contains(&stdout, "consistent");
}
#[test]
fn repair_check_json_output() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["repair", "--check", "--json"],
);
assert!(ok, "repair --check --json should succeed on clean state");
let v: serde_json::Value = serde_json::from_str(stdout.trim())
.expect("repair --check --json should produce valid JSON");
assert!(
v.get("scanned_gotchas").is_some(),
"JSON should have scanned_gotchas"
);
assert!(
v.get("scanned_files").is_some(),
"JSON should have scanned_files"
);
}
#[test]
fn review_help_explains_workflow() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(
&bin,
Path::new("."),
Path::new("/tmp"),
&["review", "--help"],
);
assert!(ok, "mati review --help should succeed");
assert_contains(&stdout, "auto-detected");
assert_contains(&stdout, "hook enforcement");
assert_contains(&stdout, "candidates");
}
#[test]
fn repair_help_explains_semantics() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(
&bin,
Path::new("."),
Path::new("/tmp"),
&["repair", "--help"],
);
assert!(ok, "mati repair --help should succeed");
assert_contains(&stdout, "canonical");
assert_contains(&stdout, "--check");
assert_contains(&stdout, "CI");
assert_contains(&stdout, "--fast");
assert_contains(&stdout, "integrity");
}
#[test]
fn status_shows_codex_platform_mode() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
let repo = repo_dir.path();
init_repo_codex(&bin, repo, home_dir.path());
let (stdout, _stderr, ok) = run(&bin, repo, home_dir.path(), &["status"]);
assert!(ok, "mati status should succeed");
assert_contains(&stdout, "Platform");
assert_contains(&stdout, "Codex");
assert_contains(&stdout, "hard Bash enforcement");
assert_contains(&stdout, "soft native-read enforcement");
}
#[test]
fn explain_help_describes_briefing() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(
&bin,
Path::new("."),
Path::new("/tmp"),
&["explain", "--help"],
);
assert!(ok);
assert_contains(&stdout, "briefing");
assert_contains(&stdout, "gotchas");
assert_contains(&stdout, "decisions");
assert_contains(&stdout, "co-change");
}
#[test]
fn diff_help_describes_premerge() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(&bin, Path::new("."), Path::new("/tmp"), &["diff", "--help"]);
assert!(ok);
assert_contains(&stdout, "Pre-merge");
assert_contains(&stdout, "gotchas");
assert_contains(&stdout, "main");
}
#[test]
fn history_enforcement_empty_state_is_explicit() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
let (stdout, _stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["history", "--enforcement", "--limit", "10"],
);
assert!(ok, "mati history --enforcement should succeed on empty log");
let clean = strip_ansi(&stdout);
assert!(
clean.contains("No enforcement events"),
"empty enforcement log must use the explicit 'No enforcement events' \
message, never the legacy 'None found' / 'No errors or warnings' wording.\n\
--- stdout ---\n{clean}"
);
assert!(
!clean.contains("None found"),
"legacy 'None found' wording leaked back in:\n{clean}"
);
assert!(
!clean.contains("No errors or warnings"),
"legacy summary wording leaked back in:\n{clean}"
);
}
#[test]
fn history_enforcement_type_filter_accepts_documented_labels() {
let bin = mati_bin();
let (repo_dir, home_dir) = setup_repo();
init_repo(&bin, repo_dir.path(), home_dir.path());
for label in &[
"deny",
"allow_receipt",
"control_changed",
"config_changed",
"gap",
] {
let (stdout, stderr, ok) = run(
&bin,
repo_dir.path(),
home_dir.path(),
&["history", "--enforcement", "--type", label, "--limit", "5"],
);
assert!(
ok,
"history --enforcement --type {label} should succeed\n\
stdout: {stdout}\nstderr: {stderr}"
);
}
}
#[test]
fn history_enforcement_help_lists_event_types() {
let bin = mati_bin();
let (stdout, _stderr, ok) = run(
&bin,
Path::new("."),
Path::new("/tmp"),
&["history", "--help"],
);
assert!(ok);
let clean = strip_ansi(&stdout);
assert!(
clean.contains("--enforcement"),
"history --help should advertise --enforcement:\n{clean}"
);
assert!(
clean.contains("--type"),
"history --help should advertise --type:\n{clean}"
);
for label in &["control_changed", "config_changed"] {
assert!(
clean.contains(label),
"history --help should mention type label {label}:\n{clean}"
);
}
}