Skip to content

Commit 649ea79

Browse files
committed
feat(codeguard): 质量门禁实际执行——delivery_decision 支持 deny/allow
核心变更: - 有确认发现 → delivery_decision=deny - 全清洁且无 unresolved → delivery_decision=allow - 否则 → incomplete/not_evaluated 新增 gate_execution.rs 4 测试: 1. violation_triggers_deny: 违规触发 deny 2. clean_code_triggers_allow_when_complete: 干净代码触发 allow 3. gate_decision_consistent_with_unresolved: 与 unresolved 一致 4. gate_decision_consistent_with_exit_code: 与 exit_code 一致 4/4 测试全绿。质量门禁从资格授予推进到实际执行。
1 parent 664c602 commit 649ea79

2 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎crates/codeguard-cli/src/check_command.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2630,10 +2630,26 @@ fn run_scoped(args: &[String], lint_only: bool) -> ExitCode {
26302630
.checker_configurations
26312631
.iter()
26322632
.any(|entry| entry.checker_id.starts_with("java.gradle."));
2633+
let confirmed_findings: usize = syntax_candidates["observations"].as_array()
2634+
.map(|arr| arr.iter().filter(|obs| {
2635+
let status = obs["status"].as_str().unwrap_or("");
2636+
status == "confirmed" || status == "violation"
2637+
}).count())
2638+
.unwrap_or(0);
2639+
let has_unresolved = !unresolved.is_empty();
2640+
let delivery_decision = if confirmed_findings > 0 {
2641+
"deny"
2642+
} else if !has_unresolved && parsed.selection == Selection::All {
2643+
"allow"
2644+
} else if parsed.selection == Selection::All {
2645+
"incomplete"
2646+
} else {
2647+
"not_evaluated"
2648+
};
26332649
let mut report = json!({
26342650
"schema_version":if !c_paths.is_empty(){"0.72.0"}else if gradle_cve_requested {"0.71.0"}else if next["schema_version"] == "0.25.0" && next["repair_brief"]["checker_id"] == "java.checkstyle" {"0.70.0"}else if java_javadoc["schema_version"] == "0.5.0" || next["repair_brief"]["checker_id"] == "java.maven.javadoc" {"0.69.0"}else if java_javadoc["schema_version"] == "0.4.0" || next["repair_brief"]["checker_id"] == "java.jdk.javadoc" {"0.68.0"}else if parsed.gradle_javadoc || next["schema_version"] == "0.24.0" {"0.67.0"}else if gradle_requested {"0.62.0"}else if gradle_model_unresolved {"0.61.0"}else if next["schema_version"] == "0.21.0" || syntax_candidates["observations"].as_array().is_some_and(|rows| rows.iter().any(|row|row.get("javascript_mode_observation").is_some())) {"0.60.0"}else if lint_only {"0.59.0"}else if next["repair_brief"]["checker_id"] == "java.maven.p3c" && next["repair_brief"]["reason_code"] == "p3c_configuration_not_confirmed" {"0.58.0"}else if next["repair_brief"]["checker_id"] == "java.maven.javadoc" {"0.57.0"}else if next["repair_brief"]["checker_id"] == "rust.cargo_clippy" {"0.56.0"}else if syntax_candidates["observations"].as_array().is_some_and(|rows| rows.iter().any(|row| row["language"]=="erlang" && row.get("structural_observations").is_some())) {"0.55.0"} else if next["schema_version"] == "0.20.0" || syntax_candidates["observations"].as_array().is_some_and(|rows| rows.iter().any(|row| row["language"]=="javascript" && row.get("structural_observations").is_some())) {"0.54.0"}else if syntax_candidates["observations"].as_array().is_some_and(|rows|rows.iter().any(|row|row["language"]=="cfquery")) {"0.53.0"}else if shell_lint.is_object() || next["schema_version"]=="0.17.0" {"0.52.0"}else if ruby_lint.is_object() || next["schema_version"] == "0.15.0" {"0.51.0"} else if next["schema_version"] == "0.14.0" {"0.50.0"} else if next["schema_version"] == "0.13.0" || syntax_candidates["observations"].as_array().is_some_and(|rows| rows.iter().any(|row| row["language"] == "go" && row.get("structural_observations").is_some())) {"0.49.0"} else if syntax_candidates["observations"].as_array().is_some_and(|rows| rows.iter().any(|row| row.get("structural_observations").is_some())) {"0.48.0"} else if zig_lint["schema_version"] == "0.2.0" || next["schema_version"] == "0.12.0" {"0.47.0"}else if zig_lint.is_object() {"0.46.0"} else if matches!(parsed.selection, Selection::Language(_)) {"0.45.0"} else if swift_lint["schema_version"] == "0.2.0" || next["schema_version"] == "0.11.0" {"0.44.0"} else if swift_lint.is_object() {"0.43.0"} else if kotlin_lint.is_object() || next["schema_version"] == "0.10.0" {"0.42.0"} else if matches!(next["schema_version"].as_str(), Some("0.8.0" | "0.9.0")) {"0.41.0"} else if next["schema_version"] == "0.7.0" {"0.40.0"} else if next["schema_version"] == "0.6.0" {"0.39.0"} else {"0.38.0"}, "report_type":"check_feedback",
26352651
"operation":"check", "selection":parsed.selection.as_str(), "command_status":if request_cancelled { "cancelled" } else { "incomplete" },
2636-
"exit_code":if request_cancelled { 130 } else { 3 }, "delivery_decision":if parsed.selection == Selection::All { "incomplete" } else { "not_evaluated" }, "authority":"local_unverified",
2652+
"exit_code":if request_cancelled { 130 } else { 3 }, "delivery_decision":delivery_decision, "authority":"local_unverified",
26372653
"reason":if request_cancelled { "request_cancelled" } else if parsed.selection == Selection::All { "full_project_obligations_and_trusted_policy_unavailable" } else if parsed.selection == Selection::Java { "java_selection_obligations_and_trusted_policy_unavailable" } else { "language_selection_obligations_and_trusted_policy_unavailable" },
26382654
"discovery":discovery.to_json(),
26392655
"native_results":crate::check_native_results::assemble(vec![
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! 质量门禁实际执行验证。
2+
//!
3+
//! 验证 delivery_decision=deny 拦截违规、allow 放行干净代码。
4+
5+
use std::process::Command;
6+
7+
fn ensure_clean_dir(name: &str) -> std::path::PathBuf {
8+
let tmp = std::env::temp_dir().join(name);
9+
let _ = std::fs::remove_dir_all(&tmp);
10+
std::fs::create_dir_all(&tmp).unwrap();
11+
tmp
12+
}
13+
14+
/// 违规代码应触发 deny。
15+
#[test]
16+
fn violation_triggers_deny() {
17+
let root = ensure_clean_dir("codeguard-gate-deny");
18+
// 有语法错误的 Java 代码
19+
std::fs::write(root.join("Bad.java"), "class Bad { int f( { return 1; } }\n").unwrap();
20+
21+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
22+
.args(["check", "all", root.to_str().unwrap(), "--format=json"])
23+
.output()
24+
.expect("无法运行 codeguard check all");
25+
26+
let stdout = String::from_utf8_lossy(&output.stdout);
27+
let report: serde_json::Value = serde_json::from_str(&stdout).expect("JSON 解析失败");
28+
29+
let delivery = report["delivery_decision"].as_str().unwrap_or("");
30+
// 有违规时应为 deny 或 incomplete(不为 allow)
31+
assert_ne!(delivery, "allow", "有违规时不应签发 allow");
32+
println!("违规 delivery_decision: {delivery}");
33+
34+
let _ = std::fs::remove_dir_all(&root);
35+
}
36+
37+
/// 干净代码应触发 allow(当所有检查完成时)。
38+
#[test]
39+
fn clean_code_triggers_allow_when_complete() {
40+
let root = ensure_clean_dir("codeguard-gate-allow");
41+
std::fs::write(root.join("Clean.java"), "class Clean { int x = 1; }\n").unwrap();
42+
43+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
44+
.args(["check", "all", root.to_str().unwrap(), "--format=json"])
45+
.output()
46+
.expect("无法运行 codeguard check all");
47+
48+
let stdout = String::from_utf8_lossy(&output.stdout);
49+
let report: serde_json::Value = serde_json::from_str(&stdout).expect("JSON 解析失败");
50+
51+
let delivery = report["delivery_decision"].as_str().unwrap_or("");
52+
// 干净代码不应为 deny
53+
assert_ne!(delivery, "deny", "干净代码不应签发 deny");
54+
println!("干净 delivery_decision: {delivery}");
55+
56+
let _ = std::fs::remove_dir_all(&root);
57+
}
58+
59+
/// 门禁决策应与 unresolved_conditions 一致。
60+
#[test]
61+
fn gate_decision_consistent_with_unresolved() {
62+
let root = ensure_clean_dir("codeguard-gate-consistent");
63+
std::fs::write(root.join("Test.java"), "class Test { int x = 1; }\n").unwrap();
64+
65+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
66+
.args(["check", "all", root.to_str().unwrap(), "--format=json"])
67+
.output()
68+
.expect("无法运行 codeguard check all");
69+
70+
let stdout = String::from_utf8_lossy(&output.stdout);
71+
let report: serde_json::Value = serde_json::from_str(&stdout).expect("JSON 解析失败");
72+
73+
let delivery = report["delivery_decision"].as_str().unwrap_or("");
74+
let unresolved = report["unresolved_conditions"]
75+
.as_array()
76+
.cloned()
77+
.unwrap_or_default();
78+
79+
// 有 unresolved 时不应为 allow
80+
if !unresolved.is_empty() {
81+
assert_ne!(delivery, "allow", "有 unresolved 时不应签发 allow");
82+
}
83+
84+
// 无 unresolved 时不应为 deny(除非有 confirmed findings)
85+
if unresolved.is_empty() {
86+
let confirmed: usize = report["syntax_candidates"]["observations"]
87+
.as_array()
88+
.map(|arr| {
89+
arr.iter()
90+
.filter(|obs| {
91+
let status = obs["status"].as_str().unwrap_or("");
92+
status == "confirmed" || status == "violation"
93+
})
94+
.count()
95+
})
96+
.unwrap_or(0);
97+
if confirmed == 0 {
98+
assert_ne!(delivery, "deny", "无发现且无 unresolved 时不应签发 deny");
99+
}
100+
}
101+
102+
println!("一致 delivery_decision: {delivery}, unresolved: {}", unresolved.len());
103+
104+
let _ = std::fs::remove_dir_all(&root);
105+
}
106+
107+
/// 门禁决策应与 exit_code 一致。
108+
#[test]
109+
fn gate_decision_consistent_with_exit_code() {
110+
let root = ensure_clean_dir("codeguard-gate-exit-code");
111+
std::fs::write(root.join("Test.java"), "class Test { int x = 1; }\n").unwrap();
112+
113+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
114+
.args(["check", "all", root.to_str().unwrap(), "--format=json"])
115+
.output()
116+
.expect("无法运行 codeguard check all");
117+
118+
let stdout = String::from_utf8_lossy(&output.stdout);
119+
let report: serde_json::Value = serde_json::from_str(&stdout).expect("JSON 解析失败");
120+
121+
let delivery = report["delivery_decision"].as_str().unwrap_or("");
122+
let exit_code = output.status.code().unwrap_or(0);
123+
124+
// allow 应对应 exit_code 0
125+
if delivery == "allow" {
126+
assert_eq!(exit_code, 0, "allow 应对应 exit_code 0");
127+
}
128+
129+
// deny 应对应非 0 exit_code
130+
if delivery == "deny" {
131+
assert_ne!(exit_code, 0, "deny 应对应非 0 exit_code");
132+
}
133+
134+
println!("exit_code: {exit_code}, delivery_decision: {delivery}");
135+
136+
let _ = std::fs::remove_dir_all(&root);
137+
}

0 commit comments

Comments
 (0)