use super::*;
use crate::json::{OValue, to_compact};
use crate::supplychain::{member, views};
use airs_test_harness::fixtures::supplychain::{self as fx, Wire, obj, w};
use scm::airs::agentguard::models::{AgentGuardScan, AgentGuardScanList, Nullable};
use scm::validation::Mode;
use serde_json::json;
fn typed_projection(scan: &AgentGuardScan) -> OValue {
let text = |value: &str| OValue::String(value.to_owned());
let nullable = |value: &Nullable<String>| match value {
Nullable::Null => OValue::Null,
Nullable::Value(value) => text(value),
};
let number = |value: &FiniteNumber| OValue::Number(value.as_number().clone());
let (vulnerabilities, chains) = match &scan.summary {
Nullable::Null => (OValue::Null, OValue::Null),
Nullable::Value(summary) => (
number(&summary.vulnerability_count),
number(&summary.attack_chain_count),
),
};
OValue::Object(vec![
member("uuid", text(&scan.uuid)),
member("artifact_type", text(&scan.artifact_type)),
member("status", text(&scan.status)),
member("eval_outcome", nullable(&scan.eval_outcome)),
member("created_at", text(&scan.created_at)),
member("vulnerability_count", vulnerabilities),
member("attack_chain_count", chains),
member("is_batch", OValue::Bool(scan.is_batch)),
member("parent_uuid", nullable(&scan.parent_uuid)),
])
}
fn body(rows: &[Wire], total: u64) -> Value {
serde_json::from_str(&fx::scan_list_body(rows, total)).unwrap()
}
fn compact(scans: &[Scan]) -> Vec<String> {
scans
.iter()
.map(|scan| to_compact(&views::scan(scan)).unwrap())
.collect()
}
#[test]
fn accepted_answers_project_as_the_typed_model_did() {
let mut numbers = fx::scans();
numbers.truncate(3);
numbers[0].set(
"summary",
obj(vec![
("vulnerability_count", Wire::Leaf(json!(2.0))),
(
"attack_chain_count",
Wire::Leaf(json!(9_007_199_254_740_991_u64)),
),
]),
);
let exponent: Value = serde_json::from_str("1e0").unwrap();
numbers[1].set(
"summary",
obj(vec![
("vulnerability_count", Wire::Leaf(exponent)),
("attack_chain_count", w(json!(0))),
]),
);
numbers[2].set("artifact_type", w(json!("ü 日本\u{1b}[31m\u{85}")));
for (rows, total) in [
(fx::scans(), fx::SCAN_COUNT),
(fx::recent_scans(), fx::SCAN_COUNT),
(numbers, 3),
(Vec::new(), 0),
] {
let answer = body(&rows, total);
let typed = AgentGuardScanList::parse(answer.clone(), Mode::ResponseWithoutDefaults)
.expect("the SDK accepts the fixture");
let page = project(&answer).unwrap();
assert_eq!(Some(page.total), typed.pagination.total_items.as_u64());
let expected: Vec<String> = typed
.scans
.iter()
.map(|scan| to_compact(&typed_projection(scan)).unwrap())
.collect();
assert_eq!(compact(&page.scans), expected);
}
}
#[test]
fn nulls_read_as_null() {
let answer = body(&fx::null_scans(), 3);
assert!(AgentGuardScanList::parse(answer.clone(), Mode::ResponseWithoutDefaults).is_err());
let page = project(&answer).unwrap();
assert_eq!(page.total, 3);
assert_eq!(
compact(&page.scans),
[
r#"{"uuid":"11111111-1111-4111-8111-000000000113","artifact_type":"SKILL","status":"COMPLETED","eval_outcome":"BLOCKED","created_at":"2026-10-19T03:04:05.006Z","vulnerability_count":1,"attack_chain_count":1,"is_batch":false,"parent_uuid":null}"#,
r#"{"uuid":"11111111-1111-4111-8111-000000000114","artifact_type":null,"status":null,"eval_outcome":null,"created_at":null,"vulnerability_count":null,"attack_chain_count":null,"is_batch":null,"parent_uuid":null}"#,
r#"{"uuid":"11111111-1111-4111-8111-000000000115","artifact_type":"AGENT","status":"COMPLETED","eval_outcome":"ALLOWED","created_at":"2026-10-19T03:04:05.006Z","vulnerability_count":null,"attack_chain_count":3,"is_batch":false,"parent_uuid":null}"#,
]
);
assert_eq!(page.scans[1].children, None);
assert_eq!(page.scans[0].children, Some(0));
let bare = scan(&json!({"uuid": "u"})).unwrap();
assert_eq!(
to_compact(&views::scan(&bare)).unwrap(),
r#"{"uuid":"u","artifact_type":null,"status":null,"eval_outcome":null,"created_at":null,"vulnerability_count":null,"attack_chain_count":null,"is_batch":null,"parent_uuid":null}"#
);
}
#[test]
fn nothing_sensitive_is_kept() {
for rows in [fx::scans(), fx::null_scans()] {
let page = project(&body(&rows, 1)).unwrap();
let kept = format!("{page:?}");
assert!(!kept.contains(fx::MARKER), "{kept}");
}
}
#[test]
fn refusals() {
let good = fx::scans()[0].clone();
let with = |key: &str, value: Value| {
let mut row = good.clone();
row.set(key, Wire::Leaf(value));
body(&[row], 1)
};
for (answer, refused) in [
(json!([]), Refused::Page),
(json!({"scans": []}), Refused::Page),
(json!({"pagination": null, "scans": []}), Refused::Page),
(
json!({"pagination": {"total_items": null}, "scans": []}),
Refused::Page,
),
(
json!({"pagination": {"total_items": -1}, "scans": []}),
Refused::Page,
),
(
json!({"pagination": {"total_items": 1.5}, "scans": []}),
Refused::Page,
),
(
json!({"pagination": {"total_items": 9_007_199_254_740_992_u64}, "scans": []}),
Refused::Page,
),
(
json!({"pagination": {"total_items": 1}, "scans": null}),
Refused::Page,
),
(
json!({"pagination": {"total_items": 1}, "scans": [null]}),
Refused::Identity,
),
(with("uuid", Value::Null), Refused::Identity),
(with("uuid", json!(101)), Refused::Identity),
(with("status", json!(1)), Refused::Member),
(with("created_at", json!({})), Refused::Member),
(with("is_batch", json!("false")), Refused::Member),
(with("child_scan_uuids", json!({})), Refused::Member),
(with("summary", json!([])), Refused::Member),
(
with(
"summary",
json!({"vulnerability_count": "2", "attack_chain_count": 0}),
),
Refused::Member,
),
(
with(
"summary",
json!({"vulnerability_count": -1, "attack_chain_count": 0}),
),
Refused::Member,
),
] {
assert_eq!(project(&answer), Err(refused), "{answer}");
}
let mut no_uuid = good.clone();
no_uuid.remove("uuid");
assert_eq!(project(&body(&[no_uuid], 1)), Err(Refused::Identity));
for (key, value) in [
("fingerprint", json!(7)),
("name", json!(["x"])),
("device_metadata", json!("x")),
("uuid", json!("")),
] {
assert!(project(&with(key, value)).is_ok(), "{key}");
}
}
#[test]
fn the_validation_bounds_are_the_typed_calls() {
let limits = crate::clients::supplychain_validation_limits().unwrap();
for (depth, accepted) in [(64, true), (65, false), (100, false)] {
let answer = body(&fx::deep_scans(depth), fx::SCAN_COUNT);
assert_eq!(fx::json_depth(&answer), depth);
let typed = scm::validation::parse_with_limits(
"AgentGuardScanListSchema",
answer.clone(),
Mode::ResponseWithoutDefaults,
&limits,
);
match &typed {
Ok(_) => assert!(accepted, "{depth}"),
Err(error) => {
assert!(!accepted, "{depth}: {error:?}");
assert_eq!(error.kind(), scm::ErrorKind::Limit);
}
}
assert_eq!(within_bounds(&answer, &limits), accepted, "{depth}");
}
for rows in [
fx::scans(),
fx::recent_scans(),
fx::null_scans(),
Vec::new(),
] {
assert!(within_bounds(&body(&rows, 1), &limits));
}
let answer = json!({"pagination": {"total_items": 1}, "scans": [{"uuid": "abc"}]});
let mut small = limits.clone();
small.max_nodes = 6;
assert!(within_bounds(&answer, &small));
small.max_nodes = 5;
assert!(!within_bounds(&answer, &small));
let mut small = limits.clone();
small.max_input_bytes = 10 + 11 + 5 + 4 + 3;
assert!(within_bounds(&answer, &small));
small.max_input_bytes -= 1;
assert!(!within_bounds(&answer, &small));
}