use std::path::PathBuf;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn agent_connector_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/api/routes/agent")
}
#[test]
fn thin_agent_connectors_invoke_run_pipeline() {
for file in [
"handlers.rs",
"streaming.rs",
"channel_message.rs",
"scheduled_tasks.rs",
] {
let path = agent_connector_dir().join(file);
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
src.contains("run_pipeline"),
"{} must call run_pipeline — connectors stay thin (ARCHITECTURE.md §1)",
path.display()
);
}
}
#[test]
fn connector_files_are_structurally_thin() {
let limits = [
("handlers.rs", 80),
("streaming.rs", 500), ("channel_message.rs", 380), ("scheduled_tasks.rs", 120),
];
for (file, max_lines) in limits {
let path = agent_connector_dir().join(file);
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let code_lines = src
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("///")
})
.count();
assert!(
code_lines <= max_lines,
"{file} has {code_lines} code lines (max {max_lines}). \
If this connector grew, check whether business logic leaked \
out of the pipeline (ARCHITECTURE.md §1).",
);
}
}
#[test]
fn connectors_do_not_contain_policy_decisions() {
let forbidden_patterns = [
(
"classify_complexity",
"complexity classification belongs in the pipeline",
),
(
"select_routed_model",
"model selection belongs in the pipeline",
),
("guard_sets::", "guard chain setup belongs in the pipeline"),
("PolicyEngine", "policy evaluation belongs in the pipeline"),
];
for file in ["handlers.rs", "channel_message.rs", "scheduled_tasks.rs"] {
let path = agent_connector_dir().join(file);
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
for (pattern, reason) in &forbidden_patterns {
assert!(
!src.contains(pattern),
"{file} contains '{pattern}' — {reason} (ARCHITECTURE.md §1)",
);
}
}
}
#[test]
fn architecture_md_documents_off_pipeline_exemption() {
let arch = repo_root().join("ARCHITECTURE.md");
let text =
std::fs::read_to_string(&arch).unwrap_or_else(|e| panic!("read {}: {e}", arch.display()));
assert!(
text.contains("Off-pipeline") && text.contains("/api/interview"),
"ARCHITECTURE.md must document off-pipeline exemptions and the interview routes"
);
}
#[test]
fn retire_unused_subagents_uses_shared_disable_primitive() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/api/routes/subagents.rs");
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
src.contains("disable_subagents_by_name"),
"retirement must call roboticus_db::agents::disable_subagents_by_name so soft-disable stays one code path"
);
}
#[test]
fn db_agents_delegation_sql_uses_core_helper() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../roboticus-db/src/agents.rs");
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
src.contains("delegation_output_tool_name_sql_in_predicate"),
"roboticus-db agents.rs must build delegation SQL from roboticus_core::delegation_tools"
);
}
#[test]
fn pipeline_crate_does_not_depend_on_api() {
let cargo_toml = repo_root().join("crates/roboticus-pipeline/Cargo.toml");
let text = std::fs::read_to_string(&cargo_toml)
.unwrap_or_else(|e| panic!("read {}: {e}", cargo_toml.display()));
assert!(
!text.contains("roboticus-api"),
"roboticus-pipeline/Cargo.toml must NOT depend on roboticus-api — \
the pipeline crate is a lower layer consumed by the API crate"
);
}
#[test]
fn pipeline_crate_has_no_appstate_references() {
let pipeline_src = repo_root().join("crates/roboticus-pipeline/src");
for entry in walkdir(&pipeline_src) {
if !entry.ends_with(".rs") {
continue;
}
let src = std::fs::read_to_string(&entry)
.unwrap_or_else(|e| panic!("read {}: {e}", entry.display()));
assert!(
!src.contains("AppState"),
"{} mentions AppState — pipeline crate must use capability traits, \
not the concrete application state type",
entry.display()
);
}
}
#[test]
fn core_module_has_no_appstate() {
let core_dir = agent_connector_dir().join("core");
for entry in walkdir(&core_dir) {
if !entry.ends_with(".rs") {
continue;
}
let src = std::fs::read_to_string(&entry)
.unwrap_or_else(|e| panic!("read {}: {e}", entry.display()));
assert!(
!src.contains("&AppState"),
"{} contains &AppState — core module functions must use \
CoreInferenceDeps or narrower trait refs",
entry.display()
);
}
}
#[test]
fn pipeline_context_stages_have_no_appstate() {
let context_dir = agent_connector_dir().join("pipeline/context");
for entry in walkdir(&context_dir) {
if !entry.ends_with(".rs") {
continue;
}
let src = std::fs::read_to_string(&entry)
.unwrap_or_else(|e| panic!("read {}: {e}", entry.display()));
assert!(
!src.contains("&AppState"),
"{} contains &AppState — pipeline context stages must use \
trait-scoped dep structs (SessionDeps, CoreInferenceDeps, etc.)",
entry.display()
);
}
}
#[test]
fn orchestrator_production_code_has_no_appstate() {
let path = agent_connector_dir().join("pipeline/run.rs");
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let production = src.split("#[cfg(test)]").next().unwrap_or(&src);
assert!(
!production.contains("&AppState"),
"pipeline/run.rs production code contains &AppState — the orchestrator \
must operate through PipelineDeps / trait-scoped dep structs only"
);
}
#[test]
fn pipeline_core_does_not_expose_hmac() {
let path = repo_root().join("crates/roboticus-pipeline/src/capabilities.rs");
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let core_start = src
.find("pub trait PipelineCore")
.expect("PipelineCore trait");
let core_end = src[core_start..]
.find("\n}")
.map(|i| core_start + i)
.unwrap_or(src.len());
let core_body = &src[core_start..core_end];
assert!(
!core_body.contains("hmac_secret"),
"PipelineCore must not expose hmac_secret — use PipelineSecurity trait"
);
}
#[test]
fn connectors_use_pipeline_deps_not_stage_deps() {
for file in ["handlers.rs", "channel_message.rs", "scheduled_tasks.rs"] {
let path = agent_connector_dir().join(file);
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
assert!(
!src.contains("CoreInferenceDeps"),
"{file} constructs CoreInferenceDeps directly — connectors should \
use state.pipeline_deps() and let the pipeline narrow internally"
);
}
}
fn walkdir(dir: &std::path::Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(walkdir(&path));
} else if path.extension().is_some_and(|e| e == "rs") {
files.push(path);
}
}
}
files
}