Skip to content

Commit 64d9be4

Browse files
authored
fix(audit): distinguish missing command-status fixture from contract violation (#8339)
A declared command_status_contracts scenario whose golden fixture file is missing or unreadable was reported as a CommandStatusContractViolation — the same finding kind as genuine status-contract drift (wrong field values, invalid envelope semantics). A missing fixture is test-data hygiene, not a contract violation. Add a distinct CommandStatusFixtureMissing finding kind (serialized as command_status_fixture_missing) with a non-"violates contract" description, and emit it from the missing/unreadable-fixture branch. Wire the new variant through all_names, the confidence map (structural), and the detector descriptor's declared findings so --only routing works. Resolves #8300.
1 parent b7b032f commit 64d9be4

4 files changed

Lines changed: 60 additions & 3 deletions

File tree

src/core/code_audit/conventions/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ pub enum AuditFinding {
444444
UnboundedOutputCapture,
445445
/// Declared command scenario output differs from its expected status contract.
446446
CommandStatusContractViolation,
447+
/// A declared command-status scenario references a golden fixture file that
448+
/// is missing or unreadable. This is test-data hygiene (write or remove the
449+
/// fixture), distinct from an actual status-contract violation.
450+
CommandStatusFixtureMissing,
447451
/// A command-layer module accumulates orchestration/business logic that
448452
/// should live in a core service. Command modules are expected to stay thin
449453
/// adapters (argument parsing, typed request construction, output
@@ -522,6 +526,7 @@ impl AuditFinding {
522526
"non_portable_artifact_path",
523527
"unbounded_output_capture",
524528
"command_status_contract_violation",
529+
"command_status_fixture_missing",
525530
"thin_command_adapter_violation",
526531
]
527532
}

src/core/code_audit/detectors/command_status_contracts.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub(in crate::core::code_audit) fn run(
5252
for scenario in &config.scenarios {
5353
let fixture = root.join(&scenario.file);
5454
let Ok(content) = std::fs::read_to_string(&fixture) else {
55-
findings.push(finding(
55+
findings.push(fixture_missing_finding(
5656
&scenario.file,
5757
&scenario.id,
5858
"scenario fixture is missing or unreadable".to_string(),
@@ -363,6 +363,27 @@ fn finding(file: &str, scenario_id: &str, description: String, suggestion: Strin
363363
}
364364
}
365365

366+
/// A declared scenario's golden fixture file is missing or unreadable. This is
367+
/// test-data hygiene, not a status-contract violation, so it gets its own kind
368+
/// and a non-"violates contract" description.
369+
fn fixture_missing_finding(
370+
file: &str,
371+
scenario_id: &str,
372+
description: String,
373+
suggestion: String,
374+
) -> Finding {
375+
Finding {
376+
convention: "command_status_contracts".to_string(),
377+
severity: Severity::Warning,
378+
file: file.to_string(),
379+
description: format!(
380+
"Command status scenario '{scenario_id}' fixture unavailable: {description}"
381+
),
382+
suggestion,
383+
kind: AuditFinding::CommandStatusFixtureMissing,
384+
}
385+
}
386+
366387
fn json_value_label(value: &serde_json::Value) -> String {
367388
serde_json::to_string(value).unwrap_or_else(|_| "<unserializable>".to_string())
368389
}
@@ -574,6 +595,33 @@ mod tests {
574595
assert!(findings[0].suggestion.contains("successful empty results"));
575596
}
576597

598+
#[test]
599+
fn missing_fixture_reports_distinct_kind_not_contract_violation() {
600+
let dir = tempfile::tempdir().expect("tempdir");
601+
// No fixture file is written, so the declared scenario's golden fixture
602+
// is missing.
603+
let config = CommandStatusContractConfig {
604+
required_commands: Vec::new(),
605+
required_output_error_commands: Vec::new(),
606+
scenarios: vec![scenario(
607+
"review-artifact-golden",
608+
Some("review"),
609+
"does-not-exist.json",
610+
None,
611+
false,
612+
[("/success", serde_json::json!(false))],
613+
)],
614+
};
615+
616+
let findings = run(dir.path(), &config);
617+
assert_eq!(findings.len(), 1);
618+
let finding = &findings[0];
619+
assert_eq!(finding.kind, AuditFinding::CommandStatusFixtureMissing);
620+
assert_ne!(finding.kind, AuditFinding::CommandStatusContractViolation);
621+
assert!(finding.description.contains("fixture unavailable"));
622+
assert!(!finding.description.contains("violates contract"));
623+
}
624+
577625
fn scenario<const N: usize>(
578626
id: &str,
579627
command: Option<&str>,

src/core/code_audit/execution_plan.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,10 @@ const DETECTOR_DESCRIPTORS: &[DetectorDescriptor] = &[
491491
},
492492
DetectorDescriptor {
493493
id: "command_status_contracts",
494-
findings: &[AuditFinding::CommandStatusContractViolation],
494+
findings: &[
495+
AuditFinding::CommandStatusContractViolation,
496+
AuditFinding::CommandStatusFixtureMissing,
497+
],
495498
access: DetectorAccess::RootOnly,
496499
runtime: DetectorRuntime::Generic(GenericDetectorRunner::CommandStatusContracts),
497500
timing_id: "detector.command_status_contracts",

src/core/code_audit/findings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ impl AuditFinding {
184184
| AuditFinding::StaleDocReference
185185
| AuditFinding::UnwiredNestedRustTest
186186
| AuditFinding::NonPortableArtifactPath
187-
| AuditFinding::CommandStatusContractViolation => FindingConfidence::Structural,
187+
| AuditFinding::CommandStatusContractViolation
188+
| AuditFinding::CommandStatusFixtureMissing => FindingConfidence::Structural,
188189

189190
// Depends on cross-file reference resolution or declared ownership maps.
190191
AuditFinding::UnusedParameter

0 commit comments

Comments
 (0)