use assert_cmd::Command;
const EXPECTED_JSON_STDOUT: &[u8] = b"{\n \"ok\": true,\n \"profile\": null,\n \"data\": {\n \"active\": null,\n \"profiles\": []\n }\n}\n";
const EXPECTED_HUMAN_STDOUT: &[u8] = b"";
fn noisy_ign() -> (tempfile::TempDir, Command) {
let dir = tempfile::tempdir().expect("tempdir");
let config = dir.path().join("config.toml");
std::fs::write(&config, "bogus_key = 1\n").expect("write noisy config");
let mut cmd = Command::cargo_bin("ign").expect("binary 'ign' not found");
cmd.env("IGNITION_CLI_CONFIG", &config);
cmd.env("IGNITION_LOG", "trace");
cmd.env_remove("IGNITION_PROFILE");
cmd.env_remove("IGNITION_JSON");
cmd.env_remove("IGNITION_YES");
(dir, cmd)
}
fn assert_stdout_bytes(actual: &[u8], expected: &[u8], label: &str) {
if actual == expected {
return;
}
let first_diff = actual
.iter()
.zip(expected.iter())
.position(|(a, e)| a != e)
.unwrap_or(actual.len().min(expected.len()));
panic!(
"{label}: stdout is not byte-pure under max diagnostics.\n first \
differing byte at offset {first_diff}\n actual len: {}, expected \
len: {}\n actual: {:?}\n expected: {:?}",
actual.len(),
expected.len(),
String::from_utf8_lossy(actual),
String::from_utf8_lossy(expected),
);
}
#[test]
fn stdout_is_byte_pure_under_max_diagnostics() {
let (_dir, mut cmd) = noisy_ign();
let output = cmd
.args(["profile", "list", "--json"])
.output()
.expect("spawn ign");
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_stdout_bytes(&output.stdout, EXPECTED_JSON_STDOUT, "json mode");
let text = std::str::from_utf8(&output.stdout).expect("stdout is valid UTF-8");
serde_json::from_slice::<serde_json::Value>(&output.stdout)
.expect("stdout parses as exactly one JSON value with nothing after it");
assert!(
text.trim_end().ends_with('}'),
"stdout ends inside a JSON value, not after it: {text:?}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("bogus_key"),
"expected the unknown-key warning on stderr (noise precondition): {stderr}"
);
}
#[test]
fn human_stdout_is_line_pure_under_max_diagnostics() {
let (_dir, mut cmd) = noisy_ign();
let output = cmd.args(["profile", "list"]).output().expect("spawn ign");
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_stdout_bytes(&output.stdout, EXPECTED_HUMAN_STDOUT, "human mode");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("bogus_key"),
"expected the unknown-key warning on stderr (noise precondition): {stderr}"
);
}
#[test]
fn tags_import_loss_prose_never_touches_stdout() {
let (_dir, mut cmd) = noisy_ign();
let dir = tempfile::tempdir().expect("tempdir");
let xml = dir.path().join("p5.xml");
std::fs::write(
&xml,
"<Tags MinVersion=\"8.0.0\" locale=\"en_US\">\r\n <Tag name=\"T1\" type=\"AtomicTag\">\r\n <Property name=\"valueSource\">memory</Property>\r\n </Tag>\r\n</Tags>\r\n",
)
.expect("write xml fixture");
let output = cmd
.args([
"tags",
"import",
"--format",
"xml",
"--file",
xml.to_str().expect("utf-8 path"),
"--provider",
"p5import",
])
.output()
.expect("spawn ign");
assert_eq!(
output.status.code(),
Some(2),
"the gate refuses exit 2 pre-resolution"
);
assert_stdout_bytes(&output.stdout, b"", "loss-gate refusal");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("loss report (xml)") && stderr.contains("re-run with --yes"),
"expected the loss report prose on stderr: {stderr}"
);
}
#[test]
fn tags_export_error_keeps_stdout_byte_empty() {
let (_dir, mut cmd) = noisy_ign();
let output = cmd
.args([
"tags",
"export",
"[default]P5",
"--format",
"xml",
"-o",
"-",
])
.output()
.expect("spawn ign");
assert_ne!(
output.status.code(),
Some(0),
"no gateway configured — must refuse"
);
assert_stdout_bytes(&output.stdout, b"", "xml export refusal");
}
#[test]
fn workspace_status_missing_manifest_refusal_never_touches_stdout() {
let (_dir, mut cmd) = noisy_ign();
let missing = tempfile::tempdir().expect("tempdir").path().join("nope");
let output = cmd
.args(["workspace", "status", missing.to_str().expect("utf-8 path")])
.output()
.expect("spawn ign");
assert_eq!(
output.status.code(),
Some(2),
"the pre-resolve refusal exits 2"
);
assert_stdout_bytes(&output.stdout, b"", "missing-manifest refusal");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("not an ign workspace"),
"expected the stable manifest refusal on stderr: {stderr}"
);
}
#[test]
fn workspace_push_and_checkout_refusals_keep_stdout_byte_empty() {
let (_dir, mut cmd) = noisy_ign();
let absent = tempfile::tempdir().expect("tempdir");
let missing = absent.path().join("nope");
let output = cmd
.args(["workspace", "push", missing.to_str().expect("utf-8 path")])
.output()
.expect("spawn ign");
assert_eq!(
output.status.code(),
Some(2),
"the pre-resolve refusal exits 2"
);
assert_stdout_bytes(&output.stdout, b"", "push missing-manifest refusal");
let dir = tempfile::tempdir().expect("tempdir");
let config = dir.path().join("config.toml");
std::fs::write(
&config,
"active = \"dev\"\n\n[profiles.dev]\nurl = \"http://127.0.0.1:9\"\nauth = { token_env = \"IGNITION_TOKEN\" }\n",
)
.expect("write dead-port config");
let output = Command::cargo_bin("ign")
.expect("binary 'ign' not found")
.env("IGNITION_CLI_CONFIG", &config)
.env("IGNITION_TOKEN", "mock:name-key")
.env("IGNITION_LOG", "trace")
.env_remove("IGNITION_PROFILE")
.env_remove("IGNITION_JSON")
.env_remove("IGNITION_YES")
.args(["workspace", "checkout", "proj", "/tmp/ign-purity-checkout"])
.output()
.expect("spawn ign");
assert_ne!(output.status.code(), Some(0), "a dead port must refuse");
assert_stdout_bytes(&output.stdout, b"", "checkout network refusal");
}