#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use std::path::Path;
use std::process::{Command, Output};
const TABLE_HEADER: &str = "TOPIC BOUND FIND STAMPED DISCOVERY UNTYPED INVALID";
const MINIMAL_EDU_ROW: &str = "edu edu-fixture 3 1 0 1 1";
fn bin(cwd: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mif-rh-cli"));
cmd.current_dir(cwd);
cmd
}
fn stdout_of(output: &Output) -> String {
String::from_utf8(output.stdout.clone()).unwrap()
}
fn stderr_of(output: &Output) -> String {
String::from_utf8(output.stderr.clone()).unwrap()
}
macro_rules! skip_without_rht {
() => {
match common::rht_root() {
Some(root) => root,
None => {
assert!(
std::env::var_os("MIF_RH_PARITY_REQUIRED").is_none(),
"MIF_RH_PARITY_REQUIRED is set but no research-harness-template \
checkout was found (set MIF_RH_PARITY_FIXTURES_ROOT to a valid \
rht checkout) — refusing to skip parity fail-open"
);
eprintln!(
"skipping: no research-harness-template checkout found (set \
MIF_RH_PARITY_FIXTURES_ROOT to override)"
);
return;
},
}
};
}
#[test]
fn resolve_a_valid_finding_prints_the_record_line_and_exits_zero() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let output = bin(dir.path())
.args([
"resolve",
"reports/edu/findings/good.json",
"--topic",
"edu",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
assert_eq!(
stdout_of(&output).trim_end(),
"f-good: resolved -> edu-fixture@0.1.0 (valid=true)"
);
}
#[test]
fn resolve_an_invalid_finding_reports_valid_false_and_exits_one() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let output = bin(dir.path())
.args([
"resolve",
"reports/edu/findings/invalid.json",
"--topic",
"edu",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert_eq!(
stdout_of(&output).trim_end(),
"f-invalid: resolved -> edu-fixture@0.1.0 (valid=false)"
);
}
#[test]
fn review_prints_the_exact_topic_table_and_ends_on_the_summary_line() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let output = bin(dir.path()).arg("review").output().unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
let stdout = stdout_of(&output);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "unexpected extra output lines: {stdout}");
assert_eq!(lines[0], TABLE_HEADER);
assert_eq!(lines[1], MINIMAL_EDU_ROW);
assert_eq!(lines[2], "---");
assert_eq!(
lines.last().copied().unwrap(),
"1 topic(s); 3 findings — 1 stamped, 0 discovery-only, 1 untyped, 1 invalid/unresolved"
);
}
#[test]
fn review_strict_exits_one_on_an_invalid_finding() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let output = bin(dir.path())
.args(["review", "--strict"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
}
#[test]
fn review_followup_writes_the_backlog_and_prints_its_confirmation_before_the_summary() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let followup_path = dir.path().join("followup.json");
let output = bin(dir.path())
.args(["review", "--followup"])
.arg(&followup_path)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
let backlog: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&followup_path).unwrap()).unwrap();
assert_eq!(backlog["total_needs_followup"], 2);
let stdout = stdout_of(&output);
let confirmation = stdout
.lines()
.position(|l| l.starts_with("ontology-review: followup backlog written to"))
.expect("followup confirmation line must be printed");
let summary = stdout
.lines()
.position(|l| l.starts_with("1 topic(s);"))
.expect("summary line must be printed");
assert!(
confirmation < summary,
"followup confirmation must print before the final summary:\n{stdout}"
);
assert!(
stdout
.lines()
.next_back()
.unwrap()
.starts_with("1 topic(s);"),
"the summary must be the final stdout line:\n{stdout}"
);
}
#[test]
fn unknown_subcommand_exits_two_with_usage_on_stderr() {
let dir = tempfile::tempdir().unwrap();
let output = bin(dir.path()).arg("bogus").output().unwrap();
assert_eq!(output.status.code(), Some(2));
assert!(
stderr_of(&output).contains("Usage:"),
"stderr must carry usage: {}",
stderr_of(&output)
);
}
#[test]
fn review_in_an_empty_dir_with_format_json_emits_a_problem_document_on_stderr() {
let dir = tempfile::tempdir().unwrap();
let output = bin(dir.path())
.args(["review", "--format", "json"])
.output()
.unwrap();
assert_ne!(output.status.code(), Some(0));
let stderr = stderr_of(&output);
let problem: serde_json::Value =
serde_json::from_str(stderr.trim()).expect("stderr must parse as JSON");
assert!(
problem["type"].is_string(),
"problem must have a type URI: {stderr}"
);
assert!(
problem["status"].is_number(),
"problem must have a status: {stderr}"
);
}
#[test]
fn review_gate_m12_mixed_rht_corpus_matches_the_documented_output_through_the_binary() {
let root = skip_without_rht!();
let fixtures = root.join("evals/fixtures/ontology");
let dir = tempfile::tempdir().unwrap();
let scratch = dir.path();
for name in ["catalog.json", "config.json"] {
std::fs::copy(fixtures.join(name), scratch.join(name)).unwrap();
}
let catalog: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(fixtures.join("catalog.json")).unwrap())
.unwrap();
for entry in catalog["ontologies"].as_array().unwrap() {
let source = entry["source"].as_str().unwrap();
let dest = scratch.join(source);
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
std::fs::copy(root.join(source), dest).unwrap();
}
let findings_dir = scratch.join("reports/edu/findings");
std::fs::create_dir_all(&findings_dir).unwrap();
for name in [
"good.json",
"discovery.json",
"untyped.json",
"missing.json",
] {
std::fs::copy(fixtures.join(name), findings_dir.join(name)).unwrap();
}
let review_args = [
"review",
"--topic",
"edu",
"--reports-dir",
"reports",
"--config",
"config.json",
"--catalog",
"catalog.json",
"--root",
".",
];
let output = bin(scratch).args(review_args).output().unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
let stdout = stdout_of(&output);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "unexpected extra output lines: {stdout}");
assert_eq!(lines[0], TABLE_HEADER);
assert_eq!(
lines[1],
"edu edu-fixture 4 1 1 1 1"
);
assert_eq!(lines[2], "---");
assert_eq!(
lines.last().copied().unwrap(),
"1 topic(s); 4 findings — 1 stamped, 1 discovery-only, 1 untyped, 1 invalid/unresolved"
);
let strict = bin(scratch)
.args(review_args)
.arg("--strict")
.output()
.unwrap();
assert_eq!(strict.status.code(), Some(1));
}
#[cfg(windows)]
#[test]
fn review_with_a_configured_relationship_script_fails_loud_on_windows() {
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let script = dir.path().join("check.sh");
std::fs::write(&script, "#!/usr/bin/env bash\nexit 0\n").unwrap();
let output = bin(dir.path())
.args(["review", "--relationship-script"])
.arg(&script)
.output()
.unwrap();
assert_ne!(
output.status.code(),
Some(0),
"a spawn failure must not exit 0"
);
let stderr = stderr_of(&output);
assert!(
stderr.contains("check.sh"),
"stderr must name the script that failed to spawn: {stderr}"
);
let stdout = stdout_of(&output);
assert!(
!stdout.contains("topic(s);"),
"a failed review must not print the normal summary line: {stdout}"
);
}
#[cfg(unix)]
#[test]
fn review_runs_a_configured_relationship_script_exactly_once() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
common::write_minimal_corpus(dir.path());
let marker = dir.path().join("marker.txt");
let script = dir.path().join("check.sh");
std::fs::write(
&script,
format!("#!/bin/sh\necho ran >> \"{}\"\nexit 0\n", marker.display()),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let output = bin(dir.path())
.args(["review", "--relationship-script"])
.arg(&script)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
let runs = std::fs::read_to_string(&marker).unwrap();
assert_eq!(
runs.lines().count(),
1,
"the relationship script must run exactly once, got: {runs:?}"
);
}
#[test]
fn synthesize_corpus_accepts_preserved_insights_starting_with_a_hyphen() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("concordance.json"),
r#"{"nodes": [], "edges": []}"#,
)
.unwrap();
let output = bin(dir.path())
.args([
"harness",
"synthesize-corpus",
"concordance.json",
"map.json",
"out.md",
])
.args(["--preserved-insights", "- a leading bullet"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
assert!(dir.path().join("out.md").exists());
}
#[test]
fn wrap_source_accepts_title_and_content_starting_with_a_hyphen() {
let dir = tempfile::tempdir().unwrap();
let schema_path = dir.path().join("source-envelope.schema.json");
std::fs::write(
&schema_path,
r#"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["@id", "content", "title"],
"properties": {
"@id": {"type": "string"},
"content": {"type": "string", "minLength": 1},
"title": {"type": "string"}
}
}"#,
)
.unwrap();
let output = bin(dir.path())
.args(["harness", "wrap-source"])
.args(["--url", "https://example.com/paper"])
.args(["--content-type", "text/html"])
.args(["--namespace", "physics"])
.args(["--slug", "example-paper"])
.args(["--out", "envelope.json"])
.args(["--title", "- a leading dash title"])
.args(["--content", "- a leading dash body"])
.arg("--schema")
.arg(&schema_path)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{}", stderr_of(&output));
assert!(dir.path().join("envelope.json").exists());
}
#[test]
fn suggest_type_accepts_free_text_starting_with_a_hyphen() {
let dir = tempfile::tempdir().unwrap();
let output = bin(dir.path())
.args(["suggest-type", "- some free text", "--topic", "foo"])
.output()
.unwrap();
assert_ne!(
output.status.code(),
Some(2),
"a clap usage-error exit code means the leading-hyphen text was \
misparsed as a flag: {}",
stderr_of(&output)
);
let stderr = stderr_of(&output);
assert!(
!stderr.contains("unexpected argument"),
"clap must not misparse the leading-hyphen text as a flag: {stderr}"
);
}