Files
hemx/slhx-test/tests/examples_contract.rs
T
slhx agent a1e3085437 docs(kanban): describe delegated runtime attachment
Replace the north-star walkthrough's listener snippet with prose and guard example docs against teaching addEventListener glue.

req: examples/003

req: runtime/001
2026-06-02 01:56:10 +02:00

213 lines
6.6 KiB
Rust

use std::path::{Path, PathBuf};
#[test]
fn canonical_examples_do_not_author_browser_javascript() {
// req: examples/005
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let examples = root.join("examples");
let mut failures = Vec::new();
scan_examples(&examples, &mut |path, text| {
if path.components().any(|part| part.as_os_str() == "tests") {
return;
}
for (line_no, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.contains("<script") && !allowed_example_script(path, trimmed) {
failures.push(format!(
"{}:{}: only the slhx runtime or explicit opaque-island scripts are allowed",
path.display(),
line_no + 1
));
}
if contains_inline_event_handler(trimmed) {
failures.push(format!(
"{}:{}: inline on*= handler is not allowed",
path.display(),
line_no + 1
));
}
if trimmed.to_ascii_lowercase().contains("javascript:") {
failures.push(format!(
"{}:{}: javascript: URL is not allowed",
path.display(),
line_no + 1
));
}
}
});
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn canonical_examples_do_not_author_low_level_resource_plumbing() {
// req: dx/002 req: ceremony/002 req: ceremony/004 req: examples/003
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let examples = root.join("examples");
let forbidden = [
"ResourceId::new",
".id().id",
"include!(concat!",
"OUT_DIR",
"global_exports(",
"EffectWriter",
"RuntimeOpcode",
"data-hid",
"data-sid",
"data-fid",
"data-aid",
"data-handle-id",
"data-slot-id",
"data-form-id",
"data-atom-id",
"::lower(include_str!",
"lower_html(",
"render_html(",
"HandlerRegistry",
"InteractionForm",
"register_handle(",
"RenderSlotExt",
".render(&",
"name=\"__h\"",
"name='__h'",
];
let mut failures = Vec::new();
scan_examples(&examples, &mut |path, text| {
if path.components().any(|part| part.as_os_str() == "tests") {
return;
}
let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
return;
};
if !matches!(ext, "rs" | "heml" | "html") {
return;
}
let runtime_source = if ext == "rs" {
text.split("#[cfg(test)]").next().unwrap_or(text)
} else {
text
};
for (line_no, line) in runtime_source.lines().enumerate() {
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
failures.push(format!(
"{}:{}: example runtime code must use generated resources, not `{token}`",
path.display(),
line_no + 1
));
}
}
});
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn canonical_example_docs_do_not_teach_low_level_plumbing() {
// req: dx/002 req: dx/003 req: examples/003
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let examples = root.join("examples");
let forbidden = [
"generated handle ids",
"numeric handle ids",
"manual registry",
"HandlerRegistry",
"InteractionForm",
"register_handle(",
"lower_html(",
"render_html(",
"SafeHtml::trusted",
".render(&",
"addEventListener(",
];
let mut failures = Vec::new();
scan_examples(&examples, &mut |path, text| {
if path.extension().and_then(|ext| ext.to_str()) != Some("md") {
return;
}
for (line_no, line) in text.lines().enumerate() {
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
failures.push(format!(
"{}:{}: example docs must teach generated ergonomic APIs, not `{token}`",
path.display(),
line_no + 1
));
}
}
});
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn browser_e2e_does_not_shortcut_product_interactions() {
// req: examples/005
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let browser_e2e = root.join("examples/techdemo/tests/browser_e2e.rs");
let source = std::fs::read_to_string(browser_e2e).unwrap();
assert!(
!source.contains("fetch(\"/\""),
"browser E2E must drive UI, not post directly"
);
assert!(
!source.contains("applyBatch(buffer"),
"browser E2E must not apply wire batches manually"
);
}
fn scan_examples(dir: &Path, visit: &mut impl FnMut(&Path, &str)) {
for entry in std::fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
scan_examples(&path, visit);
continue;
}
if !is_example_source(&path) {
continue;
}
let text = std::fs::read_to_string(&path).unwrap();
visit(&path, &text);
}
}
fn is_example_source(path: &PathBuf) -> bool {
matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("rs" | "heml" | "html" | "md")
)
}
fn allowed_example_script(path: &Path, line: &str) -> bool {
// req: examples/005
line.contains(r#"<script src="/slhx.js" defer></script>"#)
|| (path.ends_with("examples/techdemo/templates/app_shell.heml")
&& line.contains(r#"<script src="/island.js" defer></script>"#))
}
fn contains_inline_event_handler(line: &str) -> bool {
let bytes = line.as_bytes();
let mut i = 0;
while i + 3 < bytes.len() {
let boundary = bytes[i].is_ascii_whitespace() || bytes[i] == b'<';
if boundary
&& bytes[i + 1] == b'o'
&& bytes[i + 2] == b'n'
&& bytes[i + 3].is_ascii_lowercase()
{
let mut j = i + 4;
while j < bytes.len() && bytes[j].is_ascii_lowercase() {
j += 1;
}
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'=' {
return true;
}
}
i += 1;
}
false
}