use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
fn roteiro(args: &[&str]) -> std::process::Output {
Command::new(BIN).args(args).output().expect("run roteiro")
}
fn bundle(tag: &str, files: &[(&str, &str)]) -> PathBuf {
static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"roteiro-okf-inspect-{}-{seq}-{tag}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
for (rel, content) in files {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().expect("a parent")).expect("create bundle dir");
std::fs::write(&path, content).expect("write concept");
}
root
}
fn two_tier_bundle(tag: &str) -> PathBuf {
bundle(
tag,
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/revenue.md",
"---\ntype: Metric\ntitle: Revenue\nverified: { by: human:alice, at: 2026-08-01T10:00:00Z }\n---\n\n# Definition\n",
),
(
"metrics/cost.md",
"---\ntype: Metric\ntitle: Cost\ngenerated: { by: agent/1.0, at: 2026-08-01T10:00:00Z }\n---\n\n# Definition\n",
),
],
)
}
#[test]
fn trust_reports_the_tier_of_every_concept() {
let root = two_tier_bundle("trust");
let out = roteiro(&["okf", "trust", &root.to_string_lossy()]);
assert!(out.status.success(), "okf trust should succeed");
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert!(
stdout.contains(" human-reviewed 1, machine-confirmed 0, unverified 1\n"),
"the aggregate line must state all three tiers, so a bundle with none of \
one is legible as zero rather than absent; got:\n{stdout}"
);
assert!(
stdout.contains("human-reviewed metrics/revenue — verified by human:alice\n"),
"a human-verified concept must name its verifier: who signed off is the \
load-bearing half of the claim; got:\n{stdout}"
);
assert!(
stdout.contains("unverified metrics/cost\n"),
"a concept with `generated` and no `verified` is unverified, not unknown; \
got:\n{stdout}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn trust_emits_json_on_request() {
let root = two_tier_bundle("trust-json");
let out = roteiro(&["okf", "trust", &root.to_string_lossy(), "--json"]);
assert!(out.status.success());
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("--json must emit parseable JSON");
assert_eq!(v["total"], 2);
assert_eq!(v["human_reviewed"], 1);
assert_eq!(v["unverified"], 1);
assert_eq!(
v["okf_version"], "0.2",
"the root index's declared version (§10) belongs in the summary"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_broken_link_is_reported_and_gates_only_under_check() {
let root = bundle(
"links",
&[
(
"a.md",
"---\ntype: Metric\n---\n\n# A\n\nSee [B](./b.md) and [gone](./gone.md).\n",
),
("b.md", "---\ntype: Metric\n---\n\n# B\n"),
],
);
let path = root.to_string_lossy().into_owned();
let plain = roteiro(&["okf", "links", &path]);
let stdout = String::from_utf8(plain.stdout).expect("utf-8");
assert!(
plain.status.success(),
"without --check a broken link is reported, not gated: inspecting a \
peer's bundle must not fail the command"
);
assert!(
stdout.contains(" broken: a -> ./gone.md\n"),
"the broken link must name both the concept and the target as written; \
got:\n{stdout}"
);
let gated = roteiro(&["okf", "links", &path, "--check"]);
assert_eq!(
gated.status.code(),
Some(1),
"--check must exit 1 on a broken link, or it is not a gate"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn diff_separates_an_unchanged_bundle_from_a_changed_one() {
let a = two_tier_bundle("diff-a");
let b = bundle(
"diff-b",
&[(
"metrics/revenue.md",
"---\ntype: Metric\ntitle: Revenue\nverified: { by: human:alice, at: 2026-08-01T10:00:00Z }\n---\n\n# Definition\n",
)],
);
let same = roteiro(&["okf", "diff", &a.to_string_lossy(), &a.to_string_lossy()]);
let stdout = String::from_utf8(same.stdout).expect("utf-8");
assert!(
stdout.contains(" no semantic change\n"),
"a bundle must be semantically identical to itself; got:\n{stdout}"
);
let changed = roteiro(&["okf", "diff", &a.to_string_lossy(), &b.to_string_lossy()]);
let stdout = String::from_utf8(changed.stdout).expect("utf-8");
assert!(
stdout.contains(" removed metrics/cost\n"),
"a concept present in `before` and absent from `after` is a removal; \
got:\n{stdout}"
);
let _ = std::fs::remove_dir_all(&a);
let _ = std::fs::remove_dir_all(&b);
}
#[test]
fn a_path_that_is_not_a_bundle_is_refused_by_name() {
let out = roteiro(&["okf", "trust", "/no/such/bundle"]);
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert_eq!(
stderr,
"Error: OKF bundle not found: /no/such/bundle (expected a directory of concept documents)\n",
"the refusal must name the path and say what was expected"
);
}
#[test]
fn the_upstream_fixtures_stay_in_one_crate() {
let here = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/okf-upstream");
assert!(
!here.exists(),
"the specification's published bundles are vendored once, under \
crates/rto-render/tests/fixtures/okf-upstream, with their provenance and \
licence recorded beside them. A second copy would drift from the first"
);
}
#[test]
fn a_computation_that_does_not_parse_fails_the_command() {
let root = bundle(
"syntax-broken",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"computations/revenue.md",
"---\ntype: Attested Computation\ntitle: Revenue\nruntime: bigquery\n---\n\n\
# Computation\n\n```sql\nSELCT total FROM `p.d.orders`;\n```\n",
),
],
);
let path = root.to_string_lossy().into_owned();
let out = roteiro(&["okf", "syntax", &path]);
assert!(
!out.status.success(),
"a computation that does not parse must fail the command"
);
let text = String::from_utf8(out.stdout).expect("utf-8");
assert!(text.contains("revenue.md"), "names the file: {text}");
assert!(text.contains("sql"), "names the language: {text}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn nothing_to_check_is_reported_as_nothing_rather_than_as_clean() {
let root = bundle(
"syntax-empty",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/prose.md",
"---\ntype: Metric\ntitle: Prose\n---\n\n# Definition\n\nNo code here.\n",
),
],
);
let path = root.to_string_lossy().into_owned();
let out = roteiro(&["okf", "syntax", &path]);
assert!(out.status.success(), "nothing wrong, so it must not gate");
let text = String::from_utf8(out.stdout).expect("utf-8");
assert!(
text.contains("0 block(s) checked"),
"the count is stated: {text}"
);
assert!(
text.contains("nothing to check"),
"and it is not dressed up as a pass: {text}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_conformance_error_fails_the_validate_command() {
let root = bundle(
"validate-error",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/untyped.md",
"---\ntitle: Untyped\n---\n\n# Untyped\n\nProse.\n",
),
],
);
let path = root.to_string_lossy().into_owned();
let out = roteiro(&["okf", "validate", &path]);
assert!(!out.status.success(), "a conformance error must gate");
let text = String::from_utf8(out.stdout).expect("utf-8");
assert!(text.contains("metrics/untyped"), "names it: {text}");
assert!(text.contains("`type` is missing"), "{text}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn lint_reports_without_gating() {
let root = bundle(
"lint-noisy",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/messy.md",
"---\ntitle: Messy\ntype: Metric\nstatus: draft\n---\n\nNo heading at all. \n",
),
],
);
let path = root.to_string_lossy().into_owned();
let out = roteiro(&["okf", "lint", &path]);
assert!(
out.status.success(),
"hygiene never gates, however much it finds"
);
let text = String::from_utf8(out.stdout).expect("utf-8");
for code in ["[L1]", "[L12]"] {
assert!(text.contains(code), "expected {code} in: {text}");
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn validate_and_lint_are_not_the_same_command() {
let root = two_tier_bundle("conform-json");
let path = root.to_string_lossy().into_owned();
let v: serde_json::Value =
serde_json::from_slice(&roteiro(&["okf", "validate", &path, "--json"]).stdout)
.expect("validate --json");
let l: serde_json::Value =
serde_json::from_slice(&roteiro(&["okf", "lint", &path, "--json"]).stdout)
.expect("lint --json");
assert_eq!(v["check"], "validate");
assert_eq!(l["check"], "lint");
assert_eq!(v["concepts"], 2, "{v}");
assert_eq!(l["concepts"], 2, "{l}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn no_concepts_does_not_mean_nothing_to_gate_on() {
let root = bundle(
"no-concepts-error",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/broken.md",
"---\ntype: [unclosed\n---\n\n# Broken\n",
),
],
);
let path = root.to_string_lossy().into_owned();
let out = roteiro(&["okf", "validate", &path]);
let text = String::from_utf8(out.stdout).expect("utf-8");
assert!(
!out.status.success(),
"an unreadable document is an error however few concepts survived it: {text}"
);
assert!(
text.contains("no concepts examined"),
"and it still says nothing was examined: {text}"
);
assert!(text.contains("broken.md"), "and names the file: {text}");
let _ = std::fs::remove_dir_all(&root);
}
fn expiring_bundle(tag: &str) -> PathBuf {
bundle(
tag,
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/revenue.md",
"---\ntype: Metric\ntitle: Revenue\nstale_after: 2026-12-31T00:00:00Z\n\
verified:\n - by: human:alice\n at: 2026-01-01T00:00:00Z\n---\n\n\
# Revenue\n\nTotal revenue.\n",
),
],
)
}
#[test]
fn trust_judges_staleness_against_the_given_day_and_gates_on_it() {
let root = expiring_bundle("stale");
let path = root.to_string_lossy().into_owned();
let before = roteiro(&["okf", "trust", &path, "--today", "2026-12-30"]);
assert!(before.status.success());
assert!(
String::from_utf8_lossy(&before.stdout).contains("stale 0 (as of 2026-12-30)"),
"nothing is stale the day before"
);
let on = roteiro(&["okf", "trust", &path, "--today", "2026-12-31"]);
let on_stdout = String::from_utf8_lossy(&on.stdout).into_owned();
assert!(
on_stdout.contains("stale 1 (as of 2026-12-31)"),
"the day itself counts, because the rule is `now >= stale_after`; got:\n{on_stdout}"
);
assert!(
on_stdout.contains("[STALE since 2026-12-31T00:00:00Z]"),
"the concept line must say *since when*, not merely that it expired; got:\n{on_stdout}"
);
assert!(
on.status.success(),
"the bare command reports and does not gate"
);
assert!(
!roteiro(&["okf", "trust", &path, "--today", "2026-12-31", "--check"])
.status
.success(),
"--check must gate on staleness"
);
assert!(
roteiro(&["okf", "trust", &path, "--today", "2026-12-30", "--check"])
.status
.success(),
"--check must not gate when nothing is stale"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_stale_gate_is_the_same_with_and_without_json() {
let root = expiring_bundle("stale-json");
let path = root.to_string_lossy().into_owned();
let text = roteiro(&["okf", "trust", &path, "--today", "2027-01-01", "--check"]);
let json = roteiro(&[
"okf",
"trust",
&path,
"--today",
"2027-01-01",
"--check",
"--json",
]);
assert_eq!(
text.status.success(),
json.status.success(),
"--json must not change whether the command gates"
);
assert!(!json.status.success(), "and both must fail here");
let v: serde_json::Value = serde_json::from_slice(&json.stdout).expect("parseable JSON");
assert_eq!(v["stale"], 1);
assert_eq!(v["today"], "2027-01-01");
assert_eq!(v["concepts"][0]["stale"], true);
assert_eq!(v["concepts"][0]["stale_after"], "2026-12-31T00:00:00Z");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_malformed_today_fails_the_command() {
let root = expiring_bundle("stale-bad-date");
let out = roteiro(&[
"okf",
"trust",
&root.to_string_lossy(),
"--today",
"yesterday",
]);
assert!(
!out.status.success(),
"a non-ISO date must fail the command"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("is not an ISO date"),
"and must say so by name"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn computations_are_listed_and_only_incomplete_ones_gate() {
let complete = bundle(
"computations-ok",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"c/total.md",
"---\ntype: Attested Computation\ntitle: Total\nruntime: bigquery\n---\n\n\
# Computation\n\n```sql\nSELECT 1\n```\n",
),
],
);
let out = roteiro(&["okf", "computations", &complete.to_string_lossy()]);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(out.status.success());
assert!(
stdout.contains("c/total — bigquery, inline sql, 1 line(s)"),
"the listing must say the runtime and where the code is; got:\n{stdout}"
);
assert!(
roteiro(&[
"okf",
"computations",
&complete.to_string_lossy(),
"--check"
])
.status
.success(),
"a complete contract must not gate"
);
let broken = bundle(
"computations-no-runtime",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"c/total.md",
"---\ntype: Attested Computation\ntitle: Total\n---\n\n\
# Computation\n\n```sql\nSELECT 1\n```\n",
),
],
);
let broken_out = roteiro(&["okf", "computations", &broken.to_string_lossy()]);
let broken_stdout = String::from_utf8_lossy(&broken_out.stdout).into_owned();
assert!(
broken_stdout.contains("no code 0"),
"the per-kind line counts what it says; got:\n{broken_stdout}"
);
assert!(
broken_stdout.contains("1 contract(s) incomplete"),
"and the total the gate uses is stated too; got:\n{broken_stdout}"
);
assert!(
!broken_stdout.contains("incomplete 0"),
"the report must never say `incomplete 0` about a bundle it then gates on; \
got:\n{broken_stdout}"
);
assert!(
!roteiro(&["okf", "computations", &broken.to_string_lossy(), "--check"])
.status
.success(),
"a contract with no runtime must gate under --check"
);
assert!(
roteiro(&["okf", "computations", &broken.to_string_lossy()])
.status
.success(),
"and must not gate without it"
);
let none = bundle(
"computations-none",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/revenue.md",
"---\ntype: Metric\ntitle: Revenue\n---\n\n# Revenue\n\nTotal.\n",
),
],
);
let out = roteiro(&["okf", "computations", &none.to_string_lossy(), "--check"]);
assert!(out.status.success(), "declaring none is conformant");
assert!(
String::from_utf8_lossy(&out.stdout).contains("declares no attested computations"),
"and must say so, rather than printing an empty listing that reads as a failure"
);
for root in [complete, broken, none] {
let _ = std::fs::remove_dir_all(&root);
}
}
#[test]
fn info_summarises_without_gating() {
let root = expiring_bundle("info");
let out = roteiro(&[
"okf",
"info",
&root.to_string_lossy(),
"--today",
"2027-01-01",
]);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"info reports; the other commands gate — that is the division of labour"
);
for expected in [
"okf_version: 0.2",
"concepts: 1",
"stale: 1 (as of 2027-01-01)",
"internal links: 0, broken 0",
"computations: 0, incomplete 0",
] {
assert!(
stdout.contains(expected),
"info must report `{expected}`; got:\n{stdout}"
);
}
let v: serde_json::Value = serde_json::from_slice(
&roteiro(&[
"okf",
"info",
&root.to_string_lossy(),
"--today",
"2027-01-01",
"--json",
])
.stdout,
)
.expect("parseable JSON");
assert_eq!(v["trust"]["stale"], 1);
assert_eq!(v["concepts"], 1);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_named_gates_are_the_commands_that_actually_gate() {
let root = bundle(
"gate-matrix",
&[
("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Bundle\n"),
(
"metrics/revenue.md",
"---\ntype: Metric\ntitle: Revenue\nstale_after: 2026-12-31T00:00:00Z\n\
verified:\n - by: human:alice\n at: 2026-01-01T00:00:00Z\n---\n\n\
# Revenue\n\nSee [missing](absent.md).\n",
),
],
);
let p = root.to_string_lossy().into_owned();
let ok = |args: &[&str]| roteiro(args).status.success();
assert!(ok(&["okf", "lint", &p]), "`lint` never gates");
assert!(ok(&["okf", "links", &p]), "bare `links` reports");
assert!(
ok(&["okf", "trust", &p, "--today", "2027-01-01"]),
"bare `trust` reports"
);
assert!(
ok(&["okf", "info", &p, "--today", "2027-01-01"]),
"`info` never gates"
);
assert!(
!ok(&["okf", "links", &p, "--check"]),
"`links --check` gates on a broken link"
);
assert!(
!ok(&["okf", "trust", &p, "--today", "2027-01-01", "--check"]),
"`trust --check` gates on staleness — the gate this omitted"
);
let stdout =
String::from_utf8_lossy(&roteiro(&["okf", "info", &p, "--today", "2027-01-01"]).stdout)
.into_owned();
let gates = stdout
.lines()
.find(|l| l.trim_start().starts_with("gates:"))
.expect("info names its gates");
let reports = stdout
.lines()
.find(|l| l.trim_start().starts_with("reports only:"))
.expect("info names what only reports");
assert!(
gates.contains("trust"),
"the stale gate must be named: {gates}"
);
assert!(
!gates.contains("lint"),
"`lint` must not be named as a gate: {gates}"
);
assert!(
reports.contains("lint"),
"`lint` belongs with the reporters: {reports}"
);
let _ = std::fs::remove_dir_all(&root);
}