From 6e87e6ae32cdddcb1e16e80aad54e0dc48793faa Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:06:49 +0300 Subject: [PATCH 01/23] feat(cf13): add gate CLI execution module --- crates/commandf-cli/src/gate.rs | 124 ++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 crates/commandf-cli/src/gate.rs diff --git a/crates/commandf-cli/src/gate.rs b/crates/commandf-cli/src/gate.rs new file mode 100644 index 00000000..6fd7fd04 --- /dev/null +++ b/crates/commandf-cli/src/gate.rs @@ -0,0 +1,124 @@ +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Args, ValueEnum}; +use commandf_pkg::{ + classify_structural_diff, evaluate_compatibility_policy, evaluate_quality_gate, CheckDirection, + CheckFailOn, CheckPolicy, CheckReport, GateSuppressions, +}; + +use super::{build_diff_report, read_bounded_file, write_check_output}; + +const MAX_GATE_BASELINE_INPUT_BYTES: u64 = 64 * 1024 * 1024; +const MAX_GATE_SUPPRESSIONS_INPUT_BYTES: u64 = 64 * 1024 * 1024; + +#[derive(Args)] +pub(crate) struct GateArgs { + package: String, + #[arg(long)] + before_lock: PathBuf, + #[arg(long)] + before_cache: PathBuf, + #[arg(long)] + after_lock: PathBuf, + #[arg(long)] + after_cache: PathBuf, + #[arg(long, value_enum, default_value = "both")] + direction: GateDirectionArg, + #[arg(long, value_enum, default_value = "breaking")] + fail_on: GateFailOnArg, + #[arg(long)] + baseline: Option, + #[arg(long)] + suppressions: Option, + #[arg(long, value_enum, default_value = "json")] + format: GateOutputFormat, + #[arg(long)] + output: Option, +} + +#[derive(Clone, Copy, ValueEnum)] +enum GateOutputFormat { + Json, +} + +#[derive(Clone, Copy, ValueEnum)] +enum GateDirectionArg { + Both, + Producer, + Consumer, +} + +#[derive(Clone, Copy, ValueEnum)] +enum GateFailOnArg { + Breaking, + Risky, + None, +} + +impl From for CheckDirection { + fn from(value: GateDirectionArg) -> Self { + match value { + GateDirectionArg::Both => Self::Both, + GateDirectionArg::Producer => Self::Producer, + GateDirectionArg::Consumer => Self::Consumer, + } + } +} + +impl From for CheckFailOn { + fn from(value: GateFailOnArg) -> Self { + match value { + GateFailOnArg::Breaking => Self::Breaking, + GateFailOnArg::Risky => Self::Risky, + GateFailOnArg::None => Self::None, + } + } +} + +pub(crate) fn run(args: GateArgs) -> Result> { + let diff = build_diff_report( + args.package, + args.before_lock, + args.before_cache, + args.after_lock, + args.after_cache, + )?; + let compatibility = classify_structural_diff(&diff)?; + let current = evaluate_compatibility_policy( + &compatibility, + CheckPolicy { + direction: args.direction.into(), + fail_on: args.fail_on.into(), + }, + )?; + + let baseline = args + .baseline + .as_deref() + .map(|path| { + let bytes = read_bounded_file(path, MAX_GATE_BASELINE_INPUT_BYTES)?; + Ok::<_, Box>(CheckReport::from_json_slice(&bytes)?) + }) + .transpose()?; + let suppressions = args + .suppressions + .as_deref() + .map(|path| { + let bytes = read_bounded_file(path, MAX_GATE_SUPPRESSIONS_INPUT_BYTES)?; + Ok::<_, Box>(GateSuppressions::from_json_slice(&bytes)?) + }) + .transpose()?; + + let report = evaluate_quality_gate(¤t, baseline.as_ref(), suppressions.as_ref())?; + let bytes = match args.format { + GateOutputFormat::Json => report.to_json_bytes()?, + }; + write_check_output(&bytes, args.output.as_deref())?; + + if report.decision.passed { + Ok(ExitCode::SUCCESS) + } else { + Ok(ExitCode::from(2)) + } +} From e5f7f0dc6a8bee36974f474a30d32bd6bec56f88 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:09:46 +0300 Subject: [PATCH 02/23] feat(cf13): wire commandf gate CLI --- crates/commandf-cli/src/main.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/commandf-cli/src/main.rs b/crates/commandf-cli/src/main.rs index fc68b037..5f40270a 100644 --- a/crates/commandf-cli/src/main.rs +++ b/crates/commandf-cli/src/main.rs @@ -1,3 +1,4 @@ +mod gate; mod impact; mod oracle; @@ -115,6 +116,7 @@ enum Command { #[arg(long)] output: Option, }, + Gate(gate::GateArgs), Terminology { package: String, #[arg(long)] @@ -237,7 +239,11 @@ enum PkgCommand { } fn main() -> ExitCode { - let is_check = std::env::args_os().nth(1).as_deref() == Some(OsStr::new("check")); + let command = std::env::args_os().nth(1); + let normalize_usage_exit = matches!( + command.as_deref(), + Some(value) if value == OsStr::new("check") || value == OsStr::new("gate") + ); let cli = match Cli::try_parse() { Ok(cli) => cli, Err(error) => { @@ -246,7 +252,7 @@ fn main() -> ExitCode { if clap_exit == 0 { return ExitCode::SUCCESS; } - if is_check { + if normalize_usage_exit { return ExitCode::from(1); } return ExitCode::from(clap_exit as u8); @@ -426,6 +432,7 @@ fn run(cli: Cli) -> Result> { } return Ok(ExitCode::from(2)); } + Command::Gate(args) => return gate::run(args), Command::Terminology { package, before_lock, From 75ba99e97f728378690fd9b7f2b9433a4e6f620a Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:10:16 +0300 Subject: [PATCH 03/23] test(cf13): add gate parse exit contract --- .../commandf-cli/tests/gate_exit_contract.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_exit_contract.rs diff --git a/crates/commandf-cli/tests/gate_exit_contract.rs b/crates/commandf-cli/tests/gate_exit_contract.rs new file mode 100644 index 00000000..69cd1000 --- /dev/null +++ b/crates/commandf-cli/tests/gate_exit_contract.rs @@ -0,0 +1,51 @@ +use std::process::Command; + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +#[test] +fn gate_usage_errors_are_operational_exit_one() { + let output = commandf() + .args([ + "gate", + "example.package", + "--before-lock", + "before.lock", + "--before-cache", + "before-cache", + "--after-lock", + "after.lock", + "--after-cache", + "after-cache", + "--fail-on", + "invalid-threshold", + ]) + .output() + .expect("commandf gate parse failure must execute"); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("invalid value"), "stderr: {stderr}"); + assert!(stderr.contains("invalid-threshold"), "stderr: {stderr}"); +} + +#[test] +fn gate_help_remains_success() { + let output = commandf() + .args(["gate", "--help"]) + .output() + .expect("commandf gate help must execute"); + + assert_eq!(output.status.code(), Some(0)); +} + +#[test] +fn unrelated_clap_usage_behavior_remains_exit_two() { + let output = commandf() + .args(["inspect"]) + .output() + .expect("commandf unrelated parse failure must execute"); + + assert_eq!(output.status.code(), Some(2)); +} From a4370d6ccdb4e53e78cf063ce13ae307b863d569 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:11:10 +0300 Subject: [PATCH 04/23] test(cf13): add gate end-to-end regressions --- crates/commandf-cli/tests/gate_behavior.rs | 453 +++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_behavior.rs diff --git a/crates/commandf-cli/tests/gate_behavior.rs b/crates/commandf-cli/tests/gate_behavior.rs new file mode 100644 index 00000000..24a696c2 --- /dev/null +++ b/crates/commandf-cli/tests/gate_behavior.rs @@ -0,0 +1,453 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{ + finding_fingerprint_v1, CheckReport, FindingFingerprint, GateSuppression, GateSuppressions, + LockedPackage, Lockfile, PackageCache, QualityGateDisposition, QualityGateReport, +}; + +const BEFORE_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", + "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", + "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", + "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", + "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", + "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", + "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", + "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", +); + +const AFTER_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", + "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", + "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", + "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", + "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", + "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", + "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", + "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", + "00", +); + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) + .collect() +} + +fn hex_digit(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => panic!("invalid test hex digit"), + } +} + +fn unique_temp_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after the Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) +} + +fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); + fs::create_dir_all(root).expect("create state root"); + let cache = PackageCache::new(&cache_path); + let digest = cache.put(archive).expect("cache synthetic archive"); + let lockfile = Lockfile::new( + vec![format!("example.package@{version}")], + vec![LockedPackage { + name: "example.package".to_owned(), + version: version.to_owned(), + sha256: digest, + source: "synthetic-test".to_owned(), + dependencies: BTreeMap::new(), + }], + ); + fs::write(&lock_path, lockfile.to_bytes().expect("serialize lock")).expect("write lock"); + (lock_path, cache_path) +} + +fn changed_states( + dir: &Path, + before_version: &str, + after_version: &str, +) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + let before = decode_hex(BEFORE_HEX); + let after = decode_hex(AFTER_HEX); + let (before_lock, before_cache) = + write_locked_state(&dir.join("before"), &before, before_version); + let (after_lock, after_cache) = + write_locked_state(&dir.join("after"), &after, after_version); + (before_lock, before_cache, after_lock, after_cache) +} + +fn run_command( + subcommand: &str, + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, + extra: &[String], +) -> Output { + let mut command = commandf(); + command.args([ + subcommand, + "example.package", + "--before-lock", + before_lock.to_str().expect("UTF-8 path"), + "--before-cache", + before_cache.to_str().expect("UTF-8 path"), + "--after-lock", + after_lock.to_str().expect("UTF-8 path"), + "--after-cache", + after_cache.to_str().expect("UTF-8 path"), + ]); + command.args(extra); + command + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .expect("commandf must execute") +} + +fn current_check_report( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) -> CheckReport { + let output = run_command( + "check", + before_lock, + before_cache, + after_lock, + after_cache, + &[], + ); + assert_eq!(output.status.code(), Some(2)); + CheckReport::from_json_slice(&output.stdout).expect("valid check report") +} + +fn write_baseline( + path: &Path, + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) { + let output = run_command( + "check", + before_lock, + before_cache, + after_lock, + after_cache, + &[], + ); + assert_eq!(output.status.code(), Some(2)); + fs::write(path, output.stdout).expect("write baseline report"); +} + +#[test] +fn gate_help_exposes_v1_contract() { + let output = commandf() + .args(["gate", "--help"]) + .output() + .expect("gate help must execute"); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("UTF-8 help"); + for flag in [ + "--before-lock", + "--before-cache", + "--after-lock", + "--after-cache", + "--direction", + "--fail-on", + "--baseline", + "--suppressions", + "--format", + "--output", + ] { + assert!(stdout.contains(flag), "missing {flag}"); + } +} + +#[test] +fn new_blocker_emits_complete_json_before_exit_two_and_replaces_output() { + let dir = unique_temp_dir("gate-new-blocker"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + let output_path = dir.join("gate.json"); + fs::write(&output_path, b"stale-report").expect("write stale output"); + + let output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--output".to_owned(), + output_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let bytes = fs::read(&output_path).expect("gate output exists"); + let report = QualityGateReport::from_json_slice(&bytes).expect("complete gate JSON"); + assert!(!report.decision.passed); + assert!(report.decision.blocking_findings > 0); + assert!(report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::New)); + assert!(!String::from_utf8_lossy(&bytes).contains("stale-report")); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn historical_baseline_allows_same_semantic_finding() { + let dir = unique_temp_dir("gate-baseline"); + let baseline_dir = dir.join("baseline-state"); + let current_dir = dir.join("current-state"); + let (base_before_lock, base_before_cache, base_after_lock, base_after_cache) = + changed_states(&baseline_dir, "0.8.0", "0.9.0"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(¤t_dir, "1.0.0", "1.1.0"); + let baseline_path = dir.join("baseline.json"); + write_baseline( + &baseline_path, + &base_before_lock, + &base_before_cache, + &base_after_lock, + &base_after_cache, + ); + + let output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--baseline".to_owned(), + baseline_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(0)); + let report = QualityGateReport::from_json_slice(&output.stdout).expect("gate report"); + assert!(report.decision.passed); + assert!(report.decision.baseline_findings > 0); + assert_eq!(report.decision.blocking_findings, 0); + assert!(report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::Baseline)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn exact_suppression_passes_and_stale_suppression_does_not_hide_blocker() { + let dir = unique_temp_dir("gate-suppression"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + let fingerprint = finding_fingerprint_v1( + ¤t.compatibility.ruleset, + current.compatibility.findings.first().expect("finding"), + ) + .expect("fingerprint"); + let exact_path = dir.join("exact-suppressions.json"); + let exact = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: vec![GateSuppression { + finding_fingerprint: fingerprint, + rationale: "approved interoperability exception".to_owned(), + reference: Some("TEST-1".to_owned()), + }], + }; + fs::write(&exact_path, exact.to_json_bytes().expect("suppression JSON")) + .expect("write exact suppression"); + + let exact_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + exact_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(exact_output.status.code(), Some(0)); + let exact_report = + QualityGateReport::from_json_slice(&exact_output.stdout).expect("exact gate report"); + assert!(exact_report.decision.passed); + assert!(exact_report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::Suppressed)); + + let stale_path = dir.join("stale-suppressions.json"); + let stale = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: vec![GateSuppression { + finding_fingerprint: FindingFingerprint { + schema: FindingFingerprint::SCHEMA_V1, + digest: format!("sha256:{}", "f".repeat(64)), + }, + rationale: "stale exception".to_owned(), + reference: None, + }], + }; + fs::write(&stale_path, stale.to_json_bytes().expect("stale JSON")) + .expect("write stale suppression"); + let stale_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + stale_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(stale_output.status.code(), Some(2)); + let stale_report = + QualityGateReport::from_json_slice(&stale_output.stdout).expect("stale gate report"); + assert!(!stale_report.decision.passed); + assert_eq!(stale_report.unused_suppressions.len(), 1); + assert!(stale_report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::New)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn malformed_mismatched_and_version_incompatible_inputs_exit_one() { + let dir = unique_temp_dir("gate-invalid-input"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + + let malformed_path = dir.join("malformed.json"); + fs::write(&malformed_path, b"{").expect("write malformed input"); + let malformed = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + malformed_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(malformed.status.code(), Some(1)); + + let mut mismatched = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + mismatched.compatibility.package_name = "other.package".to_owned(); + let mismatched_path = dir.join("mismatched-baseline.json"); + fs::write( + &mismatched_path, + mismatched.to_json_bytes().expect("baseline JSON"), + ) + .expect("write mismatched baseline"); + let mismatch = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--baseline".to_owned(), + mismatched_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(mismatch.status.code(), Some(1)); + + let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + let current_fingerprint = finding_fingerprint_v1( + ¤t.compatibility.ruleset, + current.compatibility.findings.first().expect("finding"), + ) + .expect("fingerprint"); + let incompatible_path = dir.join("incompatible-suppressions.json"); + let incompatible = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: vec![GateSuppression { + finding_fingerprint: FindingFingerprint { + schema: 2, + digest: current_fingerprint.digest, + }, + rationale: "unsupported schema".to_owned(), + reference: None, + }], + }; + fs::write( + &incompatible_path, + incompatible.to_json_bytes().expect("incompatible JSON"), + ) + .expect("write incompatible suppressions"); + let incompatible_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + incompatible_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(incompatible_output.status.code(), Some(1)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn repeated_gate_runs_are_byte_identical() { + let dir = unique_temp_dir("gate-determinism"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + + let first = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[], + ); + let second = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[], + ); + + assert_eq!(first.status.code(), Some(2)); + assert_eq!(second.status.code(), Some(2)); + assert_eq!(first.stdout, second.stdout); + QualityGateReport::from_json_slice(&first.stdout).expect("deterministic report"); + let _ = fs::remove_dir_all(&dir); +} From c1f2fc99458bf7f536a68b2122f8e75c4e073720 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:12:40 +0300 Subject: [PATCH 05/23] test(cf13): add deterministic gate proof --- .../tests/gate_determinism_proof.rs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_determinism_proof.rs diff --git a/crates/commandf-cli/tests/gate_determinism_proof.rs b/crates/commandf-cli/tests/gate_determinism_proof.rs new file mode 100644 index 00000000..1f29ca7f --- /dev/null +++ b/crates/commandf-cli/tests/gate_determinism_proof.rs @@ -0,0 +1,208 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{ + validate_quality_gate_report, GateSuppression, GateSuppressions, LockedPackage, Lockfile, + PackageCache, QualityGateDisposition, QualityGateReport, +}; + +const BEFORE_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", + "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", + "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", + "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", + "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", + "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", + "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", + "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", +); + +const AFTER_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", + "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", + "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", + "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", + "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", + "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", + "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", + "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", + "00", +); + +#[test] +fn gate_cli_proof_is_deterministic_and_covers_new_baseline_suppression() { + let root = unique_temp_dir(); + fs::create_dir_all(&root).unwrap(); + let current = write_changed_states(&root.join("current"), "1.0.0", "1.1.0"); + let historical = write_changed_states(&root.join("historical"), "0.8.0", "0.9.0"); + + let new_first = run_gate(¤t, &[]); + let new_second = run_gate(¤t, &[]); + assert_eq!(new_first.status.code(), Some(2)); + assert_eq!(new_second.status.code(), Some(2)); + assert_eq!(new_first.stdout, new_second.stdout); + let new_report = QualityGateReport::from_json_slice(&new_first.stdout).unwrap(); + validate_quality_gate_report(&new_report).unwrap(); + assert!(!new_report.decision.passed); + assert!(new_report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::New)); + + let baseline_path = root.join("baseline.json"); + let baseline_check = run_check(&historical); + assert_eq!(baseline_check.status.code(), Some(2)); + fs::write(&baseline_path, baseline_check.stdout).unwrap(); + let baseline = run_gate( + ¤t, + &[ + "--baseline".to_owned(), + baseline_path.to_str().unwrap().to_owned(), + ], + ); + assert_eq!(baseline.status.code(), Some(0)); + let baseline_report = QualityGateReport::from_json_slice(&baseline.stdout).unwrap(); + validate_quality_gate_report(&baseline_report).unwrap(); + assert!(baseline_report.decision.passed); + + let suppression_path = root.join("suppressions.json"); + let suppression = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: new_report + .findings + .iter() + .map(|finding| GateSuppression { + finding_fingerprint: finding.fingerprint.clone(), + rationale: "CF-13 deterministic proof suppression".to_owned(), + reference: Some("CF13-PROOF".to_owned()), + }) + .collect(), + }; + fs::write(&suppression_path, suppression.to_json_bytes().unwrap()).unwrap(); + let suppressed = run_gate( + ¤t, + &[ + "--suppressions".to_owned(), + suppression_path.to_str().unwrap().to_owned(), + ], + ); + assert_eq!(suppressed.status.code(), Some(0)); + let suppression_report = QualityGateReport::from_json_slice(&suppressed.stdout).unwrap(); + validate_quality_gate_report(&suppression_report).unwrap(); + assert!(suppression_report.decision.passed); + + println!("CF13_GATE_SHA256={}", PackageCache::digest(&new_first.stdout)); + println!( + "CF13_BASELINE_CANONICAL_SHA256={}", + baseline_report.baseline.as_ref().unwrap().canonical_sha256 + ); + println!( + "CF13_SUPPRESSION_CANONICAL_SHA256={}", + suppression_report + .suppression_evidence + .as_ref() + .unwrap() + .canonical_sha256 + ); + println!( + "CF13_BEFORE_ARCHIVE_SHA256={}", + new_report.current.compatibility.before.archive_sha256 + ); + println!( + "CF13_AFTER_ARCHIVE_SHA256={}", + new_report.current.compatibility.after.archive_sha256 + ); + + let _ = fs::remove_dir_all(root); +} + +type State = (PathBuf, PathBuf, PathBuf, PathBuf); + +fn write_changed_states(root: &Path, before_version: &str, after_version: &str) -> State { + let before = decode_hex(BEFORE_HEX); + let after = decode_hex(AFTER_HEX); + let (before_lock, before_cache) = + write_locked_state(&root.join("before"), &before, before_version); + let (after_lock, after_cache) = write_locked_state(&root.join("after"), &after, after_version); + (before_lock, before_cache, after_lock, after_cache) +} + +fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { + fs::create_dir_all(root).unwrap(); + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); + let cache = PackageCache::new(&cache_path); + let sha256 = cache.put(archive).unwrap(); + let lockfile = Lockfile::new( + vec![format!("example.package@{version}")], + vec![LockedPackage { + name: "example.package".to_owned(), + version: version.to_owned(), + sha256, + source: "synthetic-cf13-proof".to_owned(), + dependencies: BTreeMap::new(), + }], + ); + fs::write(lock_path.clone(), lockfile.to_bytes().unwrap()).unwrap(); + (lock_path, cache_path) +} + +fn run_check(state: &State) -> Output { + run("check", state, &[]) +} + +fn run_gate(state: &State, extra: &[String]) -> Output { + run("gate", state, extra) +} + +fn run(subcommand: &str, state: &State, extra: &[String]) -> Output { + let (before_lock, before_cache, after_lock, after_cache) = state; + let mut command = Command::new(env!("CARGO_BIN_EXE_commandf")); + command.args([ + subcommand, + "example.package", + "--before-lock", + before_lock.to_str().unwrap(), + "--before-cache", + before_cache.to_str().unwrap(), + "--after-lock", + after_lock.to_str().unwrap(), + "--after-cache", + after_cache.to_str().unwrap(), + ]); + command.args(extra); + command + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .unwrap() +} + +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) + .collect() +} + +fn hex_digit(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => panic!("invalid hex digit"), + } +} + +fn unique_temp_dir() -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock after epoch") + .as_nanos(); + std::env::temp_dir().join(format!("commandf-cf13-proof-{}-{nonce}", std::process::id())) +} From e832a40e1d3f681619fa40c4d90048a44ab554a3 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:13:14 +0300 Subject: [PATCH 06/23] ci(cf13): add deterministic quality-gate proof --- .github/workflows/cf13-quality-gate-proof.yml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/cf13-quality-gate-proof.yml diff --git a/.github/workflows/cf13-quality-gate-proof.yml b/.github/workflows/cf13-quality-gate-proof.yml new file mode 100644 index 00000000..31889085 --- /dev/null +++ b/.github/workflows/cf13-quality-gate-proof.yml @@ -0,0 +1,97 @@ +name: cf13-quality-gate-proof + +on: + pull_request: + paths: + - .github/workflows/cf13-quality-gate-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/014-cf-13-baselines-suppression-quality-gates/** + - .specify/memory/constitution.md + - AGENTS.md + push: + branches: + - feat/cf13-quality-gate-cli + paths: + - .github/workflows/cf13-quality-gate-proof.yml + - Cargo.toml + - Cargo.lock + - crates/commandf-pkg/** + - crates/commandf-cli/** + - specs/014-cf-13-baselines-suppression-quality-gates/** + - .specify/memory/constitution.md + - AGENTS.md + workflow_dispatch: + +permissions: + contents: read + +env: + CF13_PROOF_CONTAINER: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + CF13_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + +jobs: + deterministic-quality-gate: + runs-on: ubuntu-24.04 + container: + image: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + with: + persist-credentials: false + + - name: Assert pinned execution toolchain + run: | + set -euo pipefail + rustc --version --verbose + cargo --version + test "$(rustc --version | awk '{print $2}')" = "1.97.1" + + - name: Prove CF-13 CLI contract and deterministic evidence + run: | + set -euo pipefail + cargo test --locked -p commandf --test gate_behavior --test gate_exit_contract + cargo test --locked -p commandf --test gate_determinism_proof -- --nocapture --test-threads=1 \ + | tee /tmp/cf13-quality-gate-proof.log + grep -oE 'CF13_(GATE|BASELINE_CANONICAL|SUPPRESSION_CANONICAL|BEFORE_ARCHIVE|AFTER_ARCHIVE)_SHA256=(sha256:)?[0-9a-f]{64}' \ + /tmp/cf13-quality-gate-proof.log \ + | sort \ + | tee /tmp/cf13-quality-gate-proof.env + grep -q '^CF13_GATE_SHA256=[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env + grep -q '^CF13_BASELINE_CANONICAL_SHA256=sha256:[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env + grep -q '^CF13_SUPPRESSION_CANONICAL_SHA256=sha256:[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env + + - name: Record immutable repository evidence + run: | + set -euo pipefail + { + echo "CF13_SOURCE_SHA=$CF13_SOURCE_SHA" + echo "CF13_WORKFLOW_SHA=$(sha256sum .github/workflows/cf13-quality-gate-proof.yml | awk '{print $1}')" + echo "CF13_CARGO_LOCK_SHA256=$(sha256sum Cargo.lock | awk '{print $1}')" + echo "CF13_SPEC_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/spec.md | awk '{print $1}')" + echo "CF13_PLAN_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/plan.md | awk '{print $1}')" + echo "CF13_TASKS_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/tasks.md | awk '{print $1}')" + echo "CF13_PROOF_TEST_SHA256=$(sha256sum crates/commandf-cli/tests/gate_determinism_proof.rs | awk '{print $1}')" + echo "CF13_RUSTC=$(rustc --version)" + echo "CF13_CONTAINER=$CF13_PROOF_CONTAINER" + echo "CF13_CHECKOUT_ACTION=fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09" + echo "CF13_UPLOAD_ACTION=ea165f8d65b6e75b540449e92b4886f43607fa02" + } >> /tmp/cf13-quality-gate-proof.env + sort -o /tmp/cf13-quality-gate-proof.env /tmp/cf13-quality-gate-proof.env + + - name: Assert repository remains clean + run: | + set -euo pipefail + status="$(git -c safe.directory="$GITHUB_WORKSPACE" status --porcelain)" + test -z "$status" + + - name: Upload CF-13 deterministic evidence + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: cf13-quality-gate-proof + path: /tmp/cf13-quality-gate-proof.env + if-no-files-found: error + retention-days: 3 From 7fbd2fd1c48842baf17bcd7464b5dc7e9d4fcc83 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:15:30 +0300 Subject: [PATCH 07/23] style(cf13): apply rustfmt to gate behavior tests --- crates/commandf-cli/tests/gate_behavior.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/commandf-cli/tests/gate_behavior.rs b/crates/commandf-cli/tests/gate_behavior.rs index 24a696c2..a1e811bf 100644 --- a/crates/commandf-cli/tests/gate_behavior.rs +++ b/crates/commandf-cli/tests/gate_behavior.rs @@ -90,8 +90,7 @@ fn changed_states( let after = decode_hex(AFTER_HEX); let (before_lock, before_cache) = write_locked_state(&dir.join("before"), &before, before_version); - let (after_lock, after_cache) = - write_locked_state(&dir.join("after"), &after, after_version); + let (after_lock, after_cache) = write_locked_state(&dir.join("after"), &after, after_version); (before_lock, before_cache, after_lock, after_cache) } @@ -282,8 +281,11 @@ fn exact_suppression_passes_and_stale_suppression_does_not_hide_blocker() { reference: Some("TEST-1".to_owned()), }], }; - fs::write(&exact_path, exact.to_json_bytes().expect("suppression JSON")) - .expect("write exact suppression"); + fs::write( + &exact_path, + exact.to_json_bytes().expect("suppression JSON"), + ) + .expect("write exact suppression"); let exact_output = run_command( "gate", @@ -363,7 +365,8 @@ fn malformed_mismatched_and_version_incompatible_inputs_exit_one() { ); assert_eq!(malformed.status.code(), Some(1)); - let mut mismatched = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + let mut mismatched = + current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); mismatched.compatibility.package_name = "other.package".to_owned(); let mismatched_path = dir.join("mismatched-baseline.json"); fs::write( From 64aa2f98d6cfa29c2de6a20bfb01861a7d95873f Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:16:00 +0300 Subject: [PATCH 08/23] style(cf13): apply rustfmt to gate proof --- crates/commandf-cli/tests/gate_determinism_proof.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/commandf-cli/tests/gate_determinism_proof.rs b/crates/commandf-cli/tests/gate_determinism_proof.rs index 1f29ca7f..3f1e8c47 100644 --- a/crates/commandf-cli/tests/gate_determinism_proof.rs +++ b/crates/commandf-cli/tests/gate_determinism_proof.rs @@ -94,7 +94,10 @@ fn gate_cli_proof_is_deterministic_and_covers_new_baseline_suppression() { validate_quality_gate_report(&suppression_report).unwrap(); assert!(suppression_report.decision.passed); - println!("CF13_GATE_SHA256={}", PackageCache::digest(&new_first.stdout)); + println!( + "CF13_GATE_SHA256={}", + PackageCache::digest(&new_first.stdout) + ); println!( "CF13_BASELINE_CANONICAL_SHA256={}", baseline_report.baseline.as_ref().unwrap().canonical_sha256 @@ -204,5 +207,8 @@ fn unique_temp_dir() -> PathBuf { .duration_since(UNIX_EPOCH) .expect("system clock after epoch") .as_nanos(); - std::env::temp_dir().join(format!("commandf-cf13-proof-{}-{nonce}", std::process::id())) + std::env::temp_dir().join(format!( + "commandf-cf13-proof-{}-{nonce}", + std::process::id() + )) } From 701f2fed6181dc2b9c977060396792bd1d6c0e06 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:32:07 +0300 Subject: [PATCH 09/23] test(cf13): prove exact suppression membership --- crates/commandf-cli/tests/gate_behavior_v1.rs | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_behavior_v1.rs diff --git a/crates/commandf-cli/tests/gate_behavior_v1.rs b/crates/commandf-cli/tests/gate_behavior_v1.rs new file mode 100644 index 00000000..49a9daa1 --- /dev/null +++ b/crates/commandf-cli/tests/gate_behavior_v1.rs @@ -0,0 +1,456 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{ + finding_fingerprint_v1, CheckReport, FindingFingerprint, GateSuppression, GateSuppressions, + LockedPackage, Lockfile, PackageCache, QualityGateDisposition, QualityGateReport, +}; + +const BEFORE_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", + "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", + "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", + "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", + "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", + "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", + "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", + "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", +); + +const AFTER_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", + "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", + "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", + "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", + "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", + "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", + "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", + "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", + "00", +); + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) + .collect() +} + +fn hex_digit(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => panic!("invalid test hex digit"), + } +} + +fn unique_temp_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after the Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) +} + +fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); + fs::create_dir_all(root).expect("create state root"); + let cache = PackageCache::new(&cache_path); + let digest = cache.put(archive).expect("cache synthetic archive"); + let lockfile = Lockfile::new( + vec![format!("example.package@{version}")], + vec![LockedPackage { + name: "example.package".to_owned(), + version: version.to_owned(), + sha256: digest, + source: "synthetic-test".to_owned(), + dependencies: BTreeMap::new(), + }], + ); + fs::write(&lock_path, lockfile.to_bytes().expect("serialize lock")).expect("write lock"); + (lock_path, cache_path) +} + +fn changed_states( + dir: &Path, + before_version: &str, + after_version: &str, +) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + let before = decode_hex(BEFORE_HEX); + let after = decode_hex(AFTER_HEX); + let (before_lock, before_cache) = + write_locked_state(&dir.join("before"), &before, before_version); + let (after_lock, after_cache) = write_locked_state(&dir.join("after"), &after, after_version); + (before_lock, before_cache, after_lock, after_cache) +} + +fn run_command( + subcommand: &str, + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, + extra: &[String], +) -> Output { + let mut command = commandf(); + command.args([ + subcommand, + "example.package", + "--before-lock", + before_lock.to_str().expect("UTF-8 path"), + "--before-cache", + before_cache.to_str().expect("UTF-8 path"), + "--after-lock", + after_lock.to_str().expect("UTF-8 path"), + "--after-cache", + after_cache.to_str().expect("UTF-8 path"), + ]); + command.args(extra); + command + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .expect("commandf must execute") +} + +fn current_check_report( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) -> CheckReport { + let output = run_command( + "check", + before_lock, + before_cache, + after_lock, + after_cache, + &[], + ); + assert_eq!(output.status.code(), Some(2)); + CheckReport::from_json_slice(&output.stdout).expect("valid check report") +} + +fn write_baseline( + path: &Path, + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, +) { + let report = run_command( + "check", + before_lock, + before_cache, + after_lock, + after_cache, + &[], + ); + assert_eq!(report.status.code(), Some(2)); + fs::write(path, report.stdout).expect("write baseline report"); +} + +#[test] +fn gate_help_exposes_v1_contract() { + let output = commandf() + .args(["gate", "--help"]) + .output() + .expect("gate help must execute"); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("UTF-8 help"); + for flag in [ + "--before-lock", + "--before-cache", + "--after-lock", + "--after-cache", + "--direction", + "--fail-on", + "--baseline", + "--suppressions", + "--format", + "--output", + ] { + assert!(stdout.contains(flag), "missing {flag}"); + } +} + +#[test] +fn new_blocker_emits_complete_json_before_exit_two_and_replaces_output() { + let dir = unique_temp_dir("gate-new-blocker"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + let output_path = dir.join("gate.json"); + fs::write(&output_path, b"stale-report").expect("write stale output"); + + let output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--output".to_owned(), + output_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let bytes = fs::read(&output_path).expect("gate output exists"); + let report = QualityGateReport::from_json_slice(&bytes).expect("complete gate JSON"); + assert!(!report.decision.passed); + assert!(report.decision.blocking_findings > 0); + assert!(report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::New)); + assert!(!String::from_utf8_lossy(&bytes).contains("stale-report")); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn historical_baseline_allows_same_semantic_finding() { + let dir = unique_temp_dir("gate-baseline"); + let (base_before_lock, base_before_cache, base_after_lock, base_after_cache) = + changed_states(&dir.join("baseline-state"), "0.8.0", "0.9.0"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir.join("current-state"), "1.0.0", "1.1.0"); + let baseline_path = dir.join("baseline.json"); + write_baseline( + &baseline_path, + &base_before_lock, + &base_before_cache, + &base_after_lock, + &base_after_cache, + ); + + let output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--baseline".to_owned(), + baseline_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(0)); + let report = QualityGateReport::from_json_slice(&output.stdout).expect("gate report"); + assert!(report.decision.passed); + assert!(report.decision.baseline_findings > 0); + assert_eq!(report.decision.blocking_findings, 0); + assert!(report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::Baseline)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn exact_suppression_passes_and_stale_suppression_does_not_hide_blocker() { + let dir = unique_temp_dir("gate-suppression"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + let exact = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: current + .compatibility + .findings + .iter() + .enumerate() + .map(|(index, finding)| GateSuppression { + finding_fingerprint: finding_fingerprint_v1(¤t.compatibility.ruleset, finding) + .expect("fingerprint"), + rationale: "approved interoperability exception".to_owned(), + reference: Some(format!("TEST-{}", index + 1)), + }) + .collect(), + }; + let exact_path = dir.join("exact-suppressions.json"); + fs::write( + &exact_path, + exact.to_json_bytes().expect("suppression JSON"), + ) + .expect("write exact suppression"); + + let exact_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + exact_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(exact_output.status.code(), Some(0)); + let exact_report = + QualityGateReport::from_json_slice(&exact_output.stdout).expect("exact gate report"); + assert!(exact_report.decision.passed); + assert_eq!(exact_report.decision.blocking_findings, 0); + assert!(exact_report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::Suppressed)); + + let stale_path = dir.join("stale-suppressions.json"); + let stale = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: vec![GateSuppression { + finding_fingerprint: FindingFingerprint { + schema: FindingFingerprint::SCHEMA_V1, + digest: format!("sha256:{}", "f".repeat(64)), + }, + rationale: "stale exception".to_owned(), + reference: None, + }], + }; + fs::write(&stale_path, stale.to_json_bytes().expect("stale JSON")) + .expect("write stale suppression"); + let stale_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + stale_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(stale_output.status.code(), Some(2)); + let stale_report = + QualityGateReport::from_json_slice(&stale_output.stdout).expect("stale gate report"); + assert!(!stale_report.decision.passed); + assert_eq!(stale_report.unused_suppressions.len(), 1); + assert!(stale_report + .findings + .iter() + .all(|finding| finding.disposition == QualityGateDisposition::New)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn malformed_mismatched_and_version_incompatible_inputs_exit_one() { + let dir = unique_temp_dir("gate-invalid-input"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + + let malformed_path = dir.join("malformed.json"); + fs::write(&malformed_path, b"{").expect("write malformed input"); + let malformed = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + malformed_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(malformed.status.code(), Some(1)); + + let mut mismatched = + current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + mismatched.compatibility.package_name = "other.package".to_owned(); + let mismatched_path = dir.join("mismatched-baseline.json"); + fs::write( + &mismatched_path, + mismatched.to_json_bytes().expect("baseline JSON"), + ) + .expect("write mismatched baseline"); + let mismatch = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--baseline".to_owned(), + mismatched_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(mismatch.status.code(), Some(1)); + + let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); + let current_fingerprint = finding_fingerprint_v1( + ¤t.compatibility.ruleset, + current.compatibility.findings.first().expect("finding"), + ) + .expect("fingerprint"); + let incompatible_path = dir.join("incompatible-suppressions.json"); + let incompatible = GateSuppressions { + schema: GateSuppressions::SCHEMA_V1, + suppressions: vec![GateSuppression { + finding_fingerprint: FindingFingerprint { + schema: 2, + digest: current_fingerprint.digest, + }, + rationale: "unsupported schema".to_owned(), + reference: None, + }], + }; + fs::write( + &incompatible_path, + incompatible.to_json_bytes().expect("incompatible JSON"), + ) + .expect("write incompatible suppressions"); + let incompatible_output = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[ + "--suppressions".to_owned(), + incompatible_path.to_str().expect("UTF-8 path").to_owned(), + ], + ); + assert_eq!(incompatible_output.status.code(), Some(1)); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn repeated_gate_runs_are_byte_identical() { + let dir = unique_temp_dir("gate-determinism"); + let (before_lock, before_cache, after_lock, after_cache) = + changed_states(&dir, "1.0.0", "1.1.0"); + let first = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[], + ); + let second = run_command( + "gate", + &before_lock, + &before_cache, + &after_lock, + &after_cache, + &[], + ); + + assert_eq!(first.status.code(), Some(2)); + assert_eq!(second.status.code(), Some(2)); + assert_eq!(first.stdout, second.stdout); + QualityGateReport::from_json_slice(&first.stdout).expect("deterministic report"); + let _ = fs::remove_dir_all(&dir); +} From cf477ec4430c1ab4d33185f5b30f78d1db684763 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:32:15 +0300 Subject: [PATCH 10/23] test(cf13): replace incomplete suppression regression --- crates/commandf-cli/tests/gate_behavior.rs | 456 --------------------- 1 file changed, 456 deletions(-) delete mode 100644 crates/commandf-cli/tests/gate_behavior.rs diff --git a/crates/commandf-cli/tests/gate_behavior.rs b/crates/commandf-cli/tests/gate_behavior.rs deleted file mode 100644 index a1e811bf..00000000 --- a/crates/commandf-cli/tests/gate_behavior.rs +++ /dev/null @@ -1,456 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use commandf_pkg::{ - finding_fingerprint_v1, CheckReport, FindingFingerprint, GateSuppression, GateSuppressions, - LockedPackage, Lockfile, PackageCache, QualityGateDisposition, QualityGateReport, -}; - -const BEFORE_HEX: &str = concat!( - "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", - "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", - "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", - "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", - "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", - "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", - "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", - "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", -); - -const AFTER_HEX: &str = concat!( - "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", - "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", - "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", - "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", - "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", - "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", - "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", - "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", - "00", -); - -fn commandf() -> Command { - Command::new(env!("CARGO_BIN_EXE_commandf")) -} - -fn decode_hex(value: &str) -> Vec { - assert_eq!(value.len() % 2, 0); - value - .as_bytes() - .chunks_exact(2) - .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) - .collect() -} - -fn hex_digit(value: u8) -> u8 { - match value { - b'0'..=b'9' => value - b'0', - b'a'..=b'f' => value - b'a' + 10, - _ => panic!("invalid test hex digit"), - } -} - -fn unique_temp_dir(label: &str) -> PathBuf { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock must be after the Unix epoch") - .as_nanos(); - std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) -} - -fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { - let cache_path = root.join("cache"); - let lock_path = root.join("commandf.lock"); - fs::create_dir_all(root).expect("create state root"); - let cache = PackageCache::new(&cache_path); - let digest = cache.put(archive).expect("cache synthetic archive"); - let lockfile = Lockfile::new( - vec![format!("example.package@{version}")], - vec![LockedPackage { - name: "example.package".to_owned(), - version: version.to_owned(), - sha256: digest, - source: "synthetic-test".to_owned(), - dependencies: BTreeMap::new(), - }], - ); - fs::write(&lock_path, lockfile.to_bytes().expect("serialize lock")).expect("write lock"); - (lock_path, cache_path) -} - -fn changed_states( - dir: &Path, - before_version: &str, - after_version: &str, -) -> (PathBuf, PathBuf, PathBuf, PathBuf) { - let before = decode_hex(BEFORE_HEX); - let after = decode_hex(AFTER_HEX); - let (before_lock, before_cache) = - write_locked_state(&dir.join("before"), &before, before_version); - let (after_lock, after_cache) = write_locked_state(&dir.join("after"), &after, after_version); - (before_lock, before_cache, after_lock, after_cache) -} - -fn run_command( - subcommand: &str, - before_lock: &Path, - before_cache: &Path, - after_lock: &Path, - after_cache: &Path, - extra: &[String], -) -> Output { - let mut command = commandf(); - command.args([ - subcommand, - "example.package", - "--before-lock", - before_lock.to_str().expect("UTF-8 path"), - "--before-cache", - before_cache.to_str().expect("UTF-8 path"), - "--after-lock", - after_lock.to_str().expect("UTF-8 path"), - "--after-cache", - after_cache.to_str().expect("UTF-8 path"), - ]); - command.args(extra); - command - .env("HTTP_PROXY", "http://127.0.0.1:9") - .env("HTTPS_PROXY", "http://127.0.0.1:9") - .env("NO_PROXY", "") - .output() - .expect("commandf must execute") -} - -fn current_check_report( - before_lock: &Path, - before_cache: &Path, - after_lock: &Path, - after_cache: &Path, -) -> CheckReport { - let output = run_command( - "check", - before_lock, - before_cache, - after_lock, - after_cache, - &[], - ); - assert_eq!(output.status.code(), Some(2)); - CheckReport::from_json_slice(&output.stdout).expect("valid check report") -} - -fn write_baseline( - path: &Path, - before_lock: &Path, - before_cache: &Path, - after_lock: &Path, - after_cache: &Path, -) { - let output = run_command( - "check", - before_lock, - before_cache, - after_lock, - after_cache, - &[], - ); - assert_eq!(output.status.code(), Some(2)); - fs::write(path, output.stdout).expect("write baseline report"); -} - -#[test] -fn gate_help_exposes_v1_contract() { - let output = commandf() - .args(["gate", "--help"]) - .output() - .expect("gate help must execute"); - assert!(output.status.success()); - let stdout = String::from_utf8(output.stdout).expect("UTF-8 help"); - for flag in [ - "--before-lock", - "--before-cache", - "--after-lock", - "--after-cache", - "--direction", - "--fail-on", - "--baseline", - "--suppressions", - "--format", - "--output", - ] { - assert!(stdout.contains(flag), "missing {flag}"); - } -} - -#[test] -fn new_blocker_emits_complete_json_before_exit_two_and_replaces_output() { - let dir = unique_temp_dir("gate-new-blocker"); - let (before_lock, before_cache, after_lock, after_cache) = - changed_states(&dir, "1.0.0", "1.1.0"); - let output_path = dir.join("gate.json"); - fs::write(&output_path, b"stale-report").expect("write stale output"); - - let output = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--output".to_owned(), - output_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - - assert_eq!(output.status.code(), Some(2)); - assert!(output.stdout.is_empty()); - let bytes = fs::read(&output_path).expect("gate output exists"); - let report = QualityGateReport::from_json_slice(&bytes).expect("complete gate JSON"); - assert!(!report.decision.passed); - assert!(report.decision.blocking_findings > 0); - assert!(report - .findings - .iter() - .all(|finding| finding.disposition == QualityGateDisposition::New)); - assert!(!String::from_utf8_lossy(&bytes).contains("stale-report")); - let _ = fs::remove_dir_all(&dir); -} - -#[test] -fn historical_baseline_allows_same_semantic_finding() { - let dir = unique_temp_dir("gate-baseline"); - let baseline_dir = dir.join("baseline-state"); - let current_dir = dir.join("current-state"); - let (base_before_lock, base_before_cache, base_after_lock, base_after_cache) = - changed_states(&baseline_dir, "0.8.0", "0.9.0"); - let (before_lock, before_cache, after_lock, after_cache) = - changed_states(¤t_dir, "1.0.0", "1.1.0"); - let baseline_path = dir.join("baseline.json"); - write_baseline( - &baseline_path, - &base_before_lock, - &base_before_cache, - &base_after_lock, - &base_after_cache, - ); - - let output = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--baseline".to_owned(), - baseline_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - - assert_eq!(output.status.code(), Some(0)); - let report = QualityGateReport::from_json_slice(&output.stdout).expect("gate report"); - assert!(report.decision.passed); - assert!(report.decision.baseline_findings > 0); - assert_eq!(report.decision.blocking_findings, 0); - assert!(report - .findings - .iter() - .all(|finding| finding.disposition == QualityGateDisposition::Baseline)); - let _ = fs::remove_dir_all(&dir); -} - -#[test] -fn exact_suppression_passes_and_stale_suppression_does_not_hide_blocker() { - let dir = unique_temp_dir("gate-suppression"); - let (before_lock, before_cache, after_lock, after_cache) = - changed_states(&dir, "1.0.0", "1.1.0"); - let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); - let fingerprint = finding_fingerprint_v1( - ¤t.compatibility.ruleset, - current.compatibility.findings.first().expect("finding"), - ) - .expect("fingerprint"); - let exact_path = dir.join("exact-suppressions.json"); - let exact = GateSuppressions { - schema: GateSuppressions::SCHEMA_V1, - suppressions: vec![GateSuppression { - finding_fingerprint: fingerprint, - rationale: "approved interoperability exception".to_owned(), - reference: Some("TEST-1".to_owned()), - }], - }; - fs::write( - &exact_path, - exact.to_json_bytes().expect("suppression JSON"), - ) - .expect("write exact suppression"); - - let exact_output = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--suppressions".to_owned(), - exact_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - assert_eq!(exact_output.status.code(), Some(0)); - let exact_report = - QualityGateReport::from_json_slice(&exact_output.stdout).expect("exact gate report"); - assert!(exact_report.decision.passed); - assert!(exact_report - .findings - .iter() - .all(|finding| finding.disposition == QualityGateDisposition::Suppressed)); - - let stale_path = dir.join("stale-suppressions.json"); - let stale = GateSuppressions { - schema: GateSuppressions::SCHEMA_V1, - suppressions: vec![GateSuppression { - finding_fingerprint: FindingFingerprint { - schema: FindingFingerprint::SCHEMA_V1, - digest: format!("sha256:{}", "f".repeat(64)), - }, - rationale: "stale exception".to_owned(), - reference: None, - }], - }; - fs::write(&stale_path, stale.to_json_bytes().expect("stale JSON")) - .expect("write stale suppression"); - let stale_output = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--suppressions".to_owned(), - stale_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - assert_eq!(stale_output.status.code(), Some(2)); - let stale_report = - QualityGateReport::from_json_slice(&stale_output.stdout).expect("stale gate report"); - assert!(!stale_report.decision.passed); - assert_eq!(stale_report.unused_suppressions.len(), 1); - assert!(stale_report - .findings - .iter() - .all(|finding| finding.disposition == QualityGateDisposition::New)); - let _ = fs::remove_dir_all(&dir); -} - -#[test] -fn malformed_mismatched_and_version_incompatible_inputs_exit_one() { - let dir = unique_temp_dir("gate-invalid-input"); - let (before_lock, before_cache, after_lock, after_cache) = - changed_states(&dir, "1.0.0", "1.1.0"); - - let malformed_path = dir.join("malformed.json"); - fs::write(&malformed_path, b"{").expect("write malformed input"); - let malformed = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--suppressions".to_owned(), - malformed_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - assert_eq!(malformed.status.code(), Some(1)); - - let mut mismatched = - current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); - mismatched.compatibility.package_name = "other.package".to_owned(); - let mismatched_path = dir.join("mismatched-baseline.json"); - fs::write( - &mismatched_path, - mismatched.to_json_bytes().expect("baseline JSON"), - ) - .expect("write mismatched baseline"); - let mismatch = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--baseline".to_owned(), - mismatched_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - assert_eq!(mismatch.status.code(), Some(1)); - - let current = current_check_report(&before_lock, &before_cache, &after_lock, &after_cache); - let current_fingerprint = finding_fingerprint_v1( - ¤t.compatibility.ruleset, - current.compatibility.findings.first().expect("finding"), - ) - .expect("fingerprint"); - let incompatible_path = dir.join("incompatible-suppressions.json"); - let incompatible = GateSuppressions { - schema: GateSuppressions::SCHEMA_V1, - suppressions: vec![GateSuppression { - finding_fingerprint: FindingFingerprint { - schema: 2, - digest: current_fingerprint.digest, - }, - rationale: "unsupported schema".to_owned(), - reference: None, - }], - }; - fs::write( - &incompatible_path, - incompatible.to_json_bytes().expect("incompatible JSON"), - ) - .expect("write incompatible suppressions"); - let incompatible_output = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[ - "--suppressions".to_owned(), - incompatible_path.to_str().expect("UTF-8 path").to_owned(), - ], - ); - assert_eq!(incompatible_output.status.code(), Some(1)); - let _ = fs::remove_dir_all(&dir); -} - -#[test] -fn repeated_gate_runs_are_byte_identical() { - let dir = unique_temp_dir("gate-determinism"); - let (before_lock, before_cache, after_lock, after_cache) = - changed_states(&dir, "1.0.0", "1.1.0"); - - let first = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[], - ); - let second = run_command( - "gate", - &before_lock, - &before_cache, - &after_lock, - &after_cache, - &[], - ); - - assert_eq!(first.status.code(), Some(2)); - assert_eq!(second.status.code(), Some(2)); - assert_eq!(first.stdout, second.stdout); - QualityGateReport::from_json_slice(&first.stdout).expect("deterministic report"); - let _ = fs::remove_dir_all(&dir); -} From 4069bc023b550106109ba1f7bc1944f92289a1de Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:33:41 +0300 Subject: [PATCH 11/23] style(cf13): apply rustfmt to gate regression --- crates/commandf-cli/tests/gate_behavior_v1.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/commandf-cli/tests/gate_behavior_v1.rs b/crates/commandf-cli/tests/gate_behavior_v1.rs index 49a9daa1..4d139c36 100644 --- a/crates/commandf-cli/tests/gate_behavior_v1.rs +++ b/crates/commandf-cli/tests/gate_behavior_v1.rs @@ -273,8 +273,11 @@ fn exact_suppression_passes_and_stale_suppression_does_not_hide_blocker() { .iter() .enumerate() .map(|(index, finding)| GateSuppression { - finding_fingerprint: finding_fingerprint_v1(¤t.compatibility.ruleset, finding) - .expect("fingerprint"), + finding_fingerprint: finding_fingerprint_v1( + ¤t.compatibility.ruleset, + finding, + ) + .expect("fingerprint"), rationale: "approved interoperability exception".to_owned(), reference: Some(format!("TEST-{}", index + 1)), }) From a620d3e41c332ac57534f90bca2162332965df28 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:36:01 +0300 Subject: [PATCH 12/23] ci(cf13): run renamed gate behavior target --- .github/workflows/cf13-quality-gate-proof.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cf13-quality-gate-proof.yml b/.github/workflows/cf13-quality-gate-proof.yml index 31889085..126df256 100644 --- a/.github/workflows/cf13-quality-gate-proof.yml +++ b/.github/workflows/cf13-quality-gate-proof.yml @@ -53,7 +53,7 @@ jobs: - name: Prove CF-13 CLI contract and deterministic evidence run: | set -euo pipefail - cargo test --locked -p commandf --test gate_behavior --test gate_exit_contract + cargo test --locked -p commandf --test gate_behavior_v1 --test gate_exit_contract cargo test --locked -p commandf --test gate_determinism_proof -- --nocapture --test-threads=1 \ | tee /tmp/cf13-quality-gate-proof.log grep -oE 'CF13_(GATE|BASELINE_CANONICAL|SUPPRESSION_CANONICAL|BEFORE_ARCHIVE|AFTER_ARCHIVE)_SHA256=(sha256:)?[0-9a-f]{64}' \ From 1683f635deccceca098d6c8932c2b632d6d05291 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:44:39 +0300 Subject: [PATCH 13/23] fix(cf13): bound verified cache reads --- crates/commandf-pkg/src/cache.rs | 90 ++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/crates/commandf-pkg/src/cache.rs b/crates/commandf-pkg/src/cache.rs index 325bfaf8..a6c34eba 100644 --- a/crates/commandf-pkg/src/cache.rs +++ b/crates/commandf-pkg/src/cache.rs @@ -1,5 +1,5 @@ use std::fs; -use std::io::Write; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; @@ -67,23 +67,52 @@ impl PackageCache { pub fn read_verified(&self, digest: &str) -> Result, PackageError> { validate_digest(digest)?; let path = self.object_path(digest); - let bytes = fs::read(&path).map_err(|error| { - if error.kind() == std::io::ErrorKind::NotFound { - PackageError::CacheMissing(digest.to_owned()) - } else { - PackageError::Io(error) - } - })?; - let found = Self::digest(&bytes); - if found != digest { - return Err(PackageError::CacheDigestMismatch { - path, - expected: digest.to_owned(), - found, - }); + let bytes = fs::read(&path).map_err(|error| map_cache_read_error(error, digest))?; + verify_cache_bytes(path, digest, bytes) + } + + pub fn read_verified_bounded( + &self, + digest: &str, + max_bytes: u64, + ) -> Result, PackageError> { + validate_digest(digest)?; + let path = self.object_path(digest); + let file = fs::File::open(&path).map_err(|error| map_cache_read_error(error, digest))?; + let read_limit = max_bytes.saturating_add(1); + let mut bytes = Vec::new(); + file.take(read_limit).read_to_end(&mut bytes)?; + if bytes.len() as u64 > max_bytes { + return Err(PackageError::InvalidRequest(format!( + "cache object exceeds the maximum supported size of {max_bytes} bytes" + ))); } - Ok(bytes) + verify_cache_bytes(path, digest, bytes) + } +} + +fn map_cache_read_error(error: std::io::Error, digest: &str) -> PackageError { + if error.kind() == std::io::ErrorKind::NotFound { + PackageError::CacheMissing(digest.to_owned()) + } else { + PackageError::Io(error) + } +} + +fn verify_cache_bytes( + path: PathBuf, + expected: &str, + bytes: Vec, +) -> Result, PackageError> { + let found = PackageCache::digest(&bytes); + if found != expected { + return Err(PackageError::CacheDigestMismatch { + path, + expected: expected.to_owned(), + found, + }); } + Ok(bytes) } fn validate_digest(digest: &str) -> Result<(), PackageError> { @@ -97,3 +126,32 @@ fn validate_digest(digest: &str) -> Result<(), PackageError> { Err(PackageError::InvalidDigest(digest.to_owned())) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bounded_verified_read_rejects_oversized_cache_object() { + let directory = tempfile::tempdir().expect("temp directory"); + let cache = PackageCache::new(directory.path()); + let digest = cache.put(b"abcd").expect("cache object"); + + let error = cache + .read_verified_bounded(&digest, 3) + .expect_err("oversized cache object must fail closed"); + assert!(matches!(error, PackageError::InvalidRequest(_))); + } + + #[test] + fn bounded_verified_read_returns_the_bytes_it_verified() { + let directory = tempfile::tempdir().expect("temp directory"); + let cache = PackageCache::new(directory.path()); + let digest = cache.put(b"abcd").expect("cache object"); + + let bytes = cache + .read_verified_bounded(&digest, 4) + .expect("bounded verified bytes"); + assert_eq!(bytes, b"abcd"); + } +} From 1373074db40b27cef19e13675bedb5aea0111c7a Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:44:58 +0300 Subject: [PATCH 14/23] fix(cf13): bound and bind primary gate inputs --- crates/commandf-cli/src/gate.rs | 48 ++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/crates/commandf-cli/src/gate.rs b/crates/commandf-cli/src/gate.rs index 6fd7fd04..e9e534eb 100644 --- a/crates/commandf-cli/src/gate.rs +++ b/crates/commandf-cli/src/gate.rs @@ -3,12 +3,15 @@ use std::process::ExitCode; use clap::{Args, ValueEnum}; use commandf_pkg::{ - classify_structural_diff, evaluate_compatibility_policy, evaluate_quality_gate, CheckDirection, - CheckFailOn, CheckPolicy, CheckReport, GateSuppressions, + classify_structural_diff, diff_package_archives, evaluate_compatibility_policy, + evaluate_quality_gate, CheckDirection, CheckFailOn, CheckPolicy, CheckReport, + GateSuppressions, Lockfile, PackageCache, PackageName, StructuralDiffReport, }; -use super::{build_diff_report, read_bounded_file, write_check_output}; +use super::{read_bounded_file, select_locked_package, write_check_output}; +const MAX_GATE_LOCKFILE_INPUT_BYTES: u64 = 16 * 1024 * 1024; +const MAX_GATE_ARCHIVE_INPUT_BYTES: u64 = 128 * 1024 * 1024; const MAX_GATE_BASELINE_INPUT_BYTES: u64 = 64 * 1024 * 1024; const MAX_GATE_SUPPRESSIONS_INPUT_BYTES: u64 = 64 * 1024 * 1024; @@ -77,7 +80,7 @@ impl From for CheckFailOn { } pub(crate) fn run(args: GateArgs) -> Result> { - let diff = build_diff_report( + let diff = build_gate_diff_report( args.package, args.before_lock, args.before_cache, @@ -122,3 +125,40 @@ pub(crate) fn run(args: GateArgs) -> Result Ok(ExitCode::from(2)) } } + +fn build_gate_diff_report( + package: String, + before_lock: PathBuf, + before_cache: PathBuf, + after_lock: PathBuf, + after_cache: PathBuf, +) -> Result> { + let package_name = PackageName::parse(package)?; + let before_lockfile = Lockfile::from_slice(&read_bounded_file( + &before_lock, + MAX_GATE_LOCKFILE_INPUT_BYTES, + )?)?; + let after_lockfile = Lockfile::from_slice(&read_bounded_file( + &after_lock, + MAX_GATE_LOCKFILE_INPUT_BYTES, + )?)?; + let before_locked = select_locked_package(&before_lockfile, package_name.as_str())?; + let after_locked = select_locked_package(&after_lockfile, package_name.as_str())?; + + let before_cache = PackageCache::new(before_cache); + let after_cache = PackageCache::new(after_cache); + let before_bytes = before_cache + .read_verified_bounded(&before_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?; + let after_bytes = + after_cache.read_verified_bounded(&after_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?; + + Ok(diff_package_archives( + package_name.to_string(), + &before_locked.version, + &before_locked.sha256, + &before_bytes, + &after_locked.version, + &after_locked.sha256, + &after_bytes, + )?) +} From 8ff2cad1bd0e24c08e2a97a1c7b1e0cebcbea847 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:46:57 +0300 Subject: [PATCH 15/23] fix(cf13): bind proof to exact source head --- .github/workflows/cf13-quality-gate-proof.yml | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cf13-quality-gate-proof.yml b/.github/workflows/cf13-quality-gate-proof.yml index 126df256..fb8e76b8 100644 --- a/.github/workflows/cf13-quality-gate-proof.yml +++ b/.github/workflows/cf13-quality-gate-proof.yml @@ -41,8 +41,14 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 with: + ref: ${{ env.CF13_SOURCE_SHA }} persist-credentials: false + - name: Assert exact source checkout + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$CF13_SOURCE_SHA" + - name: Assert pinned execution toolchain run: | set -euo pipefail @@ -53,7 +59,7 @@ jobs: - name: Prove CF-13 CLI contract and deterministic evidence run: | set -euo pipefail - cargo test --locked -p commandf --test gate_behavior_v1 --test gate_exit_contract + cargo test --locked -p commandf --test gate_behavior_v1 --test gate_bounds --test gate_exit_contract cargo test --locked -p commandf --test gate_determinism_proof -- --nocapture --test-threads=1 \ | tee /tmp/cf13-quality-gate-proof.log grep -oE 'CF13_(GATE|BASELINE_CANONICAL|SUPPRESSION_CANONICAL|BEFORE_ARCHIVE|AFTER_ARCHIVE)_SHA256=(sha256:)?[0-9a-f]{64}' \ @@ -69,7 +75,20 @@ jobs: set -euo pipefail { echo "CF13_SOURCE_SHA=$CF13_SOURCE_SHA" - echo "CF13_WORKFLOW_SHA=$(sha256sum .github/workflows/cf13-quality-gate-proof.yml | awk '{print $1}')" + echo "CF13_SOURCE_TREE=$(git rev-parse 'HEAD^{tree}')" + echo "CF13_WORKFLOW_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:.github/workflows/cf13-quality-gate-proof.yml")" + echo "CF13_AGENTS_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:AGENTS.md")" + echo "CF13_CONSTITUTION_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:.specify/memory/constitution.md")" + echo "CF13_SPEC_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/spec.md")" + echo "CF13_PLAN_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/plan.md")" + echo "CF13_TASKS_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/tasks.md")" + echo "CF13_CARGO_LOCK_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:Cargo.lock")" + echo "CF13_CF04_COMPATIBILITY_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/compatibility.rs")" + echo "CF13_CF05_CHECK_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/check.rs")" + echo "CF13_GATE_LIBRARY_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/gate.rs")" + echo "CF13_GATE_CLI_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/src/gate.rs")" + echo "CF13_PROOF_TEST_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/tests/gate_determinism_proof.rs")" + echo "CF13_WORKFLOW_SHA256=$(sha256sum .github/workflows/cf13-quality-gate-proof.yml | awk '{print $1}')" echo "CF13_CARGO_LOCK_SHA256=$(sha256sum Cargo.lock | awk '{print $1}')" echo "CF13_SPEC_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/spec.md | awk '{print $1}')" echo "CF13_PLAN_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/plan.md | awk '{print $1}')" From d928f9776f8cde2617ff15db0844f2dba2ccb531 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:47:38 +0300 Subject: [PATCH 16/23] test(cf13): prove bounded gate inputs --- crates/commandf-cli/tests/gate_bounds.rs | 122 +++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_bounds.rs diff --git a/crates/commandf-cli/tests/gate_bounds.rs b/crates/commandf-cli/tests/gate_bounds.rs new file mode 100644 index 00000000..4332606b --- /dev/null +++ b/crates/commandf-cli/tests/gate_bounds.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; + +const PROOF_ARCHIVE: &[u8] = include_bytes!("fixtures/proof.tgz"); +const GATE_LOCKFILE_LIMIT: u64 = 16 * 1024 * 1024; +const GATE_BASELINE_LIMIT: u64 = 64 * 1024 * 1024; + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +fn unique_temp_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after the Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) +} + +fn write_valid_state(root: &Path) -> (PathBuf, PathBuf) { + fs::create_dir_all(root).expect("create state root"); + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); + let cache = PackageCache::new(&cache_path); + let digest = cache.put(PROOF_ARCHIVE).expect("cache proof archive"); + let lockfile = Lockfile::new_v2( + vec!["acme.proof@1.0.0".to_owned()], + vec![LockedPackage { + name: "acme.proof".to_owned(), + version: "1.0.0".to_owned(), + sha256: digest, + source: "synthetic-gate-bounds".to_owned(), + dependencies: BTreeMap::new(), + }], + vec![], + ); + fs::write(&lock_path, lockfile.to_bytes().expect("serialize lockfile")) + .expect("write lockfile"); + (lock_path, cache_path) +} + +fn run_gate( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, + extra: &[String], +) -> Output { + let mut command = commandf(); + command.args([ + "gate", + "acme.proof", + "--before-lock", + before_lock.to_str().expect("UTF-8 path"), + "--before-cache", + before_cache.to_str().expect("UTF-8 path"), + "--after-lock", + after_lock.to_str().expect("UTF-8 path"), + "--after-cache", + after_cache.to_str().expect("UTF-8 path"), + ]); + command.args(extra); + command + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .expect("commandf gate must execute") +} + +fn create_sparse_file(path: &Path, bytes: u64) { + let file = fs::File::create(path).expect("create sparse input"); + file.set_len(bytes).expect("size sparse input"); +} + +#[test] +fn oversized_baseline_is_operational_exit_one() { + let root = unique_temp_dir("gate-oversized-baseline"); + let (lock, cache) = write_valid_state(&root.join("state")); + let baseline = root.join("oversized-baseline.json"); + create_sparse_file(&baseline, GATE_BASELINE_LIMIT + 1); + + let output = run_gate( + &lock, + &cache, + &lock, + &cache, + &[ + "--baseline".to_owned(), + baseline.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&output.stderr).contains("exceeds")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn oversized_primary_lockfile_is_operational_exit_one() { + let root = unique_temp_dir("gate-oversized-lockfile"); + let (valid_lock, cache) = write_valid_state(&root.join("state")); + let oversized_lock = root.join("oversized.lock"); + create_sparse_file(&oversized_lock, GATE_LOCKFILE_LIMIT + 1); + + let output = run_gate( + &oversized_lock, + &cache, + &valid_lock, + &cache, + &[], + ); + + assert_eq!(output.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&output.stderr).contains("exceeds")); + let _ = fs::remove_dir_all(root); +} From e2647dd4ddd9f38830e0c3f9a12d7f8c1d09db9c Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:48:44 +0300 Subject: [PATCH 17/23] style(cf13): apply rustfmt to bounded gate loader --- crates/commandf-cli/src/gate.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/commandf-cli/src/gate.rs b/crates/commandf-cli/src/gate.rs index e9e534eb..d305f55c 100644 --- a/crates/commandf-cli/src/gate.rs +++ b/crates/commandf-cli/src/gate.rs @@ -4,8 +4,8 @@ use std::process::ExitCode; use clap::{Args, ValueEnum}; use commandf_pkg::{ classify_structural_diff, diff_package_archives, evaluate_compatibility_policy, - evaluate_quality_gate, CheckDirection, CheckFailOn, CheckPolicy, CheckReport, - GateSuppressions, Lockfile, PackageCache, PackageName, StructuralDiffReport, + evaluate_quality_gate, CheckDirection, CheckFailOn, CheckPolicy, CheckReport, GateSuppressions, + Lockfile, PackageCache, PackageName, StructuralDiffReport, }; use super::{read_bounded_file, select_locked_package, write_check_output}; @@ -147,8 +147,8 @@ fn build_gate_diff_report( let before_cache = PackageCache::new(before_cache); let after_cache = PackageCache::new(after_cache); - let before_bytes = before_cache - .read_verified_bounded(&before_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?; + let before_bytes = + before_cache.read_verified_bounded(&before_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?; let after_bytes = after_cache.read_verified_bounded(&after_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?; From f82612c6beb9e33ad3aaf9b23b720359187d5938 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:49:02 +0300 Subject: [PATCH 18/23] style(cf13): apply rustfmt to gate bounds regression --- crates/commandf-cli/tests/gate_bounds.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/commandf-cli/tests/gate_bounds.rs b/crates/commandf-cli/tests/gate_bounds.rs index 4332606b..79102a85 100644 --- a/crates/commandf-cli/tests/gate_bounds.rs +++ b/crates/commandf-cli/tests/gate_bounds.rs @@ -108,13 +108,7 @@ fn oversized_primary_lockfile_is_operational_exit_one() { let oversized_lock = root.join("oversized.lock"); create_sparse_file(&oversized_lock, GATE_LOCKFILE_LIMIT + 1); - let output = run_gate( - &oversized_lock, - &cache, - &valid_lock, - &cache, - &[], - ); + let output = run_gate(&oversized_lock, &cache, &valid_lock, &cache, &[]); assert_eq!(output.status.code(), Some(1)); assert!(String::from_utf8_lossy(&output.stderr).contains("exceeds")); From c7d7332008438d1b8a3dcc59a733c990f4a80fc1 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 13:51:23 +0300 Subject: [PATCH 19/23] ci(cf13): use safe-directory for proof provenance --- .github/workflows/cf13-quality-gate-proof.yml | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cf13-quality-gate-proof.yml b/.github/workflows/cf13-quality-gate-proof.yml index fb8e76b8..a8bf0f75 100644 --- a/.github/workflows/cf13-quality-gate-proof.yml +++ b/.github/workflows/cf13-quality-gate-proof.yml @@ -47,7 +47,7 @@ jobs: - name: Assert exact source checkout run: | set -euo pipefail - test "$(git rev-parse HEAD)" = "$CF13_SOURCE_SHA" + test "$(git -c safe.directory="$GITHUB_WORKSPACE" rev-parse HEAD)" = "$CF13_SOURCE_SHA" - name: Assert pinned execution toolchain run: | @@ -73,21 +73,24 @@ jobs: - name: Record immutable repository evidence run: | set -euo pipefail + git_safe() { + git -c safe.directory="$GITHUB_WORKSPACE" "$@" + } { echo "CF13_SOURCE_SHA=$CF13_SOURCE_SHA" - echo "CF13_SOURCE_TREE=$(git rev-parse 'HEAD^{tree}')" - echo "CF13_WORKFLOW_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:.github/workflows/cf13-quality-gate-proof.yml")" - echo "CF13_AGENTS_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:AGENTS.md")" - echo "CF13_CONSTITUTION_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:.specify/memory/constitution.md")" - echo "CF13_SPEC_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/spec.md")" - echo "CF13_PLAN_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/plan.md")" - echo "CF13_TASKS_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/tasks.md")" - echo "CF13_CARGO_LOCK_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:Cargo.lock")" - echo "CF13_CF04_COMPATIBILITY_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/compatibility.rs")" - echo "CF13_CF05_CHECK_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/check.rs")" - echo "CF13_GATE_LIBRARY_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/gate.rs")" - echo "CF13_GATE_CLI_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/src/gate.rs")" - echo "CF13_PROOF_TEST_BLOB=$(git rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/tests/gate_determinism_proof.rs")" + echo "CF13_SOURCE_TREE=$(git_safe rev-parse 'HEAD^{tree}')" + echo "CF13_WORKFLOW_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:.github/workflows/cf13-quality-gate-proof.yml")" + echo "CF13_AGENTS_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:AGENTS.md")" + echo "CF13_CONSTITUTION_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:.specify/memory/constitution.md")" + echo "CF13_SPEC_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/spec.md")" + echo "CF13_PLAN_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/plan.md")" + echo "CF13_TASKS_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/tasks.md")" + echo "CF13_CARGO_LOCK_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:Cargo.lock")" + echo "CF13_CF04_COMPATIBILITY_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/compatibility.rs")" + echo "CF13_CF05_CHECK_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/check.rs")" + echo "CF13_GATE_LIBRARY_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/gate.rs")" + echo "CF13_GATE_CLI_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/src/gate.rs")" + echo "CF13_PROOF_TEST_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/tests/gate_determinism_proof.rs")" echo "CF13_WORKFLOW_SHA256=$(sha256sum .github/workflows/cf13-quality-gate-proof.yml | awk '{print $1}')" echo "CF13_CARGO_LOCK_SHA256=$(sha256sum Cargo.lock | awk '{print $1}')" echo "CF13_SPEC_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/spec.md | awk '{print $1}')" From 08c5072d2743f41c8e0b32fd263ffe841c33e649 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 17:00:51 +0300 Subject: [PATCH 20/23] test(cf13): cover oversized optional gate inputs --- .../tests/gate_optional_input_bounds.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 crates/commandf-cli/tests/gate_optional_input_bounds.rs diff --git a/crates/commandf-cli/tests/gate_optional_input_bounds.rs b/crates/commandf-cli/tests/gate_optional_input_bounds.rs new file mode 100644 index 00000000..7ed9b9dc --- /dev/null +++ b/crates/commandf-cli/tests/gate_optional_input_bounds.rs @@ -0,0 +1,160 @@ +use std::collections::BTreeMap; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; + +const OPTIONAL_INPUT_LIMIT_BYTES: u64 = 64 * 1024 * 1024; + +const BEFORE_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", + "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", + "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", + "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", + "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", + "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", + "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", + "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", +); + +const AFTER_HEX: &str = concat!( + "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", + "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", + "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", + "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", + "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", + "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", + "49a19bad95768d95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", + "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", + "00", +); + +fn commandf() -> Command { + Command::new(env!("CARGO_BIN_EXE_commandf")) +} + +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) + .collect() +} + +fn hex_digit(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => panic!("invalid test hex digit"), + } +} + +fn unique_temp_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after the Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) +} + +fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { + let cache_path = root.join("cache"); + let lock_path = root.join("commandf.lock"); + fs::create_dir_all(root).expect("create state root"); + let cache = PackageCache::new(&cache_path); + let digest = cache.put(archive).expect("cache synthetic archive"); + let lockfile = Lockfile::new( + vec![format!("example.package@{version}")], + vec![LockedPackage { + name: "example.package".to_owned(), + version: version.to_owned(), + sha256: digest, + source: "synthetic-test".to_owned(), + dependencies: BTreeMap::new(), + }], + ); + fs::write(&lock_path, lockfile.to_bytes().expect("serialize lock")).expect("write lock"); + (lock_path, cache_path) +} + +fn changed_states(dir: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + let before = decode_hex(BEFORE_HEX); + let after = decode_hex(AFTER_HEX); + let (before_lock, before_cache) = write_locked_state(&dir.join("before"), &before, "1.0.0"); + let (after_lock, after_cache) = write_locked_state(&dir.join("after"), &after, "1.1.0"); + (before_lock, before_cache, after_lock, after_cache) +} + +fn run_gate( + before_lock: &Path, + before_cache: &Path, + after_lock: &Path, + after_cache: &Path, + flag: &str, + input: &Path, +) -> Output { + commandf() + .args([ + "gate", + "example.package", + "--before-lock", + before_lock.to_str().expect("UTF-8 path"), + "--before-cache", + before_cache.to_str().expect("UTF-8 path"), + "--after-lock", + after_lock.to_str().expect("UTF-8 path"), + "--after-cache", + after_cache.to_str().expect("UTF-8 path"), + flag, + input.to_str().expect("UTF-8 path"), + ]) + .env("HTTP_PROXY", "http://127.0.0.1:9") + .env("HTTPS_PROXY", "http://127.0.0.1:9") + .env("NO_PROXY", "") + .output() + .expect("commandf gate must execute") +} + +fn assert_oversized_optional_input_is_rejected(flag: &str, label: &str) { + let dir = unique_temp_dir(label); + let (before_lock, before_cache, after_lock, after_cache) = changed_states(&dir); + let oversized_path = dir.join("oversized.json"); + let oversized = File::create(&oversized_path).expect("create sparse oversized input"); + oversized + .set_len(OPTIONAL_INPUT_LIMIT_BYTES + 1) + .expect("extend sparse oversized input"); + + let output = run_gate( + &before_lock, + &before_cache, + &after_lock, + &after_cache, + flag, + &oversized_path, + ); + + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("UTF-8 diagnostic"); + assert!( + stderr.contains("input exceeds 67108864 byte limit"), + "unexpected diagnostic: {stderr}" + ); + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn oversized_baseline_exits_one_with_bounded_read_diagnostic() { + assert_oversized_optional_input_is_rejected("--baseline", "gate-oversized-baseline"); +} + +#[test] +fn oversized_suppressions_exit_one_with_bounded_read_diagnostic() { + assert_oversized_optional_input_is_rejected( + "--suppressions", + "gate-oversized-suppressions", + ); +} From 49c78bdfbe3abf2b0e953e40bdabdf460c54c0ee Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 17:01:29 +0300 Subject: [PATCH 21/23] fix(cf13): correct oversized-input test fixture --- crates/commandf-cli/tests/gate_optional_input_bounds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/commandf-cli/tests/gate_optional_input_bounds.rs b/crates/commandf-cli/tests/gate_optional_input_bounds.rs index 7ed9b9dc..5559ad3f 100644 --- a/crates/commandf-cli/tests/gate_optional_input_bounds.rs +++ b/crates/commandf-cli/tests/gate_optional_input_bounds.rs @@ -26,7 +26,7 @@ const AFTER_HEX: &str = concat!( "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", - "49a19bad95768d95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", + "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", "00", ); From bdf34d75579fea3b7f1b24505dc513b868e6614b Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 17:04:39 +0300 Subject: [PATCH 22/23] test(cf13): cover oversized suppression input --- crates/commandf-cli/tests/gate_bounds.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/commandf-cli/tests/gate_bounds.rs b/crates/commandf-cli/tests/gate_bounds.rs index 79102a85..eae25fba 100644 --- a/crates/commandf-cli/tests/gate_bounds.rs +++ b/crates/commandf-cli/tests/gate_bounds.rs @@ -9,6 +9,7 @@ use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; const PROOF_ARCHIVE: &[u8] = include_bytes!("fixtures/proof.tgz"); const GATE_LOCKFILE_LIMIT: u64 = 16 * 1024 * 1024; const GATE_BASELINE_LIMIT: u64 = 64 * 1024 * 1024; +const GATE_SUPPRESSIONS_LIMIT: u64 = 64 * 1024 * 1024; fn commandf() -> Command { Command::new(env!("CARGO_BIN_EXE_commandf")) @@ -101,6 +102,29 @@ fn oversized_baseline_is_operational_exit_one() { let _ = fs::remove_dir_all(root); } +#[test] +fn oversized_suppressions_is_operational_exit_one() { + let root = unique_temp_dir("gate-oversized-suppressions"); + let (lock, cache) = write_valid_state(&root.join("state")); + let suppressions = root.join("oversized-suppressions.json"); + create_sparse_file(&suppressions, GATE_SUPPRESSIONS_LIMIT + 1); + + let output = run_gate( + &lock, + &cache, + &lock, + &cache, + &[ + "--suppressions".to_owned(), + suppressions.to_str().expect("UTF-8 path").to_owned(), + ], + ); + + assert_eq!(output.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&output.stderr).contains("exceeds")); + let _ = fs::remove_dir_all(root); +} + #[test] fn oversized_primary_lockfile_is_operational_exit_one() { let root = unique_temp_dir("gate-oversized-lockfile"); From 06da4f3f61b47afe11525b2c33306b5952cd680e Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Wed, 26 Aug 2026 17:04:46 +0300 Subject: [PATCH 23/23] test(cf13): consolidate optional input bounds coverage --- .../tests/gate_optional_input_bounds.rs | 160 ------------------ 1 file changed, 160 deletions(-) delete mode 100644 crates/commandf-cli/tests/gate_optional_input_bounds.rs diff --git a/crates/commandf-cli/tests/gate_optional_input_bounds.rs b/crates/commandf-cli/tests/gate_optional_input_bounds.rs deleted file mode 100644 index 5559ad3f..00000000 --- a/crates/commandf-cli/tests/gate_optional_input_bounds.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::collections::BTreeMap; -use std::fs::{self, File}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use commandf_pkg::{LockedPackage, Lockfile, PackageCache}; - -const OPTIONAL_INPUT_LIMIT_BYTES: u64 = 64 * 1024 * 1024; - -const BEFORE_HEX: &str = concat!( - "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f63b452cf70e60037c4216bbd35d0a655924e43d3fe3beed66d6c", - "abc4013409789f1edc388df336b1ddc8ec4dce2968b6d67fb5b5f67e98908927938d654e6d1826f1e1bdf347517c137957a1", - "77015aeba4e1edbdffc94a68599148052d65d594e4f78920466241c6aa5af35ce4877ec89e9c1ad239e94c9115e96abdf6c0", - "2fa7bfeee0d1993673ada13b9a29ad1c5ffcf52e25bedb13beaaff241e9fd4ff6d128e51ff97a97f43b66e4d464fef4dd707", - "0612812b5fe58716c1c3d6943c2e9c6bd220d8a5496de64369141c969d7794bef9dcef3fe1db702d37172133a7169de74de9", - "6ef39d4cf6c8a97586e7453a93a5a591705be90f534b66217bc953693fffc35e6e51261ba9b3429941bdc7617232aa1fa422", - "ab75b7b5d2ae93aa65638b9a65ac049554117bd3e7d5f6ac8e8334d21567ce4a71c890ad5c762722d6a3f3b57e7f1e43210e", - "73a7917627c262b23aa78d7036eb177ed0b3010000000000000000000000000000000000803fc7075ec49c6300280000", -); - -const AFTER_HEX: &str = concat!( - "1f8b08000000000002ffed944d4fc3300c86fb5350cea31f636c52cf70e60037c4216bbd35d0a555924e4353ff3b6ed7ad6c", - "abc4013409789f1edc388df336b15dcae44d2e292877d67fb585f67e9890994e26ad654e6d18cea6fd7be38fa2e94de45d85", - "de05a8ac9386b7f7fe275ba1e58a442c682357654e7e97086224d664ac2a34cf457ee487ec49a9249d924e1459116febda03", - "bf9ceeba834767aac45586ee68a1b4727cf1d7fb94f86e4ff8aafe67d3f149fddfcec231eaff32f56fc8169549e8e9bd6cfa", - "c0402270e5abb46f113cac4ccee3ccb9320e827d9a146639944641bfecbca374cde7fef009df86abb8b9089938b56e3c6f4a", - "379bef65b247cead333c2fe285cc2d8d84db497f985b326bd9499e4bfbf91f0e72b37cd64a5d64ca0cea3d0e939251dd2016", - "49a19bad95768d542d4b9b152c632b28a715b1377edeeeceea3848295d76e65c290e19b2959be644443d3a5feb77e73114a2", - "9f6b23457da4fd89b098a448a915cea67ee1073d1b000000000000000000000000000000000000f8737c00934f6565002800", - "00", -); - -fn commandf() -> Command { - Command::new(env!("CARGO_BIN_EXE_commandf")) -} - -fn decode_hex(value: &str) -> Vec { - assert_eq!(value.len() % 2, 0); - value - .as_bytes() - .chunks_exact(2) - .map(|pair| (hex_digit(pair[0]) << 4) | hex_digit(pair[1])) - .collect() -} - -fn hex_digit(value: u8) -> u8 { - match value { - b'0'..=b'9' => value - b'0', - b'a'..=b'f' => value - b'a' + 10, - _ => panic!("invalid test hex digit"), - } -} - -fn unique_temp_dir(label: &str) -> PathBuf { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock must be after the Unix epoch") - .as_nanos(); - std::env::temp_dir().join(format!("commandf-{label}-{}-{nonce}", std::process::id())) -} - -fn write_locked_state(root: &Path, archive: &[u8], version: &str) -> (PathBuf, PathBuf) { - let cache_path = root.join("cache"); - let lock_path = root.join("commandf.lock"); - fs::create_dir_all(root).expect("create state root"); - let cache = PackageCache::new(&cache_path); - let digest = cache.put(archive).expect("cache synthetic archive"); - let lockfile = Lockfile::new( - vec![format!("example.package@{version}")], - vec![LockedPackage { - name: "example.package".to_owned(), - version: version.to_owned(), - sha256: digest, - source: "synthetic-test".to_owned(), - dependencies: BTreeMap::new(), - }], - ); - fs::write(&lock_path, lockfile.to_bytes().expect("serialize lock")).expect("write lock"); - (lock_path, cache_path) -} - -fn changed_states(dir: &Path) -> (PathBuf, PathBuf, PathBuf, PathBuf) { - let before = decode_hex(BEFORE_HEX); - let after = decode_hex(AFTER_HEX); - let (before_lock, before_cache) = write_locked_state(&dir.join("before"), &before, "1.0.0"); - let (after_lock, after_cache) = write_locked_state(&dir.join("after"), &after, "1.1.0"); - (before_lock, before_cache, after_lock, after_cache) -} - -fn run_gate( - before_lock: &Path, - before_cache: &Path, - after_lock: &Path, - after_cache: &Path, - flag: &str, - input: &Path, -) -> Output { - commandf() - .args([ - "gate", - "example.package", - "--before-lock", - before_lock.to_str().expect("UTF-8 path"), - "--before-cache", - before_cache.to_str().expect("UTF-8 path"), - "--after-lock", - after_lock.to_str().expect("UTF-8 path"), - "--after-cache", - after_cache.to_str().expect("UTF-8 path"), - flag, - input.to_str().expect("UTF-8 path"), - ]) - .env("HTTP_PROXY", "http://127.0.0.1:9") - .env("HTTPS_PROXY", "http://127.0.0.1:9") - .env("NO_PROXY", "") - .output() - .expect("commandf gate must execute") -} - -fn assert_oversized_optional_input_is_rejected(flag: &str, label: &str) { - let dir = unique_temp_dir(label); - let (before_lock, before_cache, after_lock, after_cache) = changed_states(&dir); - let oversized_path = dir.join("oversized.json"); - let oversized = File::create(&oversized_path).expect("create sparse oversized input"); - oversized - .set_len(OPTIONAL_INPUT_LIMIT_BYTES + 1) - .expect("extend sparse oversized input"); - - let output = run_gate( - &before_lock, - &before_cache, - &after_lock, - &after_cache, - flag, - &oversized_path, - ); - - assert_eq!(output.status.code(), Some(1)); - assert!(output.stdout.is_empty()); - let stderr = String::from_utf8(output.stderr).expect("UTF-8 diagnostic"); - assert!( - stderr.contains("input exceeds 67108864 byte limit"), - "unexpected diagnostic: {stderr}" - ); - let _ = fs::remove_dir_all(&dir); -} - -#[test] -fn oversized_baseline_exits_one_with_bounded_read_diagnostic() { - assert_oversized_optional_input_is_rejected("--baseline", "gate-oversized-baseline"); -} - -#[test] -fn oversized_suppressions_exit_one_with_bounded_read_diagnostic() { - assert_oversized_optional_input_is_rejected( - "--suppressions", - "gate-oversized-suppressions", - ); -}