From 05154a58ed83522633b3bfe9d19b0f4de51f2199 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 17 Jul 2026 06:58:36 +0200 Subject: [PATCH] test(build): close target and identifier mutants Prove first-seen cross-kind generated-target ordering and deduplication, exact identifier/literal/canonical-symbol boundaries, invalid UTF-8 and component names, and resource insertion reuse/collisions; simplify CSS class normalization through the shared Rust identifier validator. req: codegen/005 req: diagnostics/004 req: diagnostics/005 req: surface/008 req: test/021 --- PLAN.md | 2 +- hemx-build/src/lib.rs | 170 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 150 insertions(+), 22 deletions(-) diff --git a/PLAN.md b/PLAN.md index bea6283..27bb338 100644 --- a/PLAN.md +++ b/PLAN.md @@ -21,7 +21,7 @@ - [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-core`, `hemx-js`, and the full `hemx-test` package cleanly; full package closure remains. - **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. - **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The current `hemx-build` frontier now also mutation-proves all 43 unknown/selector-attribute and unkeyed-loop diagnostic mutants, with exact `InvalidData` paths/guidance and invalid-scope fail-closed behavior. The remaining package frontier starts at generated-target deduplication, identifier/canonical-symbol helpers, and residual internal diagnostics before another full package gate. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. +- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The current `hemx-build` frontier now also mutation-proves ordered cross-kind generated-target deduplication and identifier/literal/canonical-symbol/resource-insertion boundaries, including invalid UTF-8 paths, exact collisions, missing values, and stable first-seen ordering. The remaining package frontier is concentrated in Rust-fact recursive collection and three `Resources::add_surface` propagation/default branches before another full package gate. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. - **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 ## 3. Elect and enforce the release license policy diff --git a/hemx-build/src/lib.rs b/hemx-build/src/lib.rs index 63024b3..453f59b 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -1492,16 +1492,7 @@ fn is_class_continue(byte: u8) -> bool { } fn class_ident(token: &str) -> Option { - let mut ident = String::with_capacity(token.len()); - for ch in token.chars() { - match ch { - '-' => ident.push('_'), - '_' => ident.push('_'), - ch if ch.is_ascii_alphanumeric() => ident.push(ch), - _ => return None, - } - } - rust_ident(&ident) + rust_ident(&token.replace('-', "_")) } fn is_handle_param_attr(attr: &SurfaceAttribute) -> bool { @@ -2373,6 +2364,114 @@ mod tests { // test req: diagnostics/004 req: diagnostics/006 req: surface/008 } + #[test] + fn identifier_and_literal_helpers_fail_closed_at_boundaries() { + assert_eq!(rust_ident("alpha_9"), Some("alpha_9".into())); + for invalid in ["", "9alpha", "alpha-beta", "alpha beta", "alphaé"] { + assert_eq!( + rust_ident(invalid), + None, + "accepted Rust identifier {invalid:?}" + ); + } + assert_eq!(class_ident("alpha-beta_9"), Some("alpha_beta_9".into())); + for invalid in ["", "9alpha", "alphaé", "alpha.beta", "alpha beta"] { + assert_eq!(class_ident(invalid), None, "accepted CSS class {invalid:?}"); + } + + assert_eq!(rust_str("a\n\"b\\c"), "\"a\\n\\\"b\\\\c\""); + assert_eq!(rust_str_opt(None), "None"); + assert_eq!(rust_str_opt(Some("a\n\"b\\c")), "Some(\"a\\n\\\"b\\\\c\")"); + assert_eq!( + canonical_symbol( + Path::new("templates"), + Path::new("templates/nested/panel.heml"), + "save", + ), + "nested/panel.heml::save" + ); + + let root = Path::new("templates"); + assert_eq!( + component_ident(root, Path::new("templates/nested/profile_card.heml")).unwrap(), + "profile_card" + ); + assert_eq!( + component_ident(root, Path::new("templates/9bad.heml")) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + assert_eq!( + component_ident(root, Path::new("outside.heml")).unwrap(), + "outside" + ); + + let mut resources = BTreeMap::new(); + let first = insert_resource( + &mut resources, + "slot", + "panel.heml::summary".into(), + "summary".into(), + "panel".into(), + ) + .unwrap(); + first.keyed = true; + assert!( + insert_resource( + &mut resources, + "slot", + "panel.heml::summary".into(), + "summary".into(), + "panel".into(), + ) + .unwrap() + .keyed + ); + assert_eq!( + insert_resource( + &mut resources, + "slot", + "other.heml::summary".into(), + "summary".into(), + "other".into(), + ) + .unwrap_err() + .to_string(), + "duplicate generated identifier `summary` for `panel.heml::summary` and `other.heml::summary`" + ); + + let invalid_surface = surface_for_heml_source( + Path::new("123.heml"), + r#"
"#.to_owned(), + ) + .unwrap(); + let mut extracted = Resources::default(); + assert_eq!( + extracted + .add_surface( + Path::new("templates"), + Path::new("templates/123.heml"), + &invalid_surface, + ) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let invalid = + PathBuf::from("templates").join(std::ffi::OsStr::from_bytes(b"\xff.heml")); + assert_eq!( + component_ident(root, &invalid).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); + } + // test req: codegen/005 req: diagnostics/004 + } + #[test] fn generated_contract_fingerprint_and_client_bootstrap_are_deterministic() { let resource = |symbol: &str, component: &str, id| Resource { @@ -3463,20 +3562,49 @@ fn main() {{ // req: diagnostics/004 req: diagnostics/005 let targets = generated_targets_for_heml_source( "inline.heml", - r#"
"#, + r#" +
+
+
+
+
+ + + +
+ "#, ) .expect("generated targets"); - assert_eq!(targets.len(), 3); - assert!(targets - .iter() - .any(|target| target.kind == "slot" && target.name == "summary")); - assert!(targets - .iter() - .any(|target| target.kind == "form" && target.name == "save")); - assert!(targets - .iter() - .any(|target| target.kind == "handle" && target.name == "submit")); + assert_eq!( + targets, + vec![ + GeneratedTarget { + kind: "slot".into(), + name: "summary".into(), + }, + GeneratedTarget { + kind: "slot".into(), + name: "after-root".into(), + }, + GeneratedTarget { + kind: "form".into(), + name: "summary".into(), + }, + GeneratedTarget { + kind: "slot".into(), + name: "after-missing".into(), + }, + GeneratedTarget { + kind: "handle".into(), + name: "submit".into(), + }, + GeneratedTarget { + kind: "slot".into(), + name: "later".into(), + }, + ] + ); } #[test]