use super::Harness;
use crate::AppModel;
use crate::boundary::{Action, reply::Reply};
use crate::cli_outbound::Cli;
use crate::git_tree::AgentState;
use crate::opslog;
use crate::test_support::{engine, spawn_guard};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use tempfile::{TempDir, tempdir};
fn fake_lernie(dir: &TempDir, stdout: &str, stderr: &str, code: i32) -> Cli {
let path = dir.path().join("lernie");
fs::write(
&path,
format!(
"#!/bin/sh\necho \"$@\" > {}/argv.log\nprintf '%s\\n' '{stdout}'\nprintf '%s' '{stderr}' 1>&2\nexit {code}\n",
dir.path().display()
),
)
.unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
Cli::new(path)
}
fn logged_argv(dir: &TempDir) -> String {
fs::read_to_string(dir.path().join("argv.log"))
.unwrap_or_default()
.trim()
.to_owned()
}
fn unused_bl() -> Cli {
Cli::new("bl")
}
fn delete(
model: &mut AppModel,
lernie: &Cli,
ws: &Path,
root: &str,
typed: &str,
) -> Result<Reply, String> {
let deps = model.boundary_deps(lernie, &unused_bl());
let action = Action::DeleteAgent {
workspace: model.snap.ws_name(ws),
agent: root.to_owned(),
typed: typed.to_owned(),
};
let landed = engine::act(model, &deps, "TS", &action);
model.after_lernie_verb();
model.deleted_agent(ws, root);
landed
}
fn outcome(landed: Result<Reply, String>) -> crate::actions::verbs::Outcome {
match landed.expect("the verb ran") {
Reply::Outcome(o) => o,
other => panic!("a verb answers an outcome, not {other:?}"),
}
}
#[test]
fn the_confirmation_is_offered_only_inside_named_workspaces() {
let mut h = Harness::new();
let named = h.mint_named("alba-koi", "c-1");
let replay = h.add_replay("20260101T-rr", "c-9");
let (_c, model) = h.model();
let confirm =
crate::boundary::answer::agent_confirmation_of(&model.snap, &named, "c-1").unwrap();
assert_eq!(confirm.name, "hi", "the display ladder names the dialog");
assert!(!confirm.refused());
assert!(
crate::boundary::answer::agent_confirmation_of(&model.snap, &h.ws, "c-1").is_none(),
"foreign"
);
assert!(
crate::boundary::answer::agent_confirmation_of(&model.snap, &replay, "c-9").is_none(),
"replay"
);
}
#[test]
fn an_unnamed_workspace_is_refused_outright() {
let h = Harness::new();
let (_c, mut model) = h.model();
let err = delete(&mut model, &unused_bl(), &h.ws, "c-1", "").unwrap_err();
assert_eq!(err, "not a yog-named workspace");
}
#[test]
fn a_live_member_refuses_before_anything_spawns() {
let mut h = Harness::new();
let named = h.mint_named("alba-koi", "c-1");
let (_c, mut model) = h.model();
for agent in &mut model.deriver.trees.get_mut(&named).unwrap().agents {
agent.state = AgentState::Live;
}
model.publish();
let err = delete(&mut model, &unused_bl(), &named, "c-1", "").unwrap_err();
assert_eq!(err, "refused \u{2014} live: c-1 \u{2014} stop them first");
assert!(opslog::tail(&h.roots.yog_state, 8).is_empty());
}
#[test]
fn a_leaf_fires_the_bare_verb_and_the_typed_name_arms_the_subtree() {
let _g = spawn_guard();
let mut h = Harness::new();
let named = h.mint_named("alba-koi", "c-1");
let bin = tempdir().unwrap();
let lernie = fake_lernie(
&bin,
"deleted c-1; descendants: 0; pending deposits: 0",
"",
0,
);
let (_c, mut model) = h.model_focused(Some(named.clone()));
model.focus_agent(&named, "c-1");
assert!(model.focused_agent().is_some());
assert!(outcome(delete(&mut model, &lernie, &named, "c-1", "")).ok());
assert_eq!(logged_argv(&bin), format!("delete {} c-1", named.display()));
assert!(
model.focused_agent().is_none(),
"focus never points into the deleted subtree"
);
assert!(outcome(delete(&mut model, &lernie, &named, "c-1", " hi ")).ok());
assert_eq!(
logged_argv(&bin),
format!("delete {} c-1 --children", named.display())
);
let ops = opslog::tail(&h.roots.yog_state, 8);
assert_eq!(ops.len(), 2, "each removal leaves its ops row");
assert!(ops.iter().all(|e| e.exit == 0));
}
#[test]
fn a_declined_verb_rides_back_its_stderr_verbatim() {
let _g = spawn_guard();
let mut h = Harness::new();
let named = h.mint_named("alba-koi", "c-1");
let bin = tempdir().unwrap();
let decline = "agent \"c-1\" has 1 descendant(s); pass --children";
let lernie = fake_lernie(&bin, "", decline, 2);
let (_c, mut model) = h.model();
let ran = outcome(delete(&mut model, &lernie, &named, "c-1", ""));
assert!(!ran.ok(), "a non-zero verb is not a removal");
assert_eq!(
ran.stderr.trim(),
decline,
"the substrate's own words, not a paraphrase"
);
assert_eq!(ran.exit, 2);
let gone = Cli::new(bin.path().join("no-such-lernie"));
let err = delete(&mut model, &gone, &named, "c-1", "").unwrap_err();
assert!(err.contains("No such file"), "{err}");
}
#[test]
fn a_focus_on_a_descendant_clears_with_its_root_and_a_neighbour_survives() {
let _g = spawn_guard();
let mut h = Harness::new();
let named = h.mint_named("alba-koi", "r-aa");
h.last_added().build_agent("r-aa-c-bb", "child");
h.last_added().build_agent("z-zz", "other");
let bin = tempdir().unwrap();
let lernie = fake_lernie(
&bin,
"deleted r-aa; descendants: 1 (r-aa-c-bb); pending deposits: 0",
"",
0,
);
let (_c, mut model) = h.model_focused(Some(named.clone()));
model.focus_agent(&named, "r-aa-c-bb");
assert!(outcome(delete(&mut model, &lernie, &named, "r-aa", "")).ok());
assert!(model.focused_agent().is_none());
model.focus_agent(&named, "z-zz");
assert!(outcome(delete(&mut model, &lernie, &named, "r-aa", "")).ok());
assert_eq!(
model.focused_agent().map(|a| a.agent_id.clone()),
Some("z-zz".to_owned())
);
}