use assert_cmd::Command;
use predicates::prelude::*;
fn ossctl() -> Command {
Command::cargo_bin("ossctl").expect("ossctl binary builds")
}
#[test]
fn version_json_emits_schema_fields() {
let out = ossctl().args(["version", "--json"]).output().unwrap();
assert!(out.status.success(), "version --json must exit 0");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is JSON");
assert_eq!(v["schema_version"], 1, "envelope schema_version");
assert_eq!(
v["warnings"],
serde_json::json!([]),
"warnings always present"
);
let data = &v["data"];
assert!(data["version"].is_string(), "version present: {data}");
assert!(data["commit"].is_string(), "commit present: {data}");
assert_eq!(data["schema_version"], 1, "data schema_version");
assert_eq!(
data["supported_schemas"],
serde_json::json!([1]),
"supported_schemas present"
);
assert!(
data["skills"].is_array(),
"skills is an array (empty at founding): {data}"
);
}
#[test]
fn version_text_is_default() {
ossctl()
.arg("version")
.assert()
.success()
.stdout(predicate::str::starts_with("ossctl "));
}
#[test]
fn doctor_runs_and_exits_zero() {
ossctl()
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("summary:"));
}
#[test]
fn doctor_json_shape() {
let out = ossctl().args(["doctor", "--json"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert!(v["data"]["checks"].is_array(), "checks array: {v}");
assert!(v["data"]["summary"]["ok"].is_u64(), "summary.ok: {v}");
}
#[test]
fn doctor_json_fix_leaves_stderr_clean() {
let out = ossctl()
.args(["doctor", "--json", "--fix"])
.output()
.unwrap();
assert!(out.status.success());
assert!(
out.stderr.is_empty(),
"JSON-mode --fix must not narrate on stderr: {:?}",
String::from_utf8_lossy(&out.stderr)
);
let _: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is JSON");
}
#[test]
fn stub_subcommand_returns_not_implemented() {
let out = ossctl().args(["release", "list"]).output().unwrap();
assert_eq!(out.status.code(), Some(2), "not_implemented → exit 2");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["schema_version"], 1);
assert_eq!(v["error"]["code"], "not_implemented");
assert!(out.stdout.is_empty(), "no data on stdout for an error");
}
#[test]
fn unknown_subcommand_is_structured_error() {
let out = ossctl().arg("frobnicate").output().unwrap();
assert_eq!(out.status.code(), Some(1), "user error → exit 1");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["error"]["code"], "unknown_subcommand");
}
fn fixture(name: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
#[test]
fn contract_show_positive_fixtures() {
for name in ["solo-rust-cli", "node-monorepo", "python-lib", "go-cli"] {
let out = ossctl()
.args(["contract", "show", "--json", "--repo-root"])
.arg(fixture(name))
.output()
.unwrap();
assert!(out.status.success(), "{name} must show cleanly");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
assert_eq!(v["schema_version"], 1, "{name} envelope");
let data = &v["data"];
assert_eq!(data["schema_version"], 1, "{name} contract schema_version");
assert!(data["maturity"].is_string(), "{name} maturity: {data}");
assert!(data["targets"].is_array(), "{name} targets: {data}");
assert!(data["extra_fields"].is_object(), "{name} extra_fields");
assert!(data["warnings"].is_array(), "{name} warnings");
assert!(data["versioning_pattern"].is_null() || data["versioning_pattern"].is_string());
}
}
#[test]
fn contract_show_expands_omitted_targets() {
let out = ossctl()
.args(["contract", "show", "--json", "--repo-root"])
.arg(fixture("python-lib"))
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let targets = v["data"]["targets"].as_array().unwrap();
assert_eq!(targets.len(), 1);
assert_eq!(targets[0]["ecosystem"], "python");
assert_eq!(targets[0]["registry"], "pypi");
assert_eq!(targets[0]["adapter"], "gh-action-pypi-publish");
assert!(targets[0]["package"].is_null());
}
#[test]
fn contract_show_preserves_unknown_fields() {
let out = ossctl()
.args(["contract", "show", "--json", "--repo-root"])
.arg(fixture("go-cli"))
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(
v["data"]["extra_fields"]["roadmap_url"],
"https://example.com/roadmap"
);
let warnings = v["data"]["warnings"].as_array().unwrap();
assert!(warnings
.iter()
.any(|w| w.as_str().is_some_and(|s| s.contains("roadmap_url"))));
}
#[test]
fn contract_show_is_idempotent() {
let run = || {
ossctl()
.args(["contract", "show", "--json", "--repo-root"])
.arg(fixture("solo-rust-cli"))
.output()
.unwrap()
.stdout
};
assert_eq!(run(), run(), "contract show must be deterministic");
}
#[test]
fn contract_validate_rejects_floor_violations() {
for name in [
"neg-auto-on-spike",
"neg-registry-without-license",
"neg-badge-without-producer",
"neg-schema-too-new",
"neg-fragment-dir-escape",
] {
let out = ossctl()
.args(["contract", "validate", "--repo-root"])
.arg(fixture(name))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "{name} → exit 1");
assert!(out.stdout.is_empty(), "{name} emits no body on failure");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "invalid_contract", "{name}");
assert!(
v["error"]["problems"]
.as_array()
.is_some_and(|p| !p.is_empty()),
"{name} carries the problem list: {v}"
);
}
}
#[test]
fn contract_show_rejects_invalid() {
let out = ossctl()
.args(["contract", "show", "--json", "--repo-root"])
.arg(fixture("neg-auto-on-spike"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(out.stdout.is_empty(), "no canonical body on invalid");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).unwrap();
assert_eq!(v["error"]["code"], "invalid_contract");
}
#[test]
fn contract_validate_accepts_valid() {
ossctl()
.args(["contract", "validate", "--repo-root"])
.arg(fixture("python-lib"))
.assert()
.success();
}
#[test]
fn contract_require_approved_refuses_draft() {
let out = ossctl()
.args(["contract", "show", "--require-approved", "--repo-root"])
.arg(fixture("solo-rust-cli")) .output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).unwrap();
assert_eq!(v["error"]["code"], "not_approved");
assert_eq!(v["error"]["invalid_value"], "draft");
}
#[test]
fn contract_show_missing_file_is_system_error() {
let dir = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["contract", "show", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2), "missing file → exit 2");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).unwrap();
assert_eq!(v["error"]["code"], "contract_not_found");
}
#[test]
fn validate_verdict_is_independent_of_repo_filesystem() {
for name in [
"solo-rust-cli",
"python-lib",
"neg-auto-on-spike",
"neg-registry-without-license",
"neg-badge-without-producer",
"neg-schema-too-new",
"neg-fragment-dir-escape",
] {
let doc = std::fs::read(fixture(name).join("OSS-RELEASE.md")).unwrap();
let staging = tempfile::tempdir().unwrap();
std::fs::write(staging.path().join("OSS-RELEASE.md"), &doc).unwrap();
let real = tempfile::tempdir().unwrap();
std::fs::write(real.path().join("OSS-RELEASE.md"), &doc).unwrap();
std::fs::create_dir_all(real.path().join(".git")).unwrap();
std::fs::create_dir_all(real.path().join(".github/workflows")).unwrap();
std::fs::create_dir_all(real.path().join("changelog/fragments")).unwrap();
std::fs::write(real.path().join("Cargo.toml"), "[package]\nname=\"x\"\n").unwrap();
std::fs::write(real.path().join("package.json"), "{}\n").unwrap();
std::fs::write(real.path().join("README.md"), "# x\n").unwrap();
let run = |root: &std::path::Path| {
let out = ossctl()
.args(["contract", "validate", "--json", "--repo-root"])
.arg(root)
.output()
.unwrap();
let stream = if out.status.success() {
&out.stdout
} else {
&out.stderr
};
let v: serde_json::Value = serde_json::from_slice(stream).unwrap();
(
out.status.code(),
v["error"]["code"].as_str().unwrap_or("").to_string(),
)
};
assert_eq!(
run(staging.path()),
run(real.path()),
"{name}: validate verdict must be identical at a bare staging root and a populated \
real repo root (the staging-root invariant the oss-init skill depends on)"
);
}
}
#[test]
fn facts_json_shape_and_field_names() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"toolx\"\nversion = \"0.3.0\"\ndescription = \"a tool\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join(".github/workflows")).unwrap();
std::fs::write(dir.path().join(".github/workflows/ci.yml"), "on: push\n").unwrap();
let out = ossctl()
.args(["facts", "--json", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "facts --json must exit 0");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is JSON");
assert_eq!(v["schema_version"], 1, "envelope schema_version");
assert_eq!(v["warnings"], serde_json::json!([]));
let d = &v["data"];
for key in [
"ecosystems",
"packages",
"has_ci",
"tags",
"committers_total",
"committers_recent_year",
"inferred_maturity",
"maturity_signals",
"has_semver_tag",
"has_ge_1_0_release",
] {
assert!(d.get(key).is_some(), "missing facts key {key}: {d}");
}
assert_eq!(d["ecosystems"], serde_json::json!(["rust"]));
assert_eq!(d["packages"][0]["package"], "toolx");
assert_eq!(d["packages"][0]["version"], "0.3.0");
assert_eq!(d["has_ci"], true);
assert_eq!(d["is_git"], false, "a temp dir is not a git work tree");
assert_eq!(d["committers_total"], 0);
assert_eq!(d["tags"], serde_json::json!([]));
assert_eq!(d["description"], "a tool");
assert_eq!(d["inferred_maturity"], "mvp");
}
#[test]
fn facts_binary_fallback_is_spike() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Makefile"), "all:\n\techo hi\n").unwrap();
let out = ossctl()
.args(["facts", "--json", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["data"]["ecosystems"], serde_json::json!(["binary"]));
assert_eq!(v["data"]["inferred_maturity"], "spike");
}
#[test]
fn facts_is_deterministic() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"name": "widget", "version": "1.0.0"}"#,
)
.unwrap();
let run = || {
ossctl()
.args(["facts", "--json", "--repo-root"])
.arg(dir.path())
.output()
.unwrap()
.stdout
};
assert_eq!(run(), run(), "facts must be deterministic");
}
#[test]
fn facts_missing_repo_root_is_user_error() {
let out = ossctl()
.args(["facts", "--repo-root", "/no/such/dir/anywhere"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "invalid_repo_root");
}
fn dir_snapshot(path: &std::path::Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(path)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
#[test]
fn audit_json_reports_core_gaps_and_is_read_only() {
let dir = tempfile::tempdir().unwrap();
std::fs::copy(
fixture("python-lib").join("OSS-RELEASE.md"),
dir.path().join("OSS-RELEASE.md"),
)
.unwrap();
let before = dir_snapshot(dir.path());
let out = ossctl()
.args(["audit", "--json", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "audit --json must exit 0");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is JSON");
assert_eq!(v["schema_version"], 1);
assert_eq!(v["warnings"], serde_json::json!([]));
let d = &v["data"];
assert!(d["maturity"].is_string(), "maturity present: {d}");
assert_eq!(d["core_complete"], "incomplete", "no README/LICENSE/CI");
let gaps = d["gaps"].as_array().expect("gaps is an array");
let gap_ids: Vec<&str> = gaps.iter().map(|g| g["id"].as_str().unwrap()).collect();
assert!(gap_ids.contains(&"readme"), "readme gap: {gap_ids:?}");
assert!(gap_ids.contains(&"license"), "license gap: {gap_ids:?}");
let cp = &d["community_profile"];
assert_eq!(cp["checked"], false, "no GitHub remote → unchecked");
assert_eq!(cp["readme"], "unknown", "outage ⇒ unknown, never absent");
assert_eq!(
dir_snapshot(dir.path()),
before,
"audit must not write the repo"
);
}
#[test]
fn audit_missing_contract_is_system_error() {
let dir = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["audit", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2), "missing contract → exit 2");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "contract_not_found");
}
#[test]
fn audit_missing_repo_root_is_user_error() {
let out = ossctl()
.args(["audit", "--repo-root", "/no/such/dir/anywhere"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "invalid_repo_root");
}
fn seed_journal(run_id: &str, lines: &[&str]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path().join(run_id);
std::fs::create_dir_all(&run_dir).unwrap();
std::fs::write(
run_dir.join("journal.jsonl"),
format!("{}\n", lines.join("\n")),
)
.unwrap();
dir
}
#[test]
fn release_verify_reconciles_a_journaled_run() {
let dir = seed_journal(
"RUN01",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUN01","plan_id":"plan-abc","version":"1.0.0","targets":["cargo","gh"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"published:cargo","kind":"target_published","target":"cargo","receipt":{"ecosystem":"rust","package":"tool","version":"1.0.0","registry_url":null,"digest":null}}"#,
r#"{"schema_version":1,"seq":3,"ts":1002,"idempotency_key":"published:gh","kind":"target_published","target":"gh","receipt":{"ecosystem":"binary","package":"tool","version":"1.0.0","registry_url":null,"digest":null}}"#,
],
);
let out = ossctl()
.args(["release", "verify", "RUN01", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "verify must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
assert_eq!(v["schema_version"], 1);
let data = &v["data"];
assert_eq!(data["run_id"], "RUN01");
assert_eq!(data["plan_id"], "plan-abc");
assert_eq!(data["summary"]["reconciled"], 2);
assert_eq!(data["summary"]["unknown"], 2);
assert_eq!(data["summary"]["missing"], 0);
let outcomes: Vec<&str> = data["targets"]
.as_array()
.unwrap()
.iter()
.map(|t| t["outcome"].as_str().unwrap())
.collect();
assert_eq!(outcomes, vec!["unknown", "unknown"]);
assert!(
!dir.path().join("RUN01").join("manifest.json").exists(),
"verify wrote a manifest — it must be read-only"
);
}
#[test]
fn release_verify_unknown_run_is_user_error() {
let dir = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["release", "verify", "NOPE", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "run_not_found");
}
#[test]
fn release_verify_rejects_bad_run_id() {
let dir = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["release", "verify", "../escape", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "invalid_run_id");
}
#[test]
fn release_verify_warns_about_unpublished_targets() {
let dir = seed_journal(
"RUN02",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUN02","plan_id":"plan-xyz","version":"1.0.0","targets":["cargo","npm"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"published:cargo","kind":"target_published","target":"cargo","receipt":{"ecosystem":"rust","package":"tool","version":"2.0.0","registry_url":null,"digest":null}}"#,
],
);
let out = ossctl()
.args(["release", "verify", "RUN02", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(
v["data"]["summary"]["reconciled"], 1,
"only the published target reconciles"
);
let warnings = v["warnings"].as_array().unwrap();
assert!(
warnings.iter().any(|w| w.as_str().unwrap().contains("npm")),
"the unpublished target must be surfaced as a warning: {warnings:?}"
);
}
#[test]
fn release_verify_tolerates_a_torn_final_line() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path().join("RUN03");
std::fs::create_dir_all(&run_dir).unwrap();
let good = concat!(
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUN03","plan_id":"plan-torn","version":"1.0.0","targets":["cargo"]}"#,
"\n",
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"published:cargo","kind":"target_published","target":"cargo","receipt":{"ecosystem":"rust","package":"tool","version":"1.0.0","registry_url":null,"digest":null}}"#,
"\n",
r#"{"schema_version":1,"seq":3,"ts":1002,"idempotency_key":"published:npm","kind":"target_publ"#, );
std::fs::write(run_dir.join("journal.jsonl"), good).unwrap();
let out = ossctl()
.args(["release", "verify", "RUN03", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(
out.status.success(),
"a torn tail must not fail verify: {out:?}"
);
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["data"]["summary"]["reconciled"], 1);
assert_eq!(v["data"]["journal_seq"], 2);
}
#[test]
fn release_verify_reports_cancelled_targets_distinctly() {
let dir = seed_journal(
"RUN04",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUN04","plan_id":"plan-c","version":"1.0.0","targets":["cargo","npm"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"published:cargo","kind":"target_published","target":"cargo","receipt":{"ecosystem":"rust","package":"tool","version":"1.0.0","registry_url":null,"digest":null}}"#,
r#"{"schema_version":1,"seq":3,"ts":1002,"idempotency_key":"cancelled:npm","kind":"target_cancelled","target":"npm","reason":"OTP timeout"}"#,
],
);
let out = ossctl()
.args(["release", "verify", "RUN04", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let warnings = v["warnings"].as_array().unwrap();
assert!(
warnings.iter().any(|w| w
.as_str()
.unwrap()
.contains("npm' was cancelled: OTP timeout")),
"cancelled target must report its reason: {warnings:?}"
);
}
#[test]
fn release_show_post_mortem_summary_from_a_journal() {
let dir = seed_journal(
"SHOW01",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"SHOW01","plan_id":"plan-done","version":"1.2.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"phase_entered:publish","kind":"phase_entered","phase":"publish"}"#,
r#"{"schema_version":1,"seq":3,"ts":1002,"idempotency_key":"published:cargo","kind":"target_published","target":"cargo","receipt":{"ecosystem":"rust","package":"tool","version":"1.2.0","registry_url":null,"digest":null}}"#,
r#"{"schema_version":1,"seq":4,"ts":1003,"idempotency_key":"phase_completed:publish","kind":"phase_completed","phase":"publish","outcome":"ok"}"#,
r#"{"schema_version":1,"seq":5,"ts":1004,"idempotency_key":"phase_entered:tag","kind":"phase_entered","phase":"tag"}"#,
r#"{"schema_version":1,"seq":6,"ts":1005,"idempotency_key":"tag_created_local:v1.2.0","kind":"tag_created_local","tag":"v1.2.0"}"#,
r#"{"schema_version":1,"seq":7,"ts":1006,"idempotency_key":"phase_completed:tag","kind":"phase_completed","phase":"tag","outcome":"ok"}"#,
],
);
let out = ossctl()
.args(["release", "show", "SHOW01", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "show must exit 0: {out:?}");
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("summary is one JSON doc");
assert_eq!(v["schema_version"], 1);
let data = &v["data"];
assert_eq!(data["last_seq"], 7);
let state = &data["state"];
assert_eq!(state["run_id"], "SHOW01");
assert_eq!(state["plan_id"], "plan-done");
assert_eq!(state["version"], "1.2.0");
assert_eq!(state["status"], "completed");
assert_eq!(state["published"]["cargo"]["version"], "1.2.0");
let events = data["recent_events"].as_array().unwrap();
assert_eq!(events.len(), 7, "the whole (small) log fits the window");
assert_eq!(events.last().unwrap()["kind"], "phase_completed");
assert!(
!dir.path().join("SHOW01").join("manifest.json").exists(),
"show wrote a manifest — it must be read-only"
);
}
#[test]
fn release_show_post_mortem_surfaces_abandon_reason() {
let dir = seed_journal(
"SHOW02",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"SHOW02","plan_id":"plan-x","version":"1.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"run_abandoned","kind":"run_abandoned","reason":"OTP timeout"}"#,
],
);
let out = ossctl()
.args(["release", "show", "SHOW02", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["data"]["state"]["status"], "abandoned");
let warnings = v["warnings"].as_array().unwrap();
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("abandoned: OTP timeout")),
"abandon reason must be a warning: {warnings:?}"
);
}
#[test]
fn release_show_live_returns_the_same_snapshot_envelope() {
let dir = seed_journal(
"SHOW03",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"SHOW03","plan_id":"plan-live","version":"2.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"phase_entered:dry_run","kind":"phase_entered","phase":"dry_run"}"#,
r#"{"schema_version":1,"seq":3,"ts":1002,"idempotency_key":"dry_run:cargo","kind":"target_dry_run","target":"cargo"}"#,
],
);
let out = ossctl()
.args(["release", "show", "SHOW03", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "live show must exit 0: {out:?}");
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("a live poll is one envelope, not JSONL");
assert_eq!(v["schema_version"], 1);
let data = &v["data"];
assert_eq!(data["state"]["status"], "in_progress");
assert_eq!(data["last_seq"], 3);
assert_eq!(v["warnings"], serde_json::json!([]));
let events = data["recent_events"].as_array().unwrap();
assert_eq!(events.len(), 3);
assert_eq!(events[0]["kind"], "run_created");
assert_eq!(events[2]["kind"], "target_dry_run");
assert_eq!(events[2]["seq"], 3);
}
#[test]
fn release_show_with_journal_dir_ignores_repo_root() {
let dir = seed_journal(
"SHOW04",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"SHOW04","plan_id":"p","version":"1.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"run_abandoned","kind":"run_abandoned","reason":"stopped"}"#,
],
);
let out = ossctl()
.args([
"release",
"show",
"SHOW04",
"--json",
"--repo-root",
"/nonexistent/not/a/repo",
"--journal-dir",
])
.arg(dir.path())
.output()
.unwrap();
assert!(
out.status.success(),
"an explicit --journal-dir must not require a repo root: {out:?}"
);
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["data"]["state"]["run_id"], "SHOW04");
}
#[test]
fn release_show_unknown_and_bad_run_ids_are_user_errors() {
let dir = tempfile::tempdir().unwrap();
let missing = ossctl()
.args(["release", "show", "NOPE", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(missing.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&missing.stderr).unwrap();
assert_eq!(v["error"]["code"], "run_not_found");
let bad = ossctl()
.args(["release", "show", "../escape", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(bad.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&bad.stderr).unwrap();
assert_eq!(v["error"]["code"], "invalid_run_id");
}
fn approved_git_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let contract = std::fs::read_to_string(fixture("solo-rust-cli").join("OSS-RELEASE.md"))
.unwrap()
.replace("status: draft", "status: approved");
std::fs::write(dir.path().join("OSS-RELEASE.md"), contract).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"tool\"\n",
)
.unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.current_dir(dir.path())
.args(args)
.output()
.expect("git runs")
};
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
git(&["add", "."]);
git(&["commit", "-q", "-m", "init"]);
dir
}
#[test]
fn release_cut_refuses_a_draft_contract() {
let dir = tempfile::tempdir().unwrap();
std::fs::copy(
fixture("solo-rust-cli").join("OSS-RELEASE.md"),
dir.path().join("OSS-RELEASE.md"),
)
.unwrap();
let out = ossctl()
.args([
"release",
"cut",
"--plan",
"deadbeef",
"--version",
"1.0.0",
"--repo-root",
])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "draft cut → user error");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["error"]["code"], "not_approved");
assert!(out.stdout.is_empty());
}
#[test]
fn release_cut_refuses_a_stale_plan_id() {
let dir = approved_git_repo();
let journal = tempfile::tempdir().unwrap();
let out = ossctl()
.args([
"release",
"cut",
"--plan",
"0000000000000000",
"--version",
"1.0.0",
"--repo-root",
])
.arg(dir.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "stale plan → user error");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["error"]["code"], "plan_stale");
assert_eq!(v["error"]["invalid_value"], "0000000000000000");
assert!(out.stdout.is_empty(), "a refused cut published nothing");
}
#[test]
fn release_cut_refuses_when_version_differs_from_the_sealed_plan() {
let dir = approved_git_repo();
let journal = tempfile::tempdir().unwrap();
let planned = ossctl()
.args([
"release",
"plan",
"--json",
"--version",
"1.0.0",
"--repo-root",
])
.arg(dir.path())
.output()
.unwrap();
assert_eq!(
planned.status.code(),
Some(0),
"plan should succeed on an approved repo"
);
let pv: serde_json::Value =
serde_json::from_slice(&planned.stdout).expect("plan stdout is JSON");
let plan_id = pv["data"]["plan_id"]
.as_str()
.expect("plan_id present")
.to_string();
let out = ossctl()
.args([
"release",
"cut",
"--plan",
&plan_id,
"--version",
"2.0.0",
"--repo-root",
])
.arg(dir.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "wrong version → plan_stale");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["error"]["code"], "plan_stale");
assert!(out.stdout.is_empty());
}
#[test]
fn release_resume_unknown_run_is_user_error() {
let repo = tempfile::tempdir().unwrap();
let journal = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["release", "resume", "NOPE", "--json", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "run_not_found");
assert!(out.stdout.is_empty());
}
#[test]
fn release_resume_rejects_bad_run_id() {
let repo = tempfile::tempdir().unwrap();
let journal = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["release", "resume", "../escape", "--json", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "invalid_run_id");
}
#[test]
fn release_resume_completed_run_is_idempotent_success() {
let repo = tempfile::tempdir().unwrap();
let journal = seed_journal(
"RUNC",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNC","plan_id":"plan-done","version":"1.0.0","targets":["rust"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"phase_completed:tag","kind":"phase_completed","phase":"tag","outcome":"ok"}"#,
],
);
let out = ossctl()
.args(["release", "resume", "RUNC", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert!(
out.status.success(),
"completed run resume must exit 0: {out:?}"
);
assert!(String::from_utf8_lossy(&out.stdout).contains("already complete"));
}
#[test]
fn release_resume_abandoned_run_is_refused() {
let repo = tempfile::tempdir().unwrap();
let journal = seed_journal(
"RUNA",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNA","plan_id":"plan-x","version":"1.0.0","targets":["rust"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"run_abandoned","kind":"run_abandoned","reason":"OTP timeout"}"#,
],
);
let out = ossctl()
.args(["release", "resume", "RUNA", "--json", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "run_abandoned");
assert!(v["error"]["message"]
.as_str()
.unwrap()
.contains("OTP timeout"));
}
#[test]
fn release_resume_refuses_a_drifted_repo() {
let repo = approved_git_repo();
let journal = seed_journal(
"RUND",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUND","plan_id":"plan-mismatch","version":"1.0.0","targets":["rust"]}"#,
],
);
let out = ossctl()
.args(["release", "resume", "RUND", "--json", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "drifted repo → resume_drift");
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "resume_drift");
assert_eq!(v["error"]["expected"]["sealed_plan_id"], "plan-mismatch");
assert!(out.stdout.is_empty(), "a refused resume published nothing");
}
fn approved_python_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let contract = std::fs::read_to_string(fixture("python-lib").join("OSS-RELEASE.md"))
.unwrap()
.replace("status: draft", "status: approved")
.replace(
"ecosystems: [python]",
"ecosystems: [python]\ntargets:\n - {ecosystem: python, package: mytool, registry: pypi, adapter: twine}",
);
std::fs::write(dir.path().join("OSS-RELEASE.md"), contract).unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.current_dir(dir.path())
.args(args)
.output()
.expect("git runs")
};
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
git(&["add", "."]);
git(&["commit", "-q", "-m", "init"]);
dir
}
#[test]
fn release_resume_refuses_an_unverifiable_target() {
let repo = approved_python_repo();
let planned = ossctl()
.args([
"release",
"plan",
"--json",
"--version",
"1.0.0",
"--repo-root",
])
.arg(repo.path())
.output()
.unwrap();
assert_eq!(
planned.status.code(),
Some(0),
"plan should succeed: {planned:?}"
);
let pv: serde_json::Value = serde_json::from_slice(&planned.stdout).unwrap();
let plan_id = pv["data"]["plan_id"].as_str().unwrap().to_string();
let run_created = format!(
r#"{{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNU","plan_id":"{plan_id}","version":"1.0.0","targets":["python"]}}"#
);
let published = r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"published:python","kind":"target_published","target":"python","receipt":{"ecosystem":"python","package":"mytool","version":"1.0.0","registry_url":null,"digest":null}}"#;
let journal = seed_journal("RUNU", &[&run_created, published]);
let out = ossctl()
.args(["release", "resume", "RUNU", "--json", "--repo-root"])
.arg(repo.path())
.arg("--journal-dir")
.arg(journal.path())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(1),
"unverifiable target → resume_conflict"
);
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr JSON");
assert_eq!(v["error"]["code"], "resume_conflict");
let problems = v["error"]["problems"].as_array().expect("problems listed");
assert!(
problems
.iter()
.any(|p| p.as_str().unwrap().contains("rust")),
"the blocking target must be surfaced: {problems:?}"
);
assert!(out.stdout.is_empty());
}
#[test]
fn release_cut_rejects_a_git_ref_unsafe_version() {
for bad in ["1.0..0", "1.0.0~1", "../evil", "1.0.0.lock"] {
let out = ossctl()
.args([
"release",
"cut",
"--plan",
"x",
"--version",
bad,
"--repo-root",
".",
])
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(1),
"version {bad:?} should be rejected"
);
let v: serde_json::Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(v["error"]["code"], "invalid_version", "version {bad:?}");
}
}