use camel_builder::{RouteBuilder, StepAccumulator};
use camel_component_api::template_reload::TemplateReloadRegistry;
use camel_component_direct::DirectComponent;
use camel_core::CamelContext;
use camel_template::TemplateComponent;
mod common;
const PAGE_V1: &str = "{% autoescape \"html\" %}<h1>v1 {{headers.title}}</h1>{% endautoescape %}";
const PAGE_V2: &str = "{% autoescape \"html\" %}<h1>v2 {{headers.title}}</h1>{% endautoescape %}";
const PAGE_INVALID: &str = "{% if broken %}no endif, no autoescape wrapper";
const A_V1: &str = "{% autoescape \"none\" %}A1:{{headers.title}}{% endautoescape %}";
const A_V2: &str = "{% autoescape \"none\" %}A2:{{headers.title}}{% endautoescape %}";
const A_V3: &str = "{% autoescape \"none\" %}A3:{{headers.title}}{% endautoescape %}";
const B_V1: &str = "{% autoescape \"none\" %}B1:{{body}}{% endautoescape %}";
const B_V2: &str = "{% autoescape \"none\" %}B2:{{body}}{% endautoescape %}";
async fn start_two_template_route(
a_uri: &str,
b_uri: &str,
route_id: &str,
) -> Result<CamelContext, camel_api::CamelError> {
let mut ctx = CamelContext::builder()
.build()
.await
.expect("context build");
ctx.register_component(DirectComponent::new());
ctx.register_component(TemplateComponent::default());
let route = RouteBuilder::from("direct:in")
.route_id(route_id)
.to(a_uri.to_string())
.to(b_uri.to_string())
.build()
.expect("route build");
ctx.add_route_definition(route)
.await
.expect("add_route_definition");
ctx.start().await?;
Ok(ctx)
}
async fn reload(route_id: &str) -> Result<(), camel_api::CamelError> {
TemplateReloadRegistry::global()
.reload_route(route_id)
.await
}
fn generations(route_id: &str) -> Vec<u64> {
TemplateReloadRegistry::global()
.find_all(route_id)
.iter()
.map(|t| t.current_generation())
.collect()
}
#[tokio::test(flavor = "multi_thread")]
async fn reload_valid_swaps_atomic() {
let route_id = "t-reload-swaps-atomic";
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
std::fs::write(&entry, PAGE_V1).expect("write v1");
let uri = format!("template:file://{}", entry.display());
let mut ctx = common::start_template_route(&uri, route_id)
.await
.expect("context must start for a valid template");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"<h1>v1 Hi</h1>",
"initial render must be v1"
);
std::fs::write(&entry, PAGE_V2).expect("overwrite v2");
reload(route_id)
.await
.expect("reload of a valid change must succeed");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"<h1>v2 Hi</h1>",
"render after a valid reload must reflect the new set (atomic swap)"
);
std::fs::remove_file(&entry).expect("remove source after reload");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"<h1>v2 Hi</h1>",
"render after source deletion must still use the in-memory compiled set"
);
let _ = ctx.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn reload_invalid_retains_last_good() {
let route_id = "t-reload-invalid-retains";
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
std::fs::write(&entry, PAGE_V1).expect("write v1");
let uri = format!("template:file://{}", entry.display());
let mut ctx = common::start_template_route(&uri, route_id)
.await
.expect("context must start for a valid template");
std::fs::write(&entry, PAGE_V2).expect("overwrite v2");
reload(route_id)
.await
.expect("reload to v2 must succeed (precondition)");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"<h1>v2 Hi</h1>",
"last good set must be v2 before the invalid reload"
);
let gen_before = generations(route_id);
assert_eq!(gen_before.len(), 1, "one reload target for this route");
let good_gen = gen_before[0];
std::fs::write(&entry, PAGE_INVALID).expect("overwrite invalid");
let result = reload(route_id).await;
assert!(
result.is_err(),
"reload of invalid source must return Err, got {result:?}"
);
let gen_after = generations(route_id);
assert_eq!(
gen_after, gen_before,
"generation must be unchanged by a failed reload (last good retained)"
);
assert_eq!(gen_after[0], good_gen, "no commit on failed reload");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"<h1>v2 Hi</h1>",
"render after a failed reload must retain the last good set (AC6)"
);
let _ = ctx.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn reload_multi_producer_all_or_nothing() {
let route_id = "t-reload-multi-all-or-nothing";
let dir = tempfile::tempdir().expect("tempdir");
let entry_a = dir.path().join("a.html");
let entry_b = dir.path().join("b.html");
std::fs::write(&entry_a, A_V1).expect("write a v1");
std::fs::write(&entry_b, B_V1).expect("write b v1");
let uri_a = format!("template:file://{}", entry_a.display());
let uri_b = format!("template:file://{}", entry_b.display());
let mut ctx = start_two_template_route(&uri_a, &uri_b, route_id)
.await
.expect("context must start for two valid templates");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"B1:A1:Hi",
"initial composed render"
);
let baseline = generations(route_id);
assert_eq!(
baseline.len(),
2,
"two template producers must register two reload targets under one route_id"
);
std::fs::write(&entry_a, A_V2).expect("overwrite a v2");
std::fs::write(&entry_b, B_V2).expect("overwrite b v2");
reload(route_id)
.await
.expect("reload of two valid changes must succeed");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"B2:A2:Hi",
"both producers swapped: A→A2, B→B2"
);
let after_ok = generations(route_id);
assert_eq!(
after_ok,
vec![baseline[0] + 1, baseline[1] + 1],
"both targets committed on the successful reload"
);
std::fs::write(&entry_a, A_V3).expect("overwrite a v3 (valid)");
std::fs::write(&entry_b, PAGE_INVALID).expect("overwrite b invalid");
let result = reload(route_id).await;
assert!(
result.is_err(),
"reload must fail when any producer's build fails, got {result:?}"
);
let after_err = generations(route_id);
assert_eq!(
after_err, after_ok,
"neither target committed on the failed reload (all-or-nothing)"
);
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&out),
"B2:A2:Hi",
"both producers retain their prior sets after a failed reload (AC10)"
);
let _ = ctx.stop().await;
}