use super::*;
#[test]
fn lifecycle_unavailable_run_suggests_explicit_build_with_same_phase_and_dir() {
for dir in [None, Some("nested project")] {
let mut args = serde_json::json!({"phase": "check", "action": "run"});
let mut expected = serde_json::json!({"phase": "check", "action": "build"});
if let Some(dir) = dir {
args["dir"] = dir.into();
expected["dir"] = dir.into();
}
let original = "error: cargo not in this profile's carried userland";
let (text, outcome) =
lifecycle_run_result(&args, (original.into(), crate::ExecOutcome::Unavailable));
assert_eq!(outcome, crate::ExecOutcome::Unavailable);
assert!(text.starts_with(original), "{text}");
let suggestion = text
.lines()
.find_map(|line| line.strip_prefix("Suggested lifecycle call: "))
.expect("unavailable run should expose the explicit build request");
assert_eq!(
serde_json::from_str::<serde_json::Value>(suggestion).unwrap(),
expected
);
assert!(text.contains("explicit approval"), "{text}");
assert!(text.contains("network denied"), "{text}");
assert!(text.contains("denials remain binding"), "{text}");
assert!(
!text.contains("no command ran"),
"a prior compound-command stage may have run"
);
}
}
#[test]
fn lifecycle_run_coaching_preserves_denials_and_executed_outcomes() {
for outcome in [
crate::ExecOutcome::Denied,
crate::ExecOutcome::Failed,
crate::ExecOutcome::Passed,
] {
let original = ("original tool result".to_string(), outcome);
assert_eq!(
lifecycle_run_result(&serde_json::json!({"phase":"check"}), original.clone()),
original,
);
}
}
#[test]
fn lifecycle_timed_out_run_puts_build_call_first() {
let args = serde_json::json!({"phase": "test"});
let original = (
"partial output\n(the command hit the 60s wall and was killed...)".to_string(),
crate::ExecOutcome::TimedOut,
);
let (text, outcome) = lifecycle_run_result(&args, original.clone());
assert_eq!(outcome, crate::ExecOutcome::TimedOut);
let first_line = text.lines().next().expect("non-empty result");
let suggestion = first_line
.strip_prefix("Suggested lifecycle call: ")
.expect("build call must be the first line of a timed-out run result");
assert_eq!(
serde_json::from_str::<serde_json::Value>(suggestion).unwrap(),
serde_json::json!({"phase": "test", "action": "build"})
);
assert!(
text.ends_with(&original.0),
"original (partial) output must be preserved: {text}"
);
}
#[tokio::test]
async fn lifecycle_phase_build_points_at_action_build() {
let caveats = crate::caveats::Caveats::top();
let args = serde_json::json!({ "phase": "build" });
let out = execute_tool(
"lifecycle",
&args,
".",
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await;
assert!(
out.starts_with("error: unknown lifecycle phase 'build'"),
"{out}"
);
let suggestion = out
.lines()
.find_map(|line| line.strip_prefix("Suggested lifecycle call: "))
.expect("must point at the real action=build call: {out}");
assert_eq!(
serde_json::from_str::<serde_json::Value>(suggestion).unwrap(),
serde_json::json!({"phase": "test", "action": "build"})
);
}
#[test]
fn escalated_after_timeout_labels_the_reroute_and_preserves_the_build_result() {
let wall = shell::dispatch_wall("pytest -k slow", Default::default()); for (build_result, outcome) in [
(" ✓ build check passed".to_string(), crate::ExecOutcome::Passed),
(
"capability denied: lifecycle action=build requires explicit confined build authority; no command ran".to_string(),
crate::ExecOutcome::Denied,
),
] {
let (text, out) = escalated_after_timeout((build_result.clone(), outcome), wall);
assert_eq!(out, outcome, "the build lane's own outcome is preserved");
assert!(
text.contains("action=run") && text.contains(&format!("{}s wall", wall.as_secs())),
"must say plainly it escalated and why, with the wall that applied: {text}"
);
assert!(
text.ends_with(&build_result),
"the build lane's real result must survive verbatim: {text}"
);
}
}
#[test]
fn escalates_only_on_a_default_wall_timeout() {
let _env = crate::process_env::lock();
let default_wall = shell::dispatch_wall("pytest -k slow", Default::default());
let build_wall = shell::LIFECYCLE_BUILD_TIMEOUT;
assert_eq!(
shell::dispatch_wall("cargo test", Default::default(),),
build_wall,
"#2732: a classified build retains its build budget"
);
assert_ne!(
default_wall, build_wall,
"fixture sanity: the two walls must differ for this table to mean anything"
);
for (outcome, wall, expected) in [
(crate::ExecOutcome::TimedOut, default_wall, true),
(crate::ExecOutcome::TimedOut, build_wall, false),
(crate::ExecOutcome::Passed, default_wall, false),
(crate::ExecOutcome::Failed, default_wall, false),
(crate::ExecOutcome::Denied, default_wall, false),
(crate::ExecOutcome::Unavailable, default_wall, false),
] {
assert_eq!(
escalates(outcome, wall),
expected,
"escalates({outcome:?}, {wall:?}) should be {expected}"
);
}
}
#[test]
fn a_refused_escalation_keeps_the_first_runs_evidence() {
let wall = shell::dispatch_wall("pytest -k slow", Default::default());
let first = (
format!(
"partial: test_foo hung{}",
shell::timed_out_note(wall, Default::default(),)
),
crate::ExecOutcome::TimedOut,
);
for (refusal, outcome) in [
(
"capability denied: lifecycle action=build requires explicit confined build authority; no command ran".to_string(),
crate::ExecOutcome::Denied,
),
(
"Error: frame isolation: tool authority exceeds the frame's ceiling".to_string(),
crate::ExecOutcome::Denied,
),
(
"error: build workspace: No such file or directory".to_string(),
crate::ExecOutcome::Unavailable,
),
] {
let (text, out) = escalation_result(first.clone(), (refusal.clone(), outcome), wall, Default::default(),);
assert_eq!(out, outcome, "the refusal's own class is the final outcome");
assert!(
text.contains("partial: test_foo hung"),
"the first run's evidence (which test hung) must survive: {text}"
);
assert!(
text.contains(&refusal),
"the escalation's refusal must still be visible: {text}"
);
assert!(
!text.contains("Suggested lifecycle call:"),
"a refused escalation must not recommend the exact call that was just \
declined: {text}"
);
assert!(
!text.contains("call the tool with"),
"a result must carry ONE recommendation — the shell's own timed-out \
suggestion to use action=build (which literally says 'call the tool \
with') must not survive alongside the 'declined, do not retry' line: \
{text}"
);
assert!(
text.contains("declined") && text.contains("narrow the command"),
"must say plainly that build authority was declined and to narrow the \
command instead of retrying it: {text}"
);
}
}
#[test]
fn an_executed_escalation_supersedes_the_first_runs_partial_output() {
let wall = shell::dispatch_wall("pytest -k slow", Default::default());
let first = (
"partial: test_foo hung\n(the command hit the 60s wall and was killed...)".to_string(),
crate::ExecOutcome::TimedOut,
);
let build_result = " ✓ build check passed".to_string();
let (text, out) = escalation_result(
first,
(build_result.clone(), crate::ExecOutcome::Passed),
wall,
Default::default(),
);
assert_eq!(out, crate::ExecOutcome::Passed);
assert!(
!text.contains("test_foo hung"),
"an executed escalation's own result supersedes the first run's partial output: {text}"
);
assert!(text.ends_with(&build_result), "{text}");
}
#[test]
fn the_escalation_names_itself_in_the_permission_prompt() {
let base = crate::confined_exec::workspace_confined_caveats(std::path::Path::new("/ws"));
let direct = lifecycle_build_request("/ws", "cargo test --offline", &base, None);
assert!(
!direct.reason.to_lowercase().contains("escalat"),
"a direct action=build call names no escalation: {}",
direct.reason
);
let wall = shell::dispatch_wall("pytest -k slow", Default::default());
let reason = format!(
"lifecycle action=run hit the confined shell's {}s wall",
wall.as_secs()
);
let escalated = lifecycle_build_request("/ws", "cargo test --offline", &base, Some(&reason));
assert!(
escalated.reason.contains("escalation")
&& escalated.reason.contains("action=run")
&& escalated
.reason
.contains(&format!("{}s wall", wall.as_secs())),
"the prompt must name why a build-authority request appeared: {}",
escalated.reason
);
}
#[test]
fn lifecycle_is_a_real_tool_name_not_a_hallucination() {
assert!(
ALL_TOOL_NAMES.contains(&"lifecycle"),
"lifecycle must be a real tool name"
);
assert!(
!is_hallucination("lifecycle", &serde_json::json!({"phase": "test"})),
"a real lifecycle call must not be flagged as a hallucination"
);
}
#[test]
fn lifecycle_definition_enum_matches_phase_vocabulary() {
let def = lifecycle_tool_definition();
assert_eq!(def["function"]["name"], "lifecycle");
let enum_vals: Vec<&str> = def["function"]["parameters"]["properties"]["phase"]["enum"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
let vocab: Vec<&str> = crate::tooling::Phase::ALL
.iter()
.map(|p| p.as_str())
.collect();
assert_eq!(enum_vals, vocab);
}
#[test]
fn run_phase_aliases_route_to_lifecycle() {
for a in ["run_phase", "run_lifecycle", "lifecycle_run"] {
assert!(
matches!(
resolve_tool_alias(a),
Some(AliasOutcome::Rewrite("lifecycle"))
),
"{a} should rewrite to lifecycle"
);
}
assert!(resolve_tool_alias("lifecycle").is_none());
}
#[tokio::test]
async fn lifecycle_unknown_phase_lists_valid_phases() {
let caveats = crate::caveats::Caveats::top();
let args = serde_json::json!({ "phase": "deploy" });
let out = execute_tool(
"lifecycle",
&args,
".",
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await;
assert!(
out.starts_with("error: unknown lifecycle phase 'deploy'"),
"{out}"
);
assert!(out.contains("check"), "should name valid phases: {out}");
}
#[tokio::test]
async fn lifecycle_task_state_args_are_coached_to_update_plan() {
let caveats = crate::caveats::Caveats::top();
for args in [
serde_json::json!({ "event": "complete", "message": "done" }),
serde_json::json!({ "status": "blocked" }),
serde_json::json!({ "state": "in_progress" }),
] {
let out = execute_tool(
"lifecycle",
&args,
".",
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await;
assert!(out.starts_with("error:"), "{args}: {out}");
assert!(
!out.contains("unknown lifecycle phase"),
"{args}: still the phase dead-end: {out}"
);
for tool in ["update_plan", "request_user_input"] {
assert!(out.contains(tool), "{args}: should coach to {tool}: {out}");
}
}
}
#[tokio::test]
async fn lifecycle_root_empty_names_a_nested_project_instead_of_a_silent_noop() {
let caveats = crate::caveats::Caveats::top();
let args = serde_json::json!({ "phase": "test" });
let out = execute_tool(
"lifecycle",
&args,
"../crates",
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await;
assert!(
out.starts_with("no command configured for lifecycle phase 'test'"),
"{out}"
);
assert!(
out.contains("newt-tuner"),
"names the nested project: {out}"
);
assert!(out.contains("dir=\"<path>\""), "points at the fix: {out}");
assert!(
!out.starts_with("error:"),
"an honest degrade is not a fake failure: {out}"
);
assert!(
!tool_result_ok(&out),
"a no-op must not ledger as a claimable success: {out}"
);
}
#[tokio::test]
async fn lifecycle_dir_param_resolves_a_nested_project_directly() {
let caveats = crate::caveats::Caveats::top();
let args = serde_json::json!({ "phase": "test", "action": "list", "dir": "newt-tuner" });
let out = execute_tool(
"lifecycle",
&args,
"../crates",
false,
20,
&caveats,
&mut NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await;
assert_eq!(out, "lifecycle test → cargo test", "got: {out}");
assert!(
tool_result_ok(&out),
"a genuinely resolved phase is still ok=true: {out}"
);
}
#[test]
fn run_build_check_reports_pass_fail_and_spawn_error() {
let ws = tempfile::TempDir::new().unwrap();
let ws_str = ws.path().to_string_lossy();
let passed = run_build_check(passing_build_check_cmd(), &ws_str, &crate::Scope::none());
if crate::confined_exec::kernel_fs_fence_available() {
if passed == " ✓ build check passed" {
let failed = run_build_check(
&failing_build_check_cmd("boom"),
&ws_str,
&crate::Scope::none(),
);
assert!(failed.contains("✗ build check failed"), "got: {failed}");
assert!(failed.contains("boom"), "stderr excerpt shown: {failed}");
} else {
assert!(
passed.contains("⚠ build check could not run"),
"with the egress floor, build_check must confine-and-run or fail \
closed, got: {passed}"
);
}
} else {
assert!(
passed == " ✓ build check passed" || passed.contains("⚠ build check could not run"),
"off Linux, build_check must confine-and-run or fail closed, got: {passed}"
);
}
let err = run_build_check(
passing_build_check_cmd(),
"/definitely/not/a/dir",
&crate::Scope::none(),
);
assert!(err.contains("⚠ build check could not run"), "got: {err}");
}
#[test]
fn lifecycle_build_authority_is_explicit_and_does_not_widen_shell_grants() {
let base = crate::confined_exec::workspace_confined_caveats(std::path::Path::new("/ws"));
let request = lifecycle_build_request("/ws", "cargo test --offline", &base, None);
assert_eq!(request.kind, DenialKind::Build);
assert_eq!(request.target, "/ws");
assert!(request.reason.contains("cargo test --offline"));
assert!(request.reason.contains("network denied"));
assert!(
!request.reason.contains("escalation"),
"a direct action=build call names no escalation: {}",
request.reason
);
assert_eq!(
crate::agentic::permissions::widen_caveats(&base, &[(request.kind, request.target)]),
base
);
assert_eq!(
lifecycle_tool_definition()["function"]["parameters"]["properties"]["action"]["enum"],
serde_json::json!(["run", "list", "build"])
);
}
#[test]
fn lifecycle_build_prompt_reports_existing_network_authority() {
let mut build = crate::confined_exec::build_tool_caveats(std::path::Path::new("/ws"));
build.net = crate::Scope::All;
let request = lifecycle_build_request("/ws", "cargo test", &build, None);
assert!(
request
.reason
.contains("network allowed by the existing operator grant"),
"{}",
request.reason
);
assert!(
!request.reason.contains("network denied"),
"{}",
request.reason
);
build.net = crate::Scope::only(["example.test".into()]);
let request = lifecycle_build_request("/ws", "cargo test", &build, None);
assert!(
request
.reason
.contains("network limited to the existing operator grant"),
"{}",
request.reason
);
}
#[test]
fn lifecycle_run_of_build_tool_routes_to_build_lane() {
assert!(
lifecycle_run_routes_to_build_lane("just check"),
"just is a build tool"
);
assert!(
lifecycle_run_routes_to_build_lane("cargo test --workspace"),
"cargo is a build tool"
);
assert!(
lifecycle_run_routes_to_build_lane("make test"),
"make is a build tool"
);
assert!(
!lifecycle_run_routes_to_build_lane("pytest -x"),
"pytest is not a build tool"
);
assert!(
!lifecycle_run_routes_to_build_lane("python -m pytest"),
"python is not a build tool"
);
assert!(
!lifecycle_run_routes_to_build_lane(""),
"empty command is not a build tool"
);
}
#[test]
fn lifecycle_run_of_non_build_tool_does_not_route() {
assert!(!lifecycle_run_routes_to_build_lane("ruff check ."));
assert!(!lifecycle_run_routes_to_build_lane("black --check ."));
assert!(!lifecycle_run_routes_to_build_lane("mypy src/"));
}
#[test]
fn cold_cache_recovery_is_visible_to_model_and_operator() {
#[derive(Default)]
struct Display(String);
impl crate::agentic::display::ToolPresentation for Display {
fn preview(&mut self, _: &str, _: usize) {}
fn document(&mut self, _: &str) {}
fn override_result(&mut self, text: String) {
self.0 = text;
}
}
for trim in [
None,
OutputTrim::from_json(Some(&serde_json::json!({"mode":"head","n":1}))),
] {
let mut display = Display::default();
let out = crate::confined_exec::ConfinedOutput {
success: false,
code: Some(101),
stdout: b"build header\n".to_vec(),
stderr: b"error: cache missing\n".to_vec(),
timed_out: false,
sandbox_kind: agent_bridle::SandboxKind::None,
};
let note = "note: automatic locked fetch was declined by the operator";
let (model, outcome) = render_confined_build_result(
Ok(out),
Some(note),
trim,
10,
false,
false,
None,
&mut display,
);
assert_eq!(model.matches(note).count(), 1);
assert_eq!(outcome, crate::ExecOutcome::Failed);
assert!(
display.0.contains(note),
"operator missed recovery: {}",
display.0
);
}
}