//! Workspace policy tests. //! //! These tests scan the source tree for references that violate //! product-level invariants. They run as part of `cargo nextest` and are //! cheap (text scans only). use walkdir::WalkDir; use crate::workspace_root; /// `fabro_model::bootstrap_catalog` (and its module) is the install/API-key /// validation hatch from the settings-driven LLM catalog plan. It must /// **not** appear in request-serving paths — server handlers, workflow /// operations, agent runtime, hooks, or completion handlers — because those /// must use the resolved `Arc` threaded through their state. /// /// The allowed-callers list below is the policy boundary. Adding a new /// caller is intentional and requires updating this list. /// /// The walker only descends into `lib/`, so non-`lib/` paths (docs, top-level /// markdown) are not part of the allowlist. const BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS: &[&str] = &[ // The bootstrap module itself. "lib/foundation/fabro-model/src/bootstrap_catalog", // Public module declaration for the bootstrap hatch. "lib/foundation/fabro-model/src/lib.rs", // Install / first-run / API-key validation flows that legitimately need // a built-in catalog before any project settings have been loaded. "lib/components/fabro-install/", "lib/apps/fabro-cli/src/commands/install/", "lib/apps/fabro-cli/src/shared/install_", "lib/apps/fabro-cli/src/shared/api_key_validation", // Test support modules. "tests/", "test_support", "/tests/it/", "/tests/policy.rs", ]; /// Production runtime code should build catalogs from resolved settings and /// thread the resulting `Arc` through state. Direct use of /// `Catalog::builtin()` is reserved for `fabro-model` internals and tests. const CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS: &[&str] = &[ // The catalog owner may define and test the built-in/default catalog. "lib/foundation/fabro-model/", // Tests and test support may use built-ins as fixtures. "/tests/", "/tests/it/", "test_support", "/tests/policy.rs", ]; const TEMPLATE_RENDER_ALLOWED_PATH_FRAGMENTS: &[&str] = &[ // The template crate owns the rendering API and its tests. "lib/foundation/fabro-template/src/lib.rs", // Workflow-definition rendering must stay centralized here. "lib/components/fabro-workflow/src/transforms/variable_expansion.rs", // Hook header/env interpolation is a separate system. "lib/components/fabro-hooks/src/executor.rs", // This policy test names the forbidden patterns. "/tests/it/policy.rs", ]; const TEMPLATE_RENDER_FORBIDDEN_PATTERNS: &[&str] = &[ "render_template(", "render_lenient(", "render_scan_template", "render as render_template", "render_lenient as", "fabro_template::{", ]; #[test] fn bootstrap_catalog_references_stay_in_allowlist() { let violations = source_symbol_violations( "bootstrap_catalog", BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS, ); assert!( violations.is_empty(), "bootstrap_catalog (install-only) referenced from non-allowlisted source files:\n{}\n\nIf this is intentional, add the path fragment to BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS in lib/foundation/fabro-dev/tests/it/policy.rs.", format_violations(violations), ); } #[test] fn catalog_builtin_references_stay_in_allowlist() { let violations = source_symbol_violations("Catalog::builtin()", CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS); assert!( violations.is_empty(), "Catalog::builtin() referenced from non-allowlisted production source files:\n{}\n\nRuntime code should use a resolved settings catalog via `Catalog::from_builtin_with_overrides(...)` or an injected `Arc`. If this is intentional test/bootstrap code, add the path fragment to CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS in lib/foundation/fabro-dev/tests/it/policy.rs.", format_violations(violations), ); } #[test] fn workflow_template_rendering_call_sites_stay_in_allowlist() { let mut violations = Vec::new(); for pattern in TEMPLATE_RENDER_FORBIDDEN_PATTERNS { violations.extend(source_symbol_violations( pattern, TEMPLATE_RENDER_ALLOWED_PATH_FRAGMENTS, )); } assert!( violations.is_empty(), "Workflow template rendering must go through TemplateTransform. Add an allowlist entry only for non-workflow interpolation with a reason:\n{}", format_violations(violations), ); } #[expect( clippy::disallowed_methods, reason = "policy test reads source files synchronously with std::fs" )] fn source_symbol_violations( symbol: &str, allowed_path_fragments: &[&str], ) -> Vec<(String, usize, String)> { let root = workspace_root(); let lib_root = root.join("lib"); let mut violations: Vec<(String, usize, String)> = Vec::new(); let walker = WalkDir::new(&lib_root).into_iter().filter_entry(|entry| { // Skip generated/output directories at any depth. let name = entry.file_name().to_string_lossy(); !matches!( name.as_ref(), "target" | ".git" | "node_modules" | "dist" | "build" ) }); for entry in walker.flatten() { let path = entry.path(); if !path.is_file() || path.extension().is_none_or(|ext| ext != "rs") { continue; } let Ok(contents) = std::fs::read_to_string(path) else { continue; }; // Cheap early-out: avoids per-line work for the ~99% of files with no // reference to the symbol. if !contents.contains(symbol) { continue; } let rel = path.strip_prefix(&root).unwrap_or(path); let rel_str = rel.to_string_lossy().replace('\\', "/"); let path_allowed = allowed_path_fragments .iter() .any(|frag| rel_str.contains(frag)); if path_allowed { continue; } let mut pending_cfg_test = false; let mut cfg_test_depth = None; let mut brace_depth = 0usize; for (idx, line) in contents.lines().enumerate() { let trimmed = line.trim_start(); let starts_cfg_test_module = pending_cfg_test && trimmed.contains("mod tests") && trimmed.contains('{'); let in_cfg_test_module = cfg_test_depth.is_some() || starts_cfg_test_module; if !line.contains(symbol) { update_test_module_state( trimmed, &mut pending_cfg_test, &mut cfg_test_depth, &mut brace_depth, starts_cfg_test_module, ); continue; } // Skip comments referencing the symbol in prose. if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') { update_test_module_state( trimmed, &mut pending_cfg_test, &mut cfg_test_depth, &mut brace_depth, starts_cfg_test_module, ); continue; } if in_cfg_test_module { update_test_module_state( trimmed, &mut pending_cfg_test, &mut cfg_test_depth, &mut brace_depth, starts_cfg_test_module, ); continue; } violations.push((rel_str.clone(), idx + 1, line.to_string())); update_test_module_state( trimmed, &mut pending_cfg_test, &mut cfg_test_depth, &mut brace_depth, starts_cfg_test_module, ); } } violations } fn update_test_module_state( trimmed: &str, pending_cfg_test: &mut bool, cfg_test_depth: &mut Option, brace_depth: &mut usize, starts_cfg_test_module: bool, ) { let depth_before = *brace_depth; let open_count = trimmed.chars().filter(|c| *c == '{').count(); let close_count = trimmed.chars().filter(|c| *c == '}').count(); *brace_depth = brace_depth.saturating_add(open_count); *brace_depth = brace_depth.saturating_sub(close_count); if starts_cfg_test_module { *cfg_test_depth = Some( depth_before .saturating_add(open_count) .saturating_sub(close_count), ); } if cfg_test_depth.is_some_and(|depth| *brace_depth < depth) { *cfg_test_depth = None; } if trimmed.starts_with("#[cfg(test)]") { *pending_cfg_test = true; } else if !trimmed.is_empty() && !trimmed.starts_with("#[") { *pending_cfg_test = false; } } fn format_violations(violations: Vec<(String, usize, String)>) -> String { violations .into_iter() .map(|(p, l, s)| format!(" {p}:{l}: {}", s.trim())) .collect::>() .join("\n") }