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 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"}"#,
r#"{"schema_version":2,"seq":8,"ts":1007,"idempotency_key":"phase_entered:dist","kind":"phase_entered","phase":"dist"}"#,
r#"{"schema_version":2,"seq":9,"ts":1008,"idempotency_key":"phase_completed:dist","kind":"phase_completed","phase":"dist","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"], 9);
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(), 9, "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 seed_journal_multi(runs: &[(&str, &[&str])]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
for (run_id, lines) in runs {
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
}
const RUN_CREATED_A: &str = r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNA","plan_id":"plan-aaa","version":"1.0.0","targets":["cargo"]}"#;
#[test]
fn release_list_empty_is_a_normal_empty_list() {
let dir = tempfile::tempdir().unwrap();
let out = ossctl()
.args(["release", "list", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "empty list must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
assert_eq!(v["schema_version"], 1);
assert_eq!(v["data"]["runs"].as_array().unwrap().len(), 0);
assert_eq!(v["data"]["in_flight_count"], 0);
assert_eq!(v["data"]["unreadable"].as_array().unwrap().len(), 0);
}
#[test]
fn release_list_tiebreaks_equal_start_times_by_run_id() {
let mk = |id: &str| {
format!(
r#"{{"schema_version":1,"seq":1,"ts":777,"idempotency_key":"run_created","kind":"run_created","run_id":"{id}","plan_id":"plan","version":"1.0.0","targets":["cargo"]}}"#
)
};
let z = mk("RUNZ");
let a = mk("RUNA");
let dir = seed_journal_multi(&[("RUNZ", &[z.as_str()]), ("RUNA", &[a.as_str()])]);
let out = ossctl()
.args(["release", "list", "--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 ids: Vec<&str> = v["data"]["runs"]
.as_array()
.unwrap()
.iter()
.map(|r| r["run_id"].as_str().unwrap())
.collect();
assert_eq!(ids, vec!["RUNA", "RUNZ"], "equal ts → sorted by run_id");
}
#[test]
fn release_list_surfaces_unreadable_runs() {
let good = &[RUN_CREATED_A][..];
let bad = &[
r#"{"schema_version":9999,"seq":1,"ts":2000,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNBAD","plan_id":"p","version":"9.9.9","targets":["cargo"]}"#,
][..];
let dir = seed_journal_multi(&[("RUNA", good), ("RUNBAD", bad)]);
let out = ossctl()
.args(["release", "list", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "list stays exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
let ids: Vec<&str> = v["data"]["runs"]
.as_array()
.unwrap()
.iter()
.map(|r| r["run_id"].as_str().unwrap())
.collect();
assert_eq!(ids, vec!["RUNA"]);
assert_eq!(
v["data"]["unreadable"],
serde_json::json!(["RUNBAD"]),
"an unreadable run must be surfaced, not silently dropped"
);
assert!(
v["warnings"]
.as_array()
.unwrap()
.iter()
.any(|w| w.as_str().unwrap().contains("RUNBAD")),
"unreadable run needs a warning: {}",
v["warnings"]
);
}
#[test]
fn release_list_reports_a_single_in_flight_run() {
let dir = seed_journal_multi(&[("RUNA", &[RUN_CREATED_A])]);
let out = ossctl()
.args(["release", "list", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "list must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
let runs = v["data"]["runs"].as_array().unwrap();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0]["run_id"], "RUNA");
assert_eq!(runs[0]["status"], "in_progress");
assert_eq!(runs[0]["version"], "1.0.0");
assert_eq!(runs[0]["tag"], "v1.0.0");
assert_eq!(runs[0]["plan_id"], "plan-aaa");
assert_eq!(runs[0]["in_flight"], true);
assert_eq!(runs[0]["started_ts"], 1000);
assert_eq!(v["data"]["in_flight_count"], 1);
}
#[test]
fn release_list_reports_all_statuses_sorted_by_start_time() {
let in_progress = &[RUN_CREATED_A][..];
let completed = &[
r#"{"schema_version":1,"seq":1,"ts":500,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNB","plan_id":"plan-bbb","version":"2.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":501,"idempotency_key":"phase_completed:tag","kind":"phase_completed","phase":"tag","outcome":"ok"}"#,
][..];
let abandoned = &[
r#"{"schema_version":1,"seq":1,"ts":1500,"idempotency_key":"run_created","kind":"run_created","run_id":"RUNC","plan_id":"plan-ccc","version":"3.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1501,"idempotency_key":"run_abandoned","kind":"run_abandoned","reason":"gave up"}"#,
][..];
let dir = seed_journal_multi(&[
("RUNA", in_progress),
("RUNB", completed),
("RUNC", abandoned),
]);
let out = ossctl()
.args(["release", "list", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "list must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
let runs = v["data"]["runs"].as_array().unwrap();
assert_eq!(runs.len(), 3);
let ids: Vec<&str> = runs.iter().map(|r| r["run_id"].as_str().unwrap()).collect();
assert_eq!(ids, vec!["RUNB", "RUNA", "RUNC"]);
let statuses: Vec<&str> = runs.iter().map(|r| r["status"].as_str().unwrap()).collect();
assert_eq!(statuses, vec!["completed", "in_progress", "abandoned"]);
assert_eq!(v["data"]["in_flight_count"], 1);
assert_eq!(runs[2]["abandon_reason"], "gave up");
}
#[test]
fn release_abandon_marks_a_non_terminal_run_and_keeps_publishes() {
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}}"#,
],
);
let out = ossctl()
.args([
"release",
"abandon",
"RUN01",
"--reason",
"registry outage",
"--json",
"--journal-dir",
])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "abandon must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
assert_eq!(v["data"]["run_id"], "RUN01");
assert_eq!(v["data"]["status"], "abandoned");
assert_eq!(v["data"]["reason"], "registry outage");
assert_eq!(v["data"]["published_targets"], serde_json::json!(["cargo"]));
let warnings = v["warnings"].as_array().unwrap();
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("cargo")),
"a published target must be surfaced as still-live: {warnings:?}"
);
let show = ossctl()
.args(["release", "show", "RUN01", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(show.status.success());
let sv: serde_json::Value = serde_json::from_slice(&show.stdout).unwrap();
assert_eq!(sv["data"]["state"]["status"], "abandoned");
assert_eq!(sv["data"]["state"]["abandon_reason"], "registry outage");
assert_eq!(sv["data"]["last_seq"], 3);
}
#[test]
fn release_abandon_without_reason_uses_a_default() {
let dir = seed_journal("RUN01", &[RUN_CREATED_A.replace("RUNA", "RUN01").as_str()]);
let out = ossctl()
.args(["release", "abandon", "RUN01", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
assert!(out.status.success(), "abandon must exit 0: {out:?}");
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
assert_eq!(v["data"]["status"], "abandoned");
assert!(
v["data"]["reason"].as_str().unwrap().contains("no reason"),
"default reason expected: {}",
v["data"]["reason"]
);
assert_eq!(v["data"]["published_targets"].as_array().unwrap().len(), 0);
}
#[test]
fn release_abandon_rejects_a_blank_reason() {
let dir = seed_journal("RUN01", &[RUN_CREATED_A.replace("RUNA", "RUN01").as_str()]);
let out = ossctl()
.args([
"release",
"abandon",
"RUN01",
"--reason",
" ",
"--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).unwrap();
assert_eq!(v["error"]["code"], "invalid_reason");
}
#[test]
fn release_abandon_rejects_a_control_character_reason() {
let dir = seed_journal("RUN01", &[RUN_CREATED_A.replace("RUNA", "RUN01").as_str()]);
let out = ossctl()
.args(["release", "abandon", "RUN01", "--reason"])
.arg("line one\nrun OTHER completed")
.args(["--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).unwrap();
assert_eq!(v["error"]["code"], "invalid_reason");
let show = ossctl()
.args(["release", "show", "RUN01", "--json", "--journal-dir"])
.arg(dir.path())
.output()
.unwrap();
let sv: serde_json::Value = serde_json::from_slice(&show.stdout).unwrap();
assert_eq!(sv["data"]["state"]["status"], "in_progress");
}
#[test]
fn release_abandon_refuses_terminal_runs() {
let completed = seed_journal(
"DONE1",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"DONE1","plan_id":"plan-abc","version":"1.0.0","targets":["cargo"]}"#,
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",
"abandon",
"DONE1",
"--reason",
"too late",
"--json",
"--journal-dir",
])
.arg(completed.path())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(1),
"completed → user error: {out:?}"
);
let v: serde_json::Value = serde_json::from_slice(&out.stderr).unwrap();
assert_eq!(v["error"]["code"], "run_completed");
let abandoned = seed_journal(
"GONE1",
&[
r#"{"schema_version":1,"seq":1,"ts":1000,"idempotency_key":"run_created","kind":"run_created","run_id":"GONE1","plan_id":"plan-abc","version":"1.0.0","targets":["cargo"]}"#,
r#"{"schema_version":1,"seq":2,"ts":1001,"idempotency_key":"run_abandoned","kind":"run_abandoned","reason":"first"}"#,
],
);
let out = ossctl()
.args([
"release",
"abandon",
"GONE1",
"--reason",
"again",
"--json",
"--journal-dir",
])
.arg(abandoned.path())
.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"], "run_already_abandoned");
}
#[test]
fn release_abandon_unknown_and_bad_run_ids_are_user_errors() {
let dir = tempfile::tempdir().unwrap();
let missing = ossctl()
.args([
"release",
"abandon",
"NOPE",
"--reason",
"x",
"--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",
"abandon",
"../escape",
"--reason",
"x",
"--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:?}");
}
}
fn write_dist_contract(dir: &std::path::Path, installers: &str) {
let doc = format!(
"---\n\
schema_version: 1\n\
status: approved\n\
maturity: mvp\n\
ecosystems: [rust]\n\
versioning: semver\n\
changelog: {{mode: fragment, source: issuectl-trailers}}\n\
conventional_commits: false\n\
release: {{model: gated, layout: single}}\n\
contribution_provenance: none\n\
provenance_level: keyless\n\
dependency_bot: dependabot\n\
health_badges: [ci]\n\
license: MIT\n\
docs_site: none\n\
distribution:\n\
\x20 adapter: cargo-dist\n\
\x20 installers: [{installers}]\n\
---\n\n# Test\n"
);
std::fs::write(dir.join("OSS-RELEASE.md"), doc).unwrap();
}
#[test]
fn dist_generate_writes_config_and_reports_json() {
let dir = tempfile::tempdir().unwrap();
write_dist_contract(dir.path(), "shell, powershell");
let out = ossctl()
.args(["dist", "generate", "--no-workflow", "--json", "--repo-root"])
.arg(dir.path())
.output()
.unwrap();
assert!(
out.status.success(),
"exit 0: {}",
String::from_utf8_lossy(&out.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is JSON");
assert_eq!(v["schema_version"], 1);
let data = &v["data"];
assert_eq!(data["dist_config"], "dist-workspace.toml");
assert_eq!(
data["workflow"],
serde_json::Value::Null,
"--no-workflow → null"
);
assert_eq!(data["cargo_dist_version"], "0.28.2");
assert_eq!(
data["installers"],
serde_json::json!(["shell", "powershell"])
);
let targets = data["targets"].as_array().unwrap();
assert!(
targets
.iter()
.any(|t| t.as_str().unwrap().contains("linux")),
"Linux target: {data}"
);
assert!(
targets
.iter()
.any(|t| t.as_str().unwrap().contains("darwin")),
"macOS target: {data}"
);
let toml = std::fs::read_to_string(dir.path().join("dist-workspace.toml")).unwrap();
assert!(toml.contains("pr-run-mode = \"skip\""), "{toml}");
assert!(toml.contains("github-attestations = true"), "{toml}");
assert!(
!toml.contains("github-custom-runners"),
"no personal runner override: {toml}"
);
}
#[test]
fn dist_generate_without_distribution_is_user_error() {
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(["dist", "generate", "--no-workflow", "--repo-root"])
.arg(dir.path())
.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"], "no_distribution");
assert!(
!dir.path().join("dist-workspace.toml").exists(),
"nothing written"
);
}
#[test]
fn dist_generate_refuses_to_clobber_without_force() {
let dir = tempfile::tempdir().unwrap();
write_dist_contract(dir.path(), "shell");
std::fs::write(dir.path().join("dist-workspace.toml"), "# mine\n").unwrap();
let out = ossctl()
.args(["dist", "generate", "--no-workflow", "--repo-root"])
.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 is JSON");
assert_eq!(v["error"]["code"], "dist_config_exists");
assert_eq!(
std::fs::read_to_string(dir.path().join("dist-workspace.toml")).unwrap(),
"# mine\n",
"the hand-tuned config is preserved"
);
}