use super::*;
fn workflow_with_max_depth(n: u32) -> String {
format!("events: {{}}\nbudgets:\n max_depth: {n}\n")
}
#[test]
fn max_depth_refuses_the_dispatch_that_would_breach_it() {
let (_h, ws) = fixture::workspace();
fixture::amend_config(&ws, &[("workflow.yaml", &workflow_with_max_depth(0))]);
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::ok();
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap_err();
let Error::DispatchRefused {
child,
parent,
exhausted,
} = &err
else {
panic!("{err:?}");
};
assert!(child.starts_with("20260101-p1-"), "{child}");
assert_eq!(parent, "20260101-p1");
assert_eq!(exhausted.axis, crate::prompt::budget::Axis::Depth);
assert_eq!((exhausted.limit, exhausted.actual), (0, 1));
let rendered = err.to_string();
assert!(rendered.contains("max_depth"), "{rendered}");
assert!(rendered.contains("workflow.yaml"), "{rendered}");
assert_eq!(workspace::agent_ids(&ws, &g).unwrap(), vec!["20260101-p1"]);
assert!(!ws.join("agents").join(child).exists());
assert!(!inbox::inbox_dir(&ws, child).exists());
assert!(launcher.invocations.borrow().is_empty());
}
#[test]
fn a_dispatch_within_the_depth_ceiling_forks_normally() {
let (_h, ws) = fixture::workspace();
fixture::amend_config(&ws, &[("workflow.yaml", &workflow_with_max_depth(1))]);
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&crate::template::RealGit::new(),
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&RecordingLauncher::ok(),
)
.unwrap();
}
#[test]
fn a_workflow_triggered_dispatch_reports_a_refusal_and_does_not_fail_the_branch() {
let (_h, ws) = fixture::workspace();
fixture::amend_config(&ws, &[("workflow.yaml", &workflow_with_max_depth(0))]);
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::ok();
run_procedure(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap();
assert_eq!(workspace::agent_ids(&ws, &g).unwrap(), vec!["20260101-p1"]);
assert!(launcher.invocations.borrow().is_empty());
}
#[test]
fn a_workflow_triggered_dispatch_forks_and_still_propagates_other_errors() {
let (_h, ws) = fixture::workspace();
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
run_procedure(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&RecordingLauncher::ok(),
)
.unwrap();
let err = run_procedure(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&RecordingLauncher::failing(),
)
.unwrap_err();
assert!(matches!(err, Error::ExecutorLock { .. }), "{err:?}");
}
#[test]
fn an_unreadable_workflow_declines_the_dispatch_in_the_control_read_voice() {
let (_h, ws) = fixture::workspace();
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&FailOnWorkflowShow(crate::template::RealGit::new()),
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&RecordingLauncher::ok(),
)
.unwrap_err();
let Error::ControlRead { path, .. } = &err else {
panic!("{err:?}");
};
assert!(
path.to_string_lossy().ends_with(":workflow.yaml"),
"{path:?}"
);
}
#[test]
fn a_malformed_workflow_declines_the_dispatch() {
let (_h, ws) = fixture::workspace();
fixture::amend_config(&ws, &[("workflow.yaml", "budgets: [not, a, map]\n")]);
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&crate::template::RealGit::new(),
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&RecordingLauncher::ok(),
)
.unwrap_err();
assert!(matches!(err, Error::Config(_)), "{err:?}");
}
struct FailOnWorkflowShow(crate::template::RealGit);
impl crate::template::GitRunner for FailOnWorkflowShow {
fn run(&self, dest: &Path, args: &[&str]) -> io::Result<()> {
self.0.run(dest, args)
}
fn run_capture(&self, dest: &Path, args: &[&str]) -> io::Result<String> {
if args.iter().any(|a| a.ends_with(":workflow.yaml")) {
return Err(io::Error::other("stub: workflow.yaml unreadable"));
}
self.0.run_capture(dest, args)
}
}