use super::*;
use crate::render_dispatch::dispatch_render;
use lex_extension::wire::{Format, WireNode};
use lex_extension::{AnnotationBody, HandlerError, LabelCtx, LexHandler, RenderOut};
use lex_extension_host::Registry;
#[test]
fn empty_registry_yields_empty_plan() {
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
let plan = dispatch_render(&doc, ®istry, "html");
assert!(plan.nodes.is_empty());
}
#[test]
fn registered_label_with_html_render_hook_dispatches() {
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["html"])],
Box::new(EchoRender),
)
.unwrap();
let plan = dispatch_render(&doc, ®istry, "html");
assert_eq!(plan.nodes.len(), 1);
assert_eq!(plan.nodes[0].label, "acme.task");
assert_eq!(
plan.nodes[0].output.as_deref(),
Some(r#"<RENDERED label="acme.task"/>"#)
);
}
#[test]
fn label_without_html_in_render_hooks_is_skipped() {
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["markdown"])],
Box::new(EchoRender),
)
.unwrap();
let plan = dispatch_render(&doc, ®istry, "html");
assert!(plan.nodes.is_empty());
}
#[test]
fn alias_format_name_matches_canonical_schema_render_hook() {
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["markdown"])],
Box::new(EchoRender),
)
.unwrap();
let plan = dispatch_render(&doc, ®istry, "md");
assert_eq!(plan.nodes.len(), 1);
assert_eq!(plan.nodes[0].label, "acme.task");
}
#[test]
fn handler_error_yields_diagnostic_and_no_html() {
struct Boom;
impl LexHandler for Boom {
fn on_render(&self, _: &LabelCtx, _: Format) -> Result<Option<RenderOut>, HandlerError> {
Err(HandlerError::internal("render failed"))
}
}
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace("acme", vec![schema("acme.task", &["html"])], Box::new(Boom))
.unwrap();
let plan = dispatch_render(&doc, ®istry, "html");
assert_eq!(plan.nodes.len(), 1);
assert!(plan.nodes[0].output.is_none());
let diag = plan.nodes[0].diagnostic.as_deref().expect("diagnostic");
assert!(diag.contains("render failed"));
}
#[test]
fn wire_ast_output_for_string_target_falls_back_with_diagnostic() {
struct WireOut;
impl LexHandler for WireOut {
fn on_render(&self, _: &LabelCtx, _: Format) -> Result<Option<RenderOut>, HandlerError> {
Ok(Some(RenderOut::WireAst {
ast: WireNode::Document {
range: lex_extension::wire::Range::new(
lex_extension::wire::Position(0, 0),
lex_extension::wire::Position(0, 0),
),
origin: None,
children: vec![],
},
}))
}
}
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["html"])],
Box::new(WireOut),
)
.unwrap();
let plan = dispatch_render(&doc, ®istry, "html");
assert_eq!(plan.nodes.len(), 1);
assert!(plan.nodes[0].output.is_none());
assert!(plan.nodes[0]
.diagnostic
.as_deref()
.is_some_and(|d| d.contains("WireAst")));
}
#[test]
fn handler_sees_host_node_kind_in_label_ctx() {
use std::sync::Mutex;
struct CaptureKind {
seen: std::sync::Arc<Mutex<Vec<String>>>,
}
impl LexHandler for CaptureKind {
fn on_render(&self, ctx: &LabelCtx, _: Format) -> Result<Option<RenderOut>, HandlerError> {
self.seen.lock().unwrap().push(ctx.node.kind.clone());
Ok(None)
}
}
let seen = std::sync::Arc::new(Mutex::new(Vec::new()));
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["html"])],
Box::new(CaptureKind { seen: seen.clone() }),
)
.unwrap();
let doc = parse(":: acme.task ::\n");
let _ = dispatch_render(&doc, ®istry, "html");
let kinds = seen.lock().unwrap().clone();
assert_eq!(
kinds.as_slice(),
&["document"],
"top-level annotation must surface as host kind \"document\"",
);
}
#[test]
fn dispatch_render_fires_from_markdown_path() {
let doc = parse(":: acme.task ::\n");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["markdown"])],
Box::new(EchoRender),
)
.unwrap();
let plan = dispatch_render(&doc, ®istry, "markdown");
assert_eq!(plan.nodes.len(), 1, "markdown dispatch should fire");
assert_eq!(plan.nodes[0].label, "acme.task");
assert_eq!(
plan.nodes[0].output.as_deref(),
Some(r#"<RENDERED label="acme.task"/>"#),
"markdown handler output should surface in the plan"
);
}
#[test]
fn annotation_body_surfaces_lex_children_to_handler() {
use std::sync::Mutex;
struct CaptureBody {
seen: std::sync::Arc<Mutex<Option<AnnotationBody>>>,
}
impl LexHandler for CaptureBody {
fn on_render(&self, ctx: &LabelCtx, _: Format) -> Result<Option<RenderOut>, HandlerError> {
*self.seen.lock().unwrap() = Some(ctx.body.clone());
Ok(None)
}
}
let seen = std::sync::Arc::new(Mutex::new(None));
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["html"])],
Box::new(CaptureBody { seen: seen.clone() }),
)
.unwrap();
let doc = parse(":: acme.task ::\n inside the body.\n");
let _ = dispatch_render(&doc, ®istry, "html");
let body = seen.lock().unwrap().clone().expect("handler ran");
match body {
AnnotationBody::Lex { children } => {
assert!(
!children.is_empty(),
"lex body must carry annotation children"
);
}
other => panic!("expected AnnotationBody::Lex, got {other:?}"),
}
}