fabro/lib/foundation/fabro-dev/tests/it/policy.rs
Bryan Helmkamp 5f49cdf9da
Delete fabro-model and document the lithos catalog overlay
fabro-model's ids and billing rollup now live in fabro-types, and its
pricing, catalog, provider TOMLs, and legacy index are replaced by the
lithos built-in catalog plus the Fabro policy layer.

Regenerate the configuration reference for the `[llm]` overlay and
`metadata.fabro`, and rewrite the SDK, models, and integration docs for
the lithos provider and model shapes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00

181 lines
6 KiB
Rust

//! 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;
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 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<usize>,
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::<Vec<_>>()
.join("\n")
}