From 72bce73e52542d9d6ab929e3b99e729033197b1a Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 17 Jul 2026 03:57:29 +0200 Subject: [PATCH] test(build): close resource extraction mutants Prove fail-closed slot and handler-parameter diagnostics through AppBuilder, consolidate resource insertion around the stored entry, and lower validated runtime events without redundant fallible identifier conversion. req: build/004 req: diagnostics/004 req: surface/008 req: test/021 --- PLAN.md | 2 +- hemx-build/src/lib.rs | 114 +++++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/PLAN.md b/PLAN.md index 56087f5..8972691 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 adversarially proves static convention diagnostics; public file/source diagnostics and context facts; generated-artifact refresh; client-handler rejection; and exact class/form extraction, field metadata, invalid-name, and collision behavior through `AppBuilder::run`; remaining event/slot/atom/handle extraction and internal diagnostic survivors remain. 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 adversarially proves all class/form/event/slot/atom/handle extraction, field/parameter metadata, invalid-name, collision, deduplication, and keyed-upgrade behavior through `AppBuilder::run`; resource extraction is mutation-clean, leaving only internal diagnostic/code-generation survivors before the 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 b82c93e..c43c252 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -335,7 +335,7 @@ impl Resources { if let Some(on_attr) = static_attr(&node.attrs, "data-hemx-on") { for event in event_tokens(&on_attr) { let canonical = canonical_symbol(root, path, event); - self.insert_event(canonical, event.to_owned(), component.clone())?; + self.insert_event(canonical, event.to_owned(), component.clone()); } } @@ -417,15 +417,17 @@ impl Resources { component: String, keyed: bool, ) -> io::Result<()> { - insert_resource(&mut self.slots, "slot", symbol, name, component, keyed) + let slot = insert_resource(&mut self.slots, "slot", symbol, name, component)?; + slot.keyed |= keyed; + Ok(()) } fn insert_handle(&mut self, symbol: String, name: String, component: String) -> io::Result<()> { - insert_resource(&mut self.handles, "handle", symbol, name, component, false) + insert_resource(&mut self.handles, "handle", symbol, name, component).map(|_| ()) } fn insert_atom(&mut self, symbol: String, name: String, component: String) -> io::Result<()> { - insert_resource(&mut self.atoms, "atom", symbol, name, component, false) + insert_resource(&mut self.atoms, "atom", symbol, name, component).map(|_| ()) } fn insert_class(&mut self, symbol: String, token: String, component: String) -> io::Result<()> { @@ -482,32 +484,13 @@ impl Resources { Ok(()) } - fn insert_event(&mut self, symbol: String, name: String, component: String) -> io::Result<()> { - let ident = event_ident(&name).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - format!( - "invalid hemx event `{name}`; expected an ASCII event name usable from Rust" - ), - ) - })?; - let event = EventToken { + fn insert_event(&mut self, symbol: String, name: String, component: String) { + self.events.entry(symbol.clone()).or_insert(EventToken { symbol, - ident, + ident: name.clone(), component, name, - }; - match self.events.get(&event.symbol) { - Some(existing) if existing.name != event.name => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("conflicting event name for `{}`", existing.symbol), - )), - Some(_) => Ok(()), - None => { - self.events.insert(event.symbol.clone(), event); - Ok(()) - } - } + }); } fn insert_form( @@ -1323,32 +1306,29 @@ fn __hemx_attr(tag: &str, attr: &str) -> Option<::std::string::String> { } } -fn insert_resource( - map: &mut BTreeMap, +fn insert_resource<'a>( + map: &'a mut BTreeMap, kind: &str, symbol: String, name: String, component: String, - keyed: bool, -) -> io::Result<()> { - let mut resource = make_resource(kind, symbol, name, component)?; - resource.keyed = keyed; - match map.get_mut(&resource.ident) { - Some(existing) if existing.symbol != resource.symbol => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "duplicate generated identifier `{}` for `{}` and `{}`", - resource.ident, existing.symbol, resource.symbol - ), - )), - Some(existing) => { - existing.keyed |= keyed; - Ok(()) - } - None => { - map.insert(resource.ident.clone(), resource); - Ok(()) +) -> io::Result<&'a mut Resource> { + let resource = make_resource(kind, symbol, name, component)?; + match map.entry(resource.ident.clone()) { + std::collections::btree_map::Entry::Occupied(entry) + if entry.get().symbol != resource.symbol => + { + let existing = entry.get(); + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "duplicate generated identifier `{}` for `{}` and `{}`", + resource.ident, existing.symbol, resource.symbol + ), + )) } + std::collections::btree_map::Entry::Occupied(entry) => Ok(entry.into_mut()), + std::collections::btree_map::Entry::Vacant(entry) => Ok(entry.insert(resource)), } } @@ -1570,10 +1550,6 @@ fn data_param_ident(name: &str) -> Option { rust_ident(&data_name.replace('-', "_")) } -fn event_ident(name: &str) -> Option { - rust_ident(&name.replace(['-', ':'], "_")) -} - fn can_host_keyed_collection(tag: &str) -> bool { matches!( tag, @@ -2564,6 +2540,40 @@ mod tests { invalid_handler.display() ) ); + + std::fs::remove_file(invalid_handler).unwrap(); + std::fs::write( + templates.join("invalid_param.heml"), + r#""#, + ) + .unwrap(); + let error = app() + .template_dir(&templates) + .out_dir(&invalid_out) + .run() + .unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert_eq!( + error.to_string(), + "invalid handler param attribute `data-123`; expected data-* name usable from Rust" + ); + + std::fs::remove_file(templates.join("invalid_param.heml")).unwrap(); + std::fs::write( + templates.join("invalid_slot.heml"), + r#"
Invalid
"#, + ) + .unwrap(); + let error = app() + .template_dir(&templates) + .out_dir(&invalid_out) + .run() + .unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert_eq!( + error.to_string(), + "invalid hemx slot name `123`; expected a Rust identifier" + ); let _ = std::fs::remove_dir_all(root); // test req: build/004 req: client_local/011 req: diagnostics/004 }