//! §11 tail — `sealed` (non-suppressible) group templates, end to end via //! `pic`. A group declares a **sealed** `[[routes]]` template; a descendant app //! that applies a `[suppress] routes=[...]` for it STILL serves the inherited //! path (the rebuild keeps a sealed row), the apply emits an ineffective-suppress //! warning, and `pic routes ls --group` shows `sealed = true`. Sealing an //! `[app]` route is rejected — sealing marks a *group* template, and an app //! route is never inherited. //! //! The repo-layer proof (a sealed trigger + route survive suppression while an //! unsealed sibling is declined) lives in //! `manager-core/tests/sealed_templates.rs`; this journey exercises the //! authoring + the orchestrator dispatch path the operator uses. use std::fs; use tempfile::TempDir; use crate::common; use crate::common::cleanup::{AppGuard, GroupGuard, ScriptGuard}; fn manifest_dir() -> TempDir { let dir = TempDir::new().expect("tempdir"); fs::create_dir_all(dir.path().join("scripts")).expect("scripts dir"); dir } fn group_script_id(env: &common::TestEnv, group: &str, name: &str) -> String { let ls = common::pic_as(env) .args(["scripts", "ls", "--group", group]) .output() .expect("scripts ls"); let table = String::from_utf8(ls.stdout).unwrap(); table .lines() .map(common::cells) .find(|c| c.get(2) == Some(&name)) .and_then(|c| c.first().map(|s| (*s).to_string())) .unwrap_or_else(|| panic!("group script `{name}` not found:\n{table}")) } fn route_matches(env: &common::TestEnv, app: &str, url: &str) -> bool { let out = common::pic_as(env) .args(["routes", "match", "--app", app, url]) .output() .expect("routes match"); let stdout = String::from_utf8(out.stdout).unwrap(); stdout.contains("matched") && stdout.contains("true") } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn sealed_route_template_ignores_suppression() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let group = common::unique_slug("seal-grp"); let _g = GroupGuard::new(&env.url, &env.token, &group); common::pic_as(&env) .args(["groups", "create", &group]) .assert() .success(); // Group handler + a SEALED route template binding it. let dir = manifest_dir(); fs::write( dir.path().join("scripts/ghealth.rhai"), r#"log::info("group health"); "ok""#, ) .unwrap(); common::pic_as(&env) .args(["scripts", "deploy"]) .arg(dir.path().join("scripts/ghealth.rhai")) .args(["--group", &group, "--name", "ghealth"]) .assert() .success(); let _gs = ScriptGuard::new( &env.url, &env.token, &group_script_id(&env, &group, "ghealth"), ); let gmanifest = format!( "[group]\nslug = \"{group}\"\nname = \"SealG\"\n\n\ [[routes]]\nscript = \"ghealth\"\npath = \"/ghealth\"\npath_kind = \"exact\"\n\ host_kind = \"any\"\nsealed = true\n" ); let gpath = dir.path().join("group.toml"); fs::write(&gpath, &gmanifest).unwrap(); common::pic_as(&env) .args(["apply", "--file"]) .arg(&gpath) .assert() .success(); // `pic routes ls --group` shows the sealed column true. let ls = String::from_utf8( common::pic_as(&env) .args(["routes", "ls", "--group", &group]) .output() .unwrap() .stdout, ) .unwrap(); let sealed_row = ls .lines() .map(common::cells) .find(|c| c.contains(&"/ghealth")) .unwrap_or_else(|| panic!("no /ghealth row in routes ls --group:\n{ls}")); assert!( sealed_row.contains(&"true"), "the sealed column must read true for /ghealth:\n{ls}" ); // A descendant app inherits it. let blog = common::unique_slug("seal-blog"); let _ba = AppGuard::new(&env.url, &env.token, &blog); common::pic_as(&env) .args(["apps", "create", &blog, "--group", &group]) .assert() .success(); assert!(route_matches(&env, &blog, "http://localhost/ghealth")); // blog tries to suppress the inherited route — but it is SEALED, so the // opt-out is ignored: the route keeps serving, and the apply warns. let amanifest = format!( "[app]\nslug = \"{blog}\"\nname = \"Blog\"\n\n\ [suppress]\nroutes = [\"/ghealth\"]\n" ); let apath = dir.path().join("app.toml"); fs::write(&apath, &amanifest).unwrap(); let out = common::pic_as(&env) .args(["apply", "--file"]) .arg(&apath) .output() .expect("apply blog"); assert!(out.status.success(), "blog apply failed: {out:?}"); let combined = format!( "{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr) ); assert!( combined.to_lowercase().contains("sealed") && combined.to_lowercase().contains("no effect"), "suppressing a sealed template must warn it has no effect:\n{combined}" ); assert!( route_matches(&env, &blog, "http://localhost/ghealth"), "a sealed template is non-suppressible — the route must still serve" ); // Sealing an APP route is rejected — sealing marks a group template, and an // app route is never inherited. let bad = format!( "[app]\nslug = \"{blog}\"\nname = \"Blog\"\n\n\ [[routes]]\nscript = \"ghealth\"\npath = \"/own\"\npath_kind = \"exact\"\n\ host_kind = \"any\"\nsealed = true\n" ); fs::write(&apath, &bad).unwrap(); let out = common::pic_as(&env) .args(["apply", "--file"]) .arg(&apath) .output() .expect("apply bad"); assert!( !out.status.success(), "an [app] route cannot be sealed — apply must fail" ); let err = String::from_utf8_lossy(&out.stderr); assert!( err.to_lowercase().contains("sealed"), "the rejection must mention sealing:\n{err}" ); }