test(derive): cover handler compile failures

Add a fixture-backed compile-fail test for generated slhx handler contracts: unknown handles and invalid handler shape fail during cargo check with actionable diagnostics.

req: derive_handler/001

req: test/003
This commit is contained in:
slhx agent
2026-05-25 21:14:16 +02:00
parent 6f93c7d935
commit b7055f225a
+106
View File
@@ -0,0 +1,106 @@
use std::path::{Path, PathBuf};
use std::process::Command;
#[test]
fn handler_macro_reports_unknown_handle_and_bad_shape() {
// req: derive_handler/001 req: test/003
let fixture = Fixture::new("slhx-derive-handler-fail");
fixture.write(
"Cargo.toml",
&format!(
r#"[package]
name = "slhx-derive-handler-fail"
version = "0.0.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[dependencies]
slhx = {{ path = {:?} }}
"#,
repo_path("slhx")
),
);
fixture.write(
"build.rs",
r#"fn main() {
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
std::fs::write(
out.join("slhx.syms"),
"slhx-syms-v1\nhandle\ttemplates/app.heml::known\tknown\t1\n",
)
.unwrap();
}
"#,
);
fixture.write(
"src/lib.rs",
r#"#[slhx::handler]
fn missing() -> impl slhx::IntoEffect {
slhx::EffectBatch::default()
}
#[slhx::handler]
fn known() {}
"#,
);
let output = Command::new("cargo")
.arg("check")
.arg("--quiet")
.arg("--manifest-path")
.arg(fixture.path.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", fixture.path.join("target"))
.output()
.expect("cargo check fixture runs");
assert!(!output.status.success(), "fixture unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unknown slhx handle `missing`; add `data-slhx-handle=\"missing\"`"),
"missing unknown-handle diagnostic in stderr:\n{stderr}"
);
assert!(
stderr.contains("slhx handler `known` must accept a form/context parameter or return a value implementing IntoEffect"),
"missing handler-shape diagnostic in stderr:\n{stderr}"
);
}
fn repo_path(crate_name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crate has workspace parent")
.join(crate_name)
}
struct Fixture {
path: PathBuf,
}
impl Fixture {
fn new(name: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"{name}-{}-{}",
std::process::id(),
std::thread::current().name().unwrap_or("test")
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(path.join("src")).unwrap();
Self { path }
}
fn write(&self, relative: &str, contents: &str) {
let path = self.path.join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, contents).unwrap();
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}