use std::path::{Path, PathBuf};
use std::process::Command;
use warble_claude_code::{emit_claude_code_with_models, ir::WarbleIr, ModelConfig, RenderFlavor};
use warble_cli::compile_project_to_ir;
fn bootstrap_agent_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("examples/bootstrap-agent")
}
fn compiled_ir() -> WarbleIr {
let value =
compile_project_to_ir(&bootstrap_agent_dir()).expect("bootstrap-agent compiles to IR");
serde_json::from_value(value).expect("compiled IR deserializes")
}
fn context_scope(ir: &WarbleIr, verb: &str) -> String {
ir.components
.iter()
.find(|c| c.verb == verb)
.unwrap_or_else(|| panic!("component '{verb}' present"))
.guardrails
.iter()
.find(|g| g.name == "context_write_authz")
.unwrap_or_else(|| panic!("{verb} has a context_write_authz guardrail"))
.scope
.clone()
.unwrap_or_else(|| panic!("{verb}'s context_write_authz carries a scope"))
}
#[test]
fn bootstrap_mdl_loud_fails_on_both_targets_rather_than_splitting_the_approval_gate() {
let ir = compiled_ir();
for target in ["claude-code:headless", "claude-code:interactive"] {
let out_dir = tempfile::tempdir().expect("tempdir");
let err = emit_claude_code_with_models(
&ir,
out_dir.path(),
target,
RenderFlavor::Programmatic,
&ModelConfig::default(),
)
.expect_err(&format!(
"bootstrap_mdl ({target}) must loud-fail, not silently collapse its tiers nor \
unsafely split write authority to subagents"
));
let message = err.to_string();
assert!(message.contains("llm:per_step_tier"), "{message}");
assert!(message.contains("gated-tool"), "{message}");
assert!(message.contains("bootstrap_mdl"), "{message}");
assert!(
!out_dir.path().join(".claude/agents").exists(),
"a loud-fail on {target} must abort before writing any agent file"
);
}
}
fn write_authorized(scope: &str, target_rel: &str) -> bool {
let scope = Path::new(scope);
Path::new(target_rel).starts_with(scope)
}
#[derive(Debug, PartialEq, Eq)]
enum LifecycleOutcome {
OutOfScope,
RefusedUnapproved,
Applied,
}
fn approval_granted() -> bool {
std::env::var("WARBLE_APPROVAL").as_deref() == Ok("approve")
}
fn run_context_lifecycle(
scope: &str,
target_rel: &str,
repo_dir: &Path,
) -> Result<LifecycleOutcome, String> {
if !write_authorized(scope, target_rel) {
return Ok(LifecycleOutcome::OutOfScope);
}
if !approval_granted() {
return Ok(LifecycleOutcome::RefusedUnapproved);
}
let target = repo_dir.join(target_rel);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
std::fs::write(&target, "# bootstrapped by warble\n")
.map_err(|e| format!("write {}: {e}", target.display()))?;
git(repo_dir, &["add", "-A"]);
git(repo_dir, &["commit", "-q", "-m", "apply context write"]);
Ok(LifecycleOutcome::Applied)
}
fn git(dir: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.unwrap_or_else(|e| panic!("failed to spawn git {args:?}: {e}"));
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn git_output(dir: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.unwrap_or_else(|e| panic!("failed to spawn git {args:?}: {e}"));
assert!(output.status.success(), "git {args:?} failed");
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
#[test]
fn context_write_authz_scopes_do_not_cross() {
let ir = compiled_ir();
let bootstrap_scope = context_scope(&ir, "bootstrap_mdl");
let enrich_scope = context_scope(&ir, "enrich_knowledge");
assert_eq!(bootstrap_scope, "models/");
assert_eq!(enrich_scope, "knowledge/");
assert_ne!(bootstrap_scope, enrich_scope, "the two scopes must differ");
assert!(write_authorized(&bootstrap_scope, "models/orders.yml"));
assert!(
!write_authorized(&bootstrap_scope, "knowledge/orders.md"),
"bootstrap_mdl must not write into enrich_knowledge's scope"
);
assert!(
!write_authorized(&bootstrap_scope, "warehouse/orders.csv"),
"context-write must not bleed into a production-data (data_write) path"
);
assert!(write_authorized(&enrich_scope, "knowledge/orders.md"));
assert!(
!write_authorized(&enrich_scope, "models/orders.yml"),
"enrich_knowledge must not write into bootstrap_mdl's scope"
);
assert!(
!write_authorized(&enrich_scope, "warehouse/orders.csv"),
"context-write must not bleed into a production-data (data_write) path"
);
}
#[test]
fn full_lifecycle_scoped_write_applies_and_cross_scope_or_unapproved_is_refused() {
let ir = compiled_ir();
let scope = context_scope(&ir, "bootstrap_mdl");
let repo = tempfile::tempdir().expect("tempdir");
let repo_dir = repo.path();
git(repo_dir, &["init", "-q"]);
git(
repo_dir,
&["config", "user.email", "warble-e2e@example.com"],
);
git(repo_dir, &["config", "user.name", "Warble E2E"]);
std::fs::create_dir_all(repo_dir.join("models")).unwrap();
std::fs::create_dir_all(repo_dir.join("knowledge")).unwrap();
std::fs::write(repo_dir.join(".gitkeep"), "").unwrap();
git(repo_dir, &["add", "-A"]);
git(repo_dir, &["commit", "-q", "-m", "checkpoint"]);
let checkpoint_sha = git_output(repo_dir, &["rev-parse", "HEAD"]);
std::env::remove_var("WARBLE_APPROVAL");
assert!(
!approval_granted(),
"unset WARBLE_APPROVAL must fail closed"
);
let outcome = run_context_lifecycle(&scope, "knowledge/glossary.md", repo_dir)
.expect("lifecycle runs without error");
assert_eq!(
outcome,
LifecycleOutcome::OutOfScope,
"a cross-scope write must be refused by the scope gate, not applied"
);
assert_eq!(
run_context_lifecycle(&scope, "warehouse/orders.csv", repo_dir).unwrap(),
LifecycleOutcome::OutOfScope
);
std::env::set_var("WARBLE_APPROVAL", "deny");
assert_eq!(
run_context_lifecycle(&scope, "models/orders.yml", repo_dir).unwrap(),
LifecycleOutcome::RefusedUnapproved,
"an in-scope write with approval denied must be refused, not applied"
);
assert_eq!(
git_output(repo_dir, &["rev-parse", "HEAD"]),
checkpoint_sha,
"no refused write may create a commit"
);
assert!(!repo_dir.join("models/orders.yml").exists());
assert!(!repo_dir.join("knowledge/glossary.md").exists());
assert!(!repo_dir.join("warehouse/orders.csv").exists());
std::env::set_var("WARBLE_APPROVAL", "approve");
assert_eq!(
run_context_lifecycle(&scope, "models/orders.yml", repo_dir).unwrap(),
LifecycleOutcome::Applied
);
assert!(
repo_dir.join("models/orders.yml").exists(),
"an approved in-scope write must land the file under models/"
);
assert!(
!repo_dir.join("knowledge/orders.yml").exists()
&& std::fs::read_dir(repo_dir.join("knowledge"))
.unwrap()
.next()
.is_none(),
"the approved write must NOT touch the knowledge/ scope"
);
assert_ne!(
git_output(repo_dir, &["rev-parse", "HEAD"]),
checkpoint_sha,
"the approved write must create a new commit"
);
std::env::remove_var("WARBLE_APPROVAL");
}