From c66cb301ec3a9955e14ffb564bb1921d5399570a Mon Sep 17 00:00:00 2001 From: Takashi Yamashina Date: Sat, 15 Aug 2026 19:59:41 +0900 Subject: [PATCH 1/2] test: cover schema_audit.rs's MAX_AUDIT_NAMES truncation, close issue #627 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #627 untyped_concepts, undeclared_types, and unknown_labels each get a 101-name batch (one past the 100-name cap), confirming total reports the true count while names stays a name-ordered (BTreeSet) 100-entry prefix — zero-padded names keep lexicographic and numeric order identical, so the excluded entry is deterministically the highest. reserved_alias_conflicts stays out of scope, as already documented in this issue's plan: alias registration only works before a schema is installed, but the audit itself requires one, and PUT /schema's own migration-boundary guard (ADR 0009 §6.3 guard 2) already refuses to install over a pre-existing conflicting alias — two guards close off every public-API path to a non-empty section. --- tests/http_api/schema_audit.rs | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/http_api/schema_audit.rs b/tests/http_api/schema_audit.rs index 728053d0..ea9b2e80 100644 --- a/tests/http_api/schema_audit.rs +++ b/tests/http_api/schema_audit.rs @@ -488,3 +488,126 @@ fn validate_refuses_an_invalid_proposed_document() { assert_eq!(status, 400, "{body}"); assert_eq!(body["code"], "invalid_argument", "{body}"); } + +// --- MAX_AUDIT_NAMES (100): each name-list section truncates ----------- + +/// `untyped_concepts` past `MAX_AUDIT_NAMES` (100): `total` reports the +/// true count, `names` is a name-ordered (`BTreeSet`) prefix capped at +/// 100 — zero-padded objects keep lexicographic and numeric order +/// identical, so the excluded one is deterministically the highest. +#[test] +fn audit_untyped_concepts_truncates_past_max_audit_names() { + let server = Server::start("schema-audit-untyped-cap"); + server.ok("PUT", "/contexts/sake", Some(json!({"description": "d"}))); + let mut ops: Vec = vec![json!({ + "subject": "青嶺酒造", "label": "schema:type", "object": "Brewery", + "weight": 1.0, "source": "a.md", + })]; + ops.extend((0..101).map(|i| { + json!({ + "subject": "青嶺酒造", "label": "所在地", "object": format!("個人{i:03}"), + "weight": 1.0, "source": "a.md", + }) + })); + server.ok( + "POST", + "/contexts/sake/associations", + Some(serde_json::Value::Array(ops)), + ); + server.ok("PUT", "/contexts/sake/schema", Some(strict_document())); + + let audit = server.ok("POST", "/contexts/sake/schema/audit", None); + assert_eq!(audit["untyped_concepts"]["total"], json!(101), "{audit}"); + let names: Vec<&str> = audit["untyped_concepts"]["names"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert_eq!(names.len(), 100, "{audit}"); + assert!(names.contains(&"個人000"), "{audit:?}"); + assert!(names.contains(&"個人099"), "{audit:?}"); + assert!( + !names.contains(&"個人100"), + "the 101st name (highest-ordered) is the one past the cap: {names:?}" + ); +} + +/// `undeclared_types` past `MAX_AUDIT_NAMES` (100) — same truncation +/// contract, over asserted `schema:type` objects instead. +#[test] +fn audit_undeclared_types_truncates_past_max_audit_names() { + let server = Server::start("schema-audit-undeclared-cap"); + server.ok("PUT", "/contexts/sake", Some(json!({"description": "d"}))); + let ops: Vec = (0..101) + .map(|i| { + json!({ + "subject": format!("組織{i:03}"), "label": "schema:type", "object": format!("型{i:03}"), + "weight": 1.0, "source": "a.md", + }) + }) + .collect(); + server.ok( + "POST", + "/contexts/sake/associations", + Some(serde_json::Value::Array(ops)), + ); + server.ok("PUT", "/contexts/sake/schema", Some(strict_document())); + + let audit = server.ok("POST", "/contexts/sake/schema/audit", None); + assert_eq!(audit["undeclared_types"]["total"], json!(101), "{audit}"); + let names: Vec<&str> = audit["undeclared_types"]["names"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert_eq!(names.len(), 100, "{audit}"); + assert!(names.contains(&"型000"), "{audit:?}"); + assert!(names.contains(&"型099"), "{audit:?}"); + assert!( + !names.contains(&"型100"), + "the 101st name (highest-ordered) is the one past the cap: {names:?}" + ); +} + +/// `unknown_labels` past `MAX_AUDIT_NAMES` (100) — only populated under +/// `closed_labels`, same truncation contract as the other two sections. +#[test] +fn audit_unknown_labels_truncates_past_max_audit_names() { + let server = Server::start("schema-audit-unknown-labels-cap"); + server.ok("PUT", "/contexts/sake", Some(json!({"description": "d"}))); + let ops: Vec = (0..101) + .map(|i| { + json!({ + "subject": format!("s{i:03}"), "label": format!("未知{i:03}"), "object": format!("o{i:03}"), + "weight": 1.0, "source": "a.md", + }) + }) + .collect(); + server.ok( + "POST", + "/contexts/sake/associations", + Some(serde_json::Value::Array(ops)), + ); + let mut document = strict_document(); + document["mode"] = json!("warn"); + document["closed_labels"] = json!(true); + server.ok("PUT", "/contexts/sake/schema", Some(document)); + + let audit = server.ok("POST", "/contexts/sake/schema/audit", None); + assert_eq!(audit["unknown_labels"]["total"], json!(101), "{audit}"); + let names: Vec<&str> = audit["unknown_labels"]["names"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert_eq!(names.len(), 100, "{audit}"); + assert!(names.contains(&"未知000"), "{audit:?}"); + assert!(names.contains(&"未知099"), "{audit:?}"); + assert!( + !names.contains(&"未知100"), + "the 101st name (highest-ordered) is the one past the cap: {names:?}" + ); +} From fc134ea541a787fdae278e3591a518ed8a4f1c17 Mon Sep 17 00:00:00 2001 From: Takashi Yamashina Date: Sat, 15 Aug 2026 20:06:57 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20full-sequence=20assertions=20in=20audit=20cap=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare the complete name-ordered prefix (000 through 099) with assert_eq!, not just length plus spot-checked membership — the prior form would pass even for a wrongly-ordered or partially-wrong result. --- tests/http_api/schema_audit.rs | 42 +++++++++++++++------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/tests/http_api/schema_audit.rs b/tests/http_api/schema_audit.rs index ea9b2e80..e5e1de7c 100644 --- a/tests/http_api/schema_audit.rs +++ b/tests/http_api/schema_audit.rs @@ -518,18 +518,16 @@ fn audit_untyped_concepts_truncates_past_max_audit_names() { let audit = server.ok("POST", "/contexts/sake/schema/audit", None); assert_eq!(audit["untyped_concepts"]["total"], json!(101), "{audit}"); - let names: Vec<&str> = audit["untyped_concepts"]["names"] + let names: Vec = audit["untyped_concepts"]["names"] .as_array() .unwrap() .iter() - .map(|v| v.as_str().unwrap()) + .map(|v| v.as_str().unwrap().to_string()) .collect(); - assert_eq!(names.len(), 100, "{audit}"); - assert!(names.contains(&"個人000"), "{audit:?}"); - assert!(names.contains(&"個人099"), "{audit:?}"); - assert!( - !names.contains(&"個人100"), - "the 101st name (highest-ordered) is the one past the cap: {names:?}" + let expected: Vec = (0..100).map(|i| format!("個人{i:03}")).collect(); + assert_eq!( + names, expected, + "the full name-ordered prefix, 個人100 excluded: {audit}" ); } @@ -556,18 +554,16 @@ fn audit_undeclared_types_truncates_past_max_audit_names() { let audit = server.ok("POST", "/contexts/sake/schema/audit", None); assert_eq!(audit["undeclared_types"]["total"], json!(101), "{audit}"); - let names: Vec<&str> = audit["undeclared_types"]["names"] + let names: Vec = audit["undeclared_types"]["names"] .as_array() .unwrap() .iter() - .map(|v| v.as_str().unwrap()) + .map(|v| v.as_str().unwrap().to_string()) .collect(); - assert_eq!(names.len(), 100, "{audit}"); - assert!(names.contains(&"型000"), "{audit:?}"); - assert!(names.contains(&"型099"), "{audit:?}"); - assert!( - !names.contains(&"型100"), - "the 101st name (highest-ordered) is the one past the cap: {names:?}" + let expected: Vec = (0..100).map(|i| format!("型{i:03}")).collect(); + assert_eq!( + names, expected, + "the full name-ordered prefix, 型100 excluded: {audit}" ); } @@ -597,17 +593,15 @@ fn audit_unknown_labels_truncates_past_max_audit_names() { let audit = server.ok("POST", "/contexts/sake/schema/audit", None); assert_eq!(audit["unknown_labels"]["total"], json!(101), "{audit}"); - let names: Vec<&str> = audit["unknown_labels"]["names"] + let names: Vec = audit["unknown_labels"]["names"] .as_array() .unwrap() .iter() - .map(|v| v.as_str().unwrap()) + .map(|v| v.as_str().unwrap().to_string()) .collect(); - assert_eq!(names.len(), 100, "{audit}"); - assert!(names.contains(&"未知000"), "{audit:?}"); - assert!(names.contains(&"未知099"), "{audit:?}"); - assert!( - !names.contains(&"未知100"), - "the 101st name (highest-ordered) is the one past the cap: {names:?}" + let expected: Vec = (0..100).map(|i| format!("未知{i:03}")).collect(); + assert_eq!( + names, expected, + "the full name-ordered prefix, 未知100 excluded: {audit}" ); }