use serde::{Deserialize, Serialize};
fn de_verdict_label<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LabelVisitor;
impl<'de> serde::de::Visitor<'de> for LabelVisitor {
type Value = String;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a verdict label string or an envelope-v2 {type,…} object")
}
fn visit_str<E>(self, v: &str) -> Result<String, E> {
Ok(v.to_string())
}
fn visit_string<E>(self, v: String) -> Result<String, E> {
Ok(v)
}
fn visit_unit<E>(self) -> Result<String, E> {
Ok(String::new())
}
fn visit_none<E>(self) -> Result<String, E> {
Ok(String::new())
}
fn visit_map<A>(self, mut map: A) -> Result<String, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut label = String::new();
while let Some(key) = map.next_key::<String>()? {
if key == "type" {
label = map.next_value::<String>()?;
} else {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
Ok(label)
}
}
if deserializer.is_human_readable() {
deserializer.deserialize_any(LabelVisitor)
} else {
deserializer.deserialize_string(LabelVisitor)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct Verdict {
pub verdict_id: String,
#[serde(deserialize_with = "de_verdict_label")]
pub verdict: String,
pub source: String,
pub confidence: f64,
pub composite_score: u8,
pub reasoning: String,
pub similar_to: Vec<String>,
pub evidence_gaps: Vec<String>,
pub suggested_actions: Vec<String>,
pub data_freshness_at: Option<String>,
pub data_oldest_signal_at: Option<String>,
pub stale_since_at: Option<String>,
pub staleness_reason: Option<String>,
pub computed_at: Option<String>,
pub severity: Option<String>,
#[serde(alias = "policy_decision")]
pub decision: Option<String>,
pub previous_verdict: Option<PreviousVerdict>,
pub availability: AvailabilityBlock,
pub envelope_version: Option<u32>,
pub customer_state: Option<String>,
pub state: Option<String>,
pub source_state: Option<String>,
pub policy_version: Option<String>,
pub audit_record_id: Option<String>,
pub audit_record_hash: Option<String>,
pub attestation: Option<serde_json::Value>,
pub evidence: Option<serde_json::Value>,
pub composition: Option<serde_json::Value>,
pub rich_data: Option<RichData>,
pub top_findings: Option<TopFindings>,
pub reason_class: Option<String>,
pub attestation_status: Option<String>,
#[serde(default)]
pub freshness: Option<Freshness>,
#[serde(default)]
pub remediation: Option<Remediation>,
#[serde(default)]
pub axes: Option<Axes>,
#[serde(default)]
pub matched_rule_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
#[serde(default)]
pub struct Remediation {
pub target_version: String,
pub command_hint: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub clears: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub still_open: Vec<String>,
#[serde(default)]
pub compatibility: Compatibility,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub screened_at: Option<String>,
#[serde(default)]
pub peer_compatibility_checked: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_compatibility_note: Option<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Compatibility {
SameMajor,
MajorBump,
#[default]
Unknown,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
#[serde(default)]
pub struct Freshness {
pub cve_data_at: Option<String>,
pub behavioral_data_at: Option<String>,
pub policy_evaluated_at: Option<String>,
pub stalest_axis: Option<String>,
pub overall_as_of: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct TopFindings {
pub cve_count: Option<u32>,
pub on_kev: Option<bool>,
pub on_ransomware: Option<bool>,
pub findings: Vec<Finding>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct Finding {
pub cve_id: Option<String>,
pub fixed_version: Option<String>,
pub vulnerable_versions: Option<String>,
pub severity: Option<String>,
pub cvss_v3_score: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct RichData {
pub recommended_version: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct AvailabilityBlock {
pub degraded_stale: bool,
pub kev: Option<String>,
pub epss: Option<String>,
pub exploitation_fusion: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct Axes {
pub advisory: AxisAdvisory,
pub threat: AxisThreat,
pub availability: AxisAvailability,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct AxisAdvisory {
pub ran: bool,
pub result: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources_consulted: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consulted_at: Option<String>,
pub advisory_count: usize,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct AxisThreat {
pub ran: bool,
pub result: String,
pub finding_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub evaluated_at: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct AxisAvailability {
pub ran: bool,
pub result: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct PreviousVerdict {
pub verdict_id: String,
pub verdict: String,
pub computed_at: String,
pub diff: String,
}
impl Default for Verdict {
fn default() -> Self {
Self {
verdict_id: String::new(),
verdict: String::new(),
source: String::new(),
confidence: 0.0,
composite_score: 0,
reasoning: String::new(),
similar_to: Vec::new(),
evidence_gaps: Vec::new(),
suggested_actions: Vec::new(),
data_freshness_at: None,
data_oldest_signal_at: None,
stale_since_at: None,
staleness_reason: None,
computed_at: None,
severity: None,
decision: None,
previous_verdict: None,
availability: AvailabilityBlock::default(),
envelope_version: None,
customer_state: None,
state: None,
source_state: None,
policy_version: None,
audit_record_id: None,
audit_record_hash: None,
attestation: None,
evidence: None,
composition: None,
rich_data: None,
top_findings: None,
reason_class: None,
attestation_status: None,
freshness: None,
remediation: None,
axes: None,
matched_rule_id: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PackageRef {
pub ecosystem: String,
#[serde(rename = "package")]
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct PolicyPreviewRequest {
pub packages: Vec<PackageRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct PolicyDecision {
pub ecosystem: String,
pub package: String,
pub version: String,
pub decision: String,
#[serde(alias = "reasoning")]
pub reason: String,
pub verdict_id: Option<String>,
#[serde(alias = "matched_rule_id")]
pub policy_rule_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct PolicyPreviewResponse {
#[serde(alias = "results")]
pub decisions: Vec<PolicyDecision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ScanRequest {
pub packages: Vec<PackageRef>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ScanResult {
pub ecosystem: String,
pub package: String,
pub version: String,
pub verdict: Option<Verdict>,
pub error: Option<String>,
pub customer_state: Option<String>,
pub state: Option<String>,
pub source_state: Option<String>,
pub reason_class: Option<String>,
pub attestation_status: Option<String>,
pub freshness: Option<Freshness>,
pub remediation: Option<Remediation>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
#[serde(default)]
pub struct NotAssessedReason {
pub coordinate: String,
pub reason_class: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
#[serde(default)]
pub struct Coverage {
pub assessed: usize,
pub not_assessed: usize,
pub not_assessed_reasons: Vec<NotAssessedReason>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ScanResponse {
pub results: Vec<ScanResult>,
pub coverage: Option<Coverage>,
pub policy_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
fn deserialize_optional_audit_id_normalize_nil<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let raw: Option<String> = Option::deserialize(deserializer)?;
match raw {
None => Ok(None),
Some(s) if s.is_empty() => Ok(None),
Some(s) if s == "00000000-0000-0000-0000-000000000000" => Ok(None),
Some(s) => Ok(Some(s)),
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AuditEntry {
#[serde(deserialize_with = "deserialize_optional_audit_id_normalize_nil")]
pub request_id: Option<String>,
pub correlation_id: String,
pub ecosystem: String,
pub package_name: String,
pub package_version: String,
pub variant: String,
pub policy_decision: String,
#[serde(deserialize_with = "deserialize_optional_audit_id_normalize_nil")]
pub verdict_id: Option<String>,
pub verdict_source: String,
pub policy_rule_id_matched: String,
pub risk_acceptance_status: String,
pub reasoning: String,
pub gcs_hit: bool,
pub request_at: String,
pub verdict_at: String,
pub response_at: String,
pub app_version: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AuditWindow {
pub since: Option<String>,
pub until: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AuditResponse {
pub window: AuditWindow,
pub records: Vec<AuditEntry>,
pub record_count: usize,
pub per_route: std::collections::BTreeMap<String, usize>,
pub backend_status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_verdict() {
let json = r#"{
"verdict_id": "01JBYK000",
"verdict": "ALLOWED_NO_FINDINGS",
"source": "ALLOWED_NO_FINDINGS"
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
assert_eq!(v.verdict_id, "01JBYK000");
assert_eq!(v.verdict, "ALLOWED_NO_FINDINGS");
assert_eq!(v.confidence, 0.0);
assert!(v.similar_to.is_empty());
}
#[test]
fn cleanlib_780_v1_verdict_has_no_axes() {
let json = r#"{"verdict_id":"01A","verdict":"ALLOWED_NO_FINDINGS","source":"ALLOWED_NO_FINDINGS"}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
assert!(v.axes.is_none());
}
#[test]
fn cleanlib_780_parses_axes_consulted_empty_clean() {
let json = r#"{
"verdict_id":"01B","verdict":"ALLOWED_NO_FINDINGS","source":"ALLOWED_NO_FINDINGS",
"axes":{
"advisory":{"ran":true,"result":"clean","sources_consulted":["cve","nvd","ghsa"],"consulted_at":"2026-09-02T08:00:00Z","advisory_count":0},
"threat":{"ran":true,"result":"clean","finding_count":0,"evaluated_at":"2026-09-02T08:00:00Z"},
"availability":{"ran":false,"result":"not_run"}
}
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
let axes = v.axes.expect("axes present under v2");
assert_eq!(axes.advisory.result, "clean");
assert_eq!(axes.advisory.sources_consulted.as_deref(), Some(&["cve".to_string(),"nvd".to_string(),"ghsa".to_string()][..]));
assert_eq!(axes.advisory.consulted_at.as_deref(), Some("2026-09-02T08:00:00Z"));
assert_eq!(axes.advisory.advisory_count, 0);
assert_eq!(axes.threat.result, "clean");
assert!(!axes.availability.ran);
}
#[test]
fn cleanlib_780_parses_axes_never_consulted_no_sources() {
let json = r#"{
"verdict_id":"01C","verdict":"INSUFFICIENT_DATA","source":"INSUFFICIENT_DATA",
"axes":{
"advisory":{"ran":false,"result":"never_consulted","advisory_count":0},
"threat":{"ran":false,"result":"never_consulted","finding_count":0},
"availability":{"ran":false,"result":"not_run"}
}
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
let axes = v.axes.expect("axes present");
assert_eq!(axes.advisory.result, "never_consulted");
assert!(axes.advisory.sources_consulted.is_none(), "no fabricated source list");
assert!(axes.advisory.consulted_at.is_none());
}
#[test]
fn parses_envelope_v2_path_a_wire_with_verdict_object_and_label() {
let json = r#"{
"verdict_id": "01JBYK042",
"verdict": { "type": "VECTOR_VERDICT", "status": "WARN", "customer_state": "vulnerable" },
"verdict_label": "VECTOR_VERDICT",
"source": "CVE_FINDING",
"envelope_version": 2,
"customer_state": "vulnerable",
"state": "WARN",
"source_state": "CVE_FINDING"
}"#;
let v: Verdict = serde_json::from_str(json)
.expect("Path-A v2 wire must deserialize — CLEANLIB-468");
assert_eq!(v.verdict, "VECTOR_VERDICT");
assert_eq!(v.source, "CVE_FINDING");
assert_eq!(v.customer_state.as_deref(), Some("vulnerable"));
assert_eq!(v.source_state.as_deref(), Some("CVE_FINDING"));
}
#[test]
fn parses_full_verdict() {
let json = r#"{
"verdict_id": "01JBYK001",
"verdict": "VECTOR_VERDICT",
"source": "VECTOR_VERDICT",
"confidence": 0.98,
"composite_score": 92,
"reasoning": "Confirmed malware",
"similar_to": ["01JBYK999"],
"evidence_gaps": [],
"suggested_actions": ["DENY across customers"],
"data_freshness_at": "2026-05-21T10:00:00Z",
"computed_at": "2026-05-21T10:01:00Z"
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
assert_eq!(v.composite_score, 92);
assert_eq!(v.confidence, 0.98);
assert_eq!(v.similar_to.len(), 1);
assert_eq!(v.suggested_actions[0], "DENY across customers");
}
#[test]
fn cleanlib_613_parses_top_findings_fixed_version() {
let json = r#"{
"verdict_id": "01JBYK613",
"verdict": {"customer_state":"vulnerable","status":"WARN","type":"VECTOR_VERDICT"},
"source": "CVE_FINDING",
"rich_data": null,
"suggested_actions": ["Upgrade to 4.18.0+ to address CVE-2026-4800 (HIGH, CVSS 8.1)"],
"top_findings": {
"cve_count": 6,
"on_kev": false,
"on_ransomware": false,
"findings": [
{"cve_id":"CVE-2026-4800","fixed_version":"4.18.0","vulnerable_versions":">=4.0.0,<4.18.0","severity":"HIGH","cvss_v3_score":8.1},
{"cve_id":"CVE-2021-23337","fixed_version":"4.17.21","severity":"HIGH"}
]
}
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
assert_eq!(v.verdict, "VECTOR_VERDICT");
assert!(v.rich_data.is_none());
let tf = v.top_findings.expect("top_findings must parse");
assert_eq!(tf.cve_count, Some(6));
assert_eq!(tf.findings.len(), 2);
assert_eq!(tf.findings[0].fixed_version.as_deref(), Some("4.18.0"));
assert_eq!(tf.findings[0].cve_id.as_deref(), Some("CVE-2026-4800"));
assert_eq!(tf.findings[1].fixed_version.as_deref(), Some("4.17.21"));
assert!(tf.findings[1].cvss_v3_score.is_none());
}
#[test]
fn parses_policy_preview_response() {
let json = r#"{
"decisions": [
{"ecosystem":"npm","package":"left-pad","version":"1.3.0","decision":"ALLOW","reason":"ok"},
{"ecosystem":"npm","package":"event-stream","version":"3.3.6","decision":"DENY","reason":"malware","verdict_id":"01JBYK999"}
]
}"#;
let resp: PolicyPreviewResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.decisions.len(), 2);
assert_eq!(resp.decisions[0].decision, "ALLOW");
assert_eq!(resp.decisions[1].decision, "DENY");
assert_eq!(resp.decisions[1].verdict_id.as_deref(), Some("01JBYK999"));
}
#[test]
fn parses_audit_response_matches_app_wire_shape() {
let json = r#"{
"window": {"since": "2026-05-22T00:00:00Z", "until": "2026-05-23T00:00:00Z"},
"records": [{
"request_id": "01936b8f-3c4a-7a12-9c00-000000000001",
"correlation_id": "corr-1",
"customer_ip_hashed": "sha256:aaaa",
"ecosystem": "npm",
"package_name": "lodash",
"package_version": "4.17.21",
"variant": "default",
"user_agent": "cleanlib-cli/0.1.4",
"policy_decision": "ALLOW",
"verdict_id": "01936b8f-3c4a-7a12-9c00-0000000000aa",
"verdict_source": "ALLOWED_NO_FINDINGS",
"policy_rule_id_matched": "rule-42",
"risk_acceptance_status": "NONE",
"reasoning": "ok",
"gcs_hit": true,
"gcs_object_path": "gs://bucket/obj",
"bytes_served": 4096,
"request_at": "2026-05-22T10:00:00Z",
"ingest_at": null,
"gcs_at": null,
"verdict_at": "2026-05-22T10:00:01Z",
"policy_eval_at": "2026-05-22T10:00:02Z",
"response_at": "2026-05-22T10:00:03Z",
"app_version": "1.2.3"
}],
"record_count": 1,
"per_route": {"/v1/customer/verdicts/npm": 1},
"backend_status": "wired"
}"#;
let resp: AuditResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.records.len(), 1);
assert_eq!(resp.record_count, 1);
assert_eq!(resp.backend_status, "wired");
assert_eq!(resp.window.since.as_deref(), Some("2026-05-22T00:00:00Z"));
assert_eq!(resp.per_route.get("/v1/customer/verdicts/npm"), Some(&1));
let e = &resp.records[0];
assert_eq!(
e.request_id.as_deref(),
Some("01936b8f-3c4a-7a12-9c00-000000000001")
);
assert_eq!(e.correlation_id, "corr-1");
assert_eq!(e.ecosystem, "npm");
assert_eq!(e.package_name, "lodash");
assert_eq!(e.package_version, "4.17.21");
assert_eq!(e.variant, "default");
assert_eq!(e.policy_decision, "ALLOW");
assert_eq!(
e.verdict_id.as_deref(),
Some("01936b8f-3c4a-7a12-9c00-0000000000aa")
);
assert_eq!(e.verdict_source, "ALLOWED_NO_FINDINGS");
assert_eq!(e.policy_rule_id_matched, "rule-42");
assert_eq!(e.risk_acceptance_status, "NONE");
assert_eq!(e.reasoning, "ok");
assert!(e.gcs_hit);
assert_eq!(e.request_at, "2026-05-22T10:00:00Z");
assert_eq!(e.verdict_at, "2026-05-22T10:00:01Z");
assert_eq!(e.response_at, "2026-05-22T10:00:03Z");
assert_eq!(e.app_version, "1.2.3");
}
#[test]
fn empty_audit_response_carries_backend_status() {
let json = r#"{
"window": {"since": null, "until": null},
"records": [],
"record_count": 0,
"per_route": {},
"backend_status": "not_wired"
}"#;
let resp: AuditResponse = serde_json::from_str(json).unwrap();
assert!(resp.records.is_empty());
assert_eq!(resp.record_count, 0);
assert_eq!(resp.backend_status, "not_wired");
assert!(resp.window.since.is_none());
assert!(resp.window.until.is_none());
}
#[test]
fn policy_preview_request_omits_none_policy() {
let req = PolicyPreviewRequest {
packages: vec![PackageRef {
ecosystem: "npm".to_string(),
name: "lodash".to_string(),
version: "4.17.21".to_string(),
}],
policy: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("policy"));
assert!(json.contains("lodash"));
}
#[test]
fn policy_preview_request_emits_policy_when_some() {
let req = PolicyPreviewRequest {
packages: vec![],
policy: Some(serde_json::json!({"rules": []})),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"policy\""));
assert!(json.contains("\"rules\""));
}
#[test]
fn round_trips_via_json() {
let v = Verdict {
verdict_id: "01JBYK002".to_string(),
verdict: "INSUFFICIENT_DATA".to_string(),
source: "INSUFFICIENT_DATA".to_string(),
stale_since_at: Some("2026-04-21T00:00:00Z".to_string()),
staleness_reason: Some("upstream silent >30d".to_string()),
..Default::default()
};
let s = serde_json::to_string(&v).unwrap();
let parsed: Verdict = serde_json::from_str(&s).unwrap();
assert_eq!(parsed.verdict_id, "01JBYK002");
assert_eq!(parsed.stale_since_at.as_deref(), Some("2026-04-21T00:00:00Z"));
}
#[test]
fn cleanlib_601_preserves_rich_data_recommended_version() {
let json = r#"{
"verdict_id": "01JBYK601",
"verdict": "VECTOR_VERDICT",
"source": "VECTOR_VERDICT",
"rich_data": { "recommended_version": "4.17.21", "some_other_key": 7 }
}"#;
let v: Verdict = serde_json::from_str(json).unwrap();
assert_eq!(
v.rich_data
.as_ref()
.and_then(|r| r.recommended_version.as_deref()),
Some("4.17.21")
);
let v1: Verdict =
serde_json::from_str(r#"{"verdict_id":"x","verdict":"ALLOWED_NO_FINDINGS","source":"x"}"#)
.unwrap();
assert!(v1.rich_data.is_none());
}
#[test]
fn verdict_round_trips_severity_and_decision() {
let v = Verdict {
verdict_id: "01JM1S001".to_string(),
verdict: "VECTOR_VERDICT".to_string(),
source: "VECTOR_VERDICT".to_string(),
severity: Some("HIGH".to_string()),
decision: Some("DENY".to_string()),
..Default::default()
};
let s = serde_json::to_string(&v).unwrap();
let parsed: Verdict = serde_json::from_str(&s).unwrap();
assert_eq!(parsed.severity.as_deref(), Some("HIGH"));
assert_eq!(parsed.decision.as_deref(), Some("DENY"));
}
#[test]
fn verdict_tolerates_missing_severity_and_decision() {
let pre_m1_json = r#"{
"verdict_id": "01JM1S002",
"verdict": "ALLOWED_NO_FINDINGS",
"source": "ALLOWED_NO_FINDINGS",
"confidence": 0.95,
"composite_score": 8,
"reasoning": "",
"similar_to": [],
"evidence_gaps": [],
"suggested_actions": []
}"#;
let v: Verdict = serde_json::from_str(pre_m1_json).expect("pre-M1 shape must still parse");
assert!(v.severity.is_none());
assert!(v.decision.is_none());
}
#[test]
fn verdict_decision_canonical_values_match_js_py_go() {
for d in ["ALLOW", "WARN", "DENY", "RISK_ACCEPTANCE_REQUIRED"] {
let v = Verdict {
decision: Some(d.to_string()),
..Default::default()
};
let s = serde_json::to_string(&v).unwrap();
assert!(s.contains(&format!("\"decision\":\"{}\"", d)));
}
}
#[test]
fn verdict_severity_canonical_values_match_cleanlib_core() {
for sev in ["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"] {
let v = Verdict {
severity: Some(sev.to_string()),
..Default::default()
};
let s = serde_json::to_string(&v).unwrap();
assert!(s.contains(&format!("\"severity\":\"{}\"", sev)));
}
}
#[test]
fn cleanlib_743_audit_entry_tolerates_literal_null_request_id_and_verdict_id() {
let json = r#"{"request_id": null,
"verdict_id": null,
"correlation_id": "01M2J6R8J5QDQXGE43GW6MMPTZ",
"ecosystem": "npm",
"package_name": "exceljs",
"package_version": "4.4.0",
"policy_decision": "ALLOW",
"policy_rule_id_matched": "allow-default",
"reasoning": "ALLOW_BY_ABSENCE -- no live vulnerability findings",
"request_at": "2026-08-31T05:52:40.003567Z"}"#;
let e: AuditEntry = serde_json::from_str(json)
.expect("a literal null request_id/verdict_id must not crash deserialize");
assert!(e.request_id.is_none());
assert!(e.verdict_id.is_none());
assert_eq!(e.correlation_id, "01M2J6R8J5QDQXGE43GW6MMPTZ");
assert_eq!(e.package_name, "exceljs");
assert_eq!(e.policy_decision, "ALLOW");
}
#[test]
fn cleanlib_743_audit_response_survives_null_ids_on_every_record() {
let json = r#"{
"window": {"since": null, "until": null},
"records": [
{"request_id": null, "verdict_id": null, "ecosystem": "npm",
"package_name": "lodash", "package_version": "4.17.15",
"policy_decision": "DENY"},
{"request_id": null, "verdict_id": null, "ecosystem": "npm",
"package_name": "axios", "package_version": "1.6.0",
"policy_decision": "DENY"}
],
"record_count": 2,
"per_route": {},
"backend_status": "wired"
}"#;
let resp: AuditResponse =
serde_json::from_str(json).expect("500 real-shaped null-id records must parse");
assert_eq!(resp.records.len(), 2);
assert!(resp.records.iter().all(|r| r.request_id.is_none()));
assert!(resp.records.iter().all(|r| r.verdict_id.is_none()));
}
#[test]
fn cleanlib_794_audit_entry_nil_uuid_string_normalizes_to_none() {
let json = r#"{"request_id": "00000000-0000-0000-0000-000000000000",
"verdict_id": "00000000-0000-0000-0000-000000000000",
"ecosystem": "npm",
"package_name": "lodash",
"package_version": "4.17.21",
"policy_decision": "ALLOW"}"#;
let e: AuditEntry = serde_json::from_str(json).unwrap();
assert!(
e.request_id.is_none(),
"nil-UUID string must fold to None; pre-fix would surface the fake id verbatim"
);
assert!(e.verdict_id.is_none(), "same fold applies to verdict_id");
assert_eq!(e.package_name, "lodash");
assert_eq!(e.ecosystem, "npm");
assert_eq!(e.policy_decision, "ALLOW");
}
#[test]
fn cleanlib_794_audit_entry_real_ids_pass_through() {
let json = r#"{"request_id": "01936b8f-3c4a-7a12-9c00-000000000001",
"verdict_id": "01936b8f-3c4a-7a12-9c00-0000000000aa",
"ecosystem": "npm",
"package_name": "lodash"}"#;
let e: AuditEntry = serde_json::from_str(json).unwrap();
assert_eq!(
e.request_id.as_deref(),
Some("01936b8f-3c4a-7a12-9c00-000000000001")
);
assert_eq!(
e.verdict_id.as_deref(),
Some("01936b8f-3c4a-7a12-9c00-0000000000aa")
);
}
#[test]
fn cleanlib_794_audit_entry_missing_ids_are_none() {
let json = r#"{"ecosystem": "npm", "package_name": "lodash"}"#;
let e: AuditEntry = serde_json::from_str(json).unwrap();
assert!(e.request_id.is_none());
assert!(e.verdict_id.is_none());
}
#[test]
fn cleanlib_794_audit_entry_empty_string_id_normalizes_to_none() {
let json = r#"{"request_id": "", "verdict_id": "", "ecosystem": "npm", "package_name": "lodash"}"#;
let e: AuditEntry = serde_json::from_str(json).unwrap();
assert!(e.request_id.is_none());
assert!(e.verdict_id.is_none());
}
#[test]
fn cleanlib_755_745_remediation_round_trips_every_server_field() {
let json = r#"{
"target_version": "9.26.0",
"command_hint": "npm install eslint@9.26.0",
"clears": [],
"still_open": [],
"compatibility": "major_bump",
"screened_at": "2026-09-15T10:55:00Z",
"peer_compatibility_checked": false,
"peer_compatibility_note": "this target was selected from advisory data only..."
}"#;
let r: Remediation = serde_json::from_str(json).unwrap();
assert_eq!(r.target_version, "9.26.0");
assert_eq!(r.compatibility, Compatibility::MajorBump);
assert_eq!(r.screened_at.as_deref(), Some("2026-09-15T10:55:00Z"));
assert!(!r.peer_compatibility_checked);
assert!(r.peer_compatibility_note.is_some());
let out = serde_json::to_string(&r).unwrap();
let back: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(back["screened_at"], "2026-09-15T10:55:00Z", "755: screened_at must survive re-serialize: {out}");
assert_eq!(back["peer_compatibility_checked"], false, "745: peer_compatibility_checked must survive re-serialize: {out}");
assert!(back["peer_compatibility_note"].is_string(), "745: peer_compatibility_note must survive re-serialize: {out}");
assert_eq!(back["compatibility"], "major_bump");
}
#[test]
fn cleanlib_755_745_remediation_tolerates_absent_optional_fields() {
let json = r#"{"target_version": "1.0.0", "command_hint": "npm install x@1.0.0"}"#;
let r: Remediation = serde_json::from_str(json).unwrap();
assert_eq!(r.screened_at, None);
assert!(!r.peer_compatibility_checked);
assert_eq!(r.peer_compatibility_note, None);
assert_eq!(r.compatibility, Compatibility::Unknown, "absent compatibility defaults to Unknown, not SameMajor");
let out = serde_json::to_string(&r).unwrap();
assert!(!out.contains("screened_at"), "absent screened_at must be omitted, not null: {out}");
assert!(!out.contains("peer_compatibility_note"), "absent note must be omitted, not null: {out}");
}
#[test]
fn cleanlib_755_745_remediation_default_is_the_safe_unset_state() {
let r = Remediation {
target_version: "1.0.0".to_string(),
command_hint: "x".to_string(),
..Default::default()
};
assert_eq!(r.compatibility, Compatibility::Unknown);
assert!(!r.peer_compatibility_checked);
assert_eq!(r.screened_at, None);
assert_eq!(r.peer_compatibility_note, None);
}
}