Huginn/crates/mind-body/src/self_run/tests.rs
Meta Vens ffcd2666c5 Restore LF line endings on the files the WIP commits converted to CRLF
The base is LF; the wip commits rewrote whole files as CRLF, hiding the real diff. Content unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:45:37 +01:00

1736 lines
87 KiB
Rust

//! The actuator's tests: a real mind over a temporary directory holds her run,
//! a scripted `Child` plays the stream and a settable clock plays time. The
//! stream lines are built from Claude Code 2.1.268's own records (the two
//! fixture files beside this one), varying only ids and usage numbers. The real
//! `ClaudeProcess` is driven through a fake `claude` script on Unix.
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use chrono::Duration as ChronoDuration;
use eureka_pipeline::{PipelineDocument, PipelineKind as K, PipelineRef, RunOperator, RunTurn, Slug};
use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage};
use super::*;
use cultnet_rs::Selection;
use huginn_mind::PipelinePageItems;
use crate::lease_client::token_holding;
use crate::testkit::*;
/// A cap no constant of the library equals, so that a request of it came from the argument.
const TEST_CAP_S: u64 = 5_400;
const RECORDS: &str = include_str!("claude-2.1.268-assistant-usage-records.jsonl");
const PROBE: &str = include_str!("claude-2.1.268-stream-json-probe.jsonl");
// Pinned against Python's uuid.uuid5(uuid.NAMESPACE_OID, <run id>), not against the code under test.
const SESSION_OF_MIND_RUN: &str = "00cb82a4-c0ac-5ad4-83e2-8e685589fb67";
const SESSION_OF_RUN_A: &str = "d6db8da1-224c-5430-91b5-17aa4df8ae7a";
const SESSION_OF_RUN_B: &str = "546bec88-f7a9-5fa0-bf07-0fada54f071b";
fn foreign_run(id: &str) -> PipelineRef {
PipelineRef { kind: K::Run, id: id.into() }
}
/// The first real assistant record with its id and usage replaced.
fn request(id: &str, input: u64, read: u64, write: u64) -> String {
let mut event: Value = serde_json::from_str(RECORDS.lines().next().unwrap()).unwrap();
event["message"]["id"] = json!(id);
event["message"]["usage"]["input_tokens"] = json!(input);
event["message"]["usage"]["cache_read_input_tokens"] = json!(read);
event["message"]["usage"]["cache_creation_input_tokens"] = json!(write);
event.to_string()
}
/// A request whose whole prefix is `prefix`, under a message id of its own.
fn at(id: &str, prefix: u64) -> String {
request(id, 0, prefix, 0)
}
fn subagent(id: &str, prefix: u64) -> String {
let mut event: Value = serde_json::from_str(&at(id, prefix)).unwrap();
event["parent_tool_use_id"] = json!("toolu_0001");
event.to_string()
}
fn result_line() -> String {
PROBE.lines().nth(2).unwrap().to_string()
}
/// The replay echo of a user line the CLI took into a request.
fn echo() -> String {
json!({"type": "user", "isReplay": true, "uuid": "00000000-0000-4000-8000-000000000001", "message": {"role": "user", "content": "a line"}}).to_string()
}
/// A turn that ended well, with an answer.
fn ok_result() -> String {
json!({"type": "result", "subtype": "success", "is_error": false, "terminal_reason": "completed", "result": "The answer."}).to_string()
}
fn init_line() -> String {
PROBE.lines().next().unwrap().to_string()
}
fn self_run(label: &str, budget: &str) -> PipelineDocument {
let PipelineDocument::Run(mut document) = run(label, RunTurn::SelfRun, RunOperator::Mind, &[]) else { unreachable!() };
document.budget_usd = budget.into();
PipelineDocument::Run(document)
}
use Step::{After, Eof, Exit, Idle, Line as Out};
/// The mind as the actuator sees it, over a `TestMind`: another instance's name,
/// admissions counted, and optionally refused as a mind that cannot be reached would.
struct Seen<'a> {
inner: &'a TestMind,
instance: Slug,
doubled: bool,
admit_fails: bool,
/// A query for this key fails.
query_fails_on: Option<String>,
admits: Cell<usize>,
}
impl<'a> Seen<'a> {
fn of(inner: &'a TestMind) -> Self {
Self { inner, instance: inner.instance().clone(), doubled: false, admit_fails: false, query_fails_on: None, admits: Cell::new(0) }
}
}
impl MindPort for Seen<'_> {
fn instance(&self) -> &Slug {
&self.instance
}
fn query(&self, selection: &Selection) -> Result<PipelineSelectionPage> {
if let (Some(key), Some(keys)) = (&self.query_fails_on, &selection.keys)
&& keys.contains(key)
{
return Err(anyhow!("mind-down-canary"));
}
let mut page = self.inner.query(selection)?;
if self.doubled
&& let PipelinePageItems::Headers(items) = &mut page.items
{
let again = items.clone();
items.extend(again);
}
Ok(page)
}
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
self.admits.set(self.admits.get() + 1);
if self.admit_fails {
return Err(anyhow!("down"));
}
self.inner.admit(agent, session, documents)
}
}
struct Ran {
outcome: Result<SelfRunOutcome>,
out: String,
events: Vec<Event>,
/// Every event, the lease's included.
all: Vec<Event>,
spawns: Vec<Vec<String>>,
admits: usize,
asked: Vec<LeaseRequest>,
}
impl Ran {
fn ended(&self) -> (Cause, Option<i32>, u64, u64) {
match self.outcome.as_ref().unwrap() {
SelfRunOutcome::Ended { cause, code, requests, prefix } => (*cause, *code, *requests, *prefix),
other => panic!("not ended: {other:?}"),
}
}
/// The text of every user line written to the child, in order.
fn written(&self) -> Vec<String> {
self.events
.iter()
.filter_map(|event| match event {
Event::Wrote(line) => {
let line: Value = serde_json::from_str(line).unwrap();
assert_eq!((line["type"].as_str(), line["message"]["role"].as_str()), (Some("user"), Some("user")), "{line}");
Some(line["message"]["content"].as_str().unwrap().to_string())
}
_ => None,
})
.collect()
}
/// What the unit's stdout holds apart from the decisions the actuator journals.
fn lines(&self) -> String {
self.out.lines().filter(|line| !line.starts_with("decision ")).map(|line| format!("{line}\n")).collect()
}
fn count(&self, wanted: &Event) -> usize {
self.events.iter().filter(|event| *event == wanted).count()
}
fn asked(&self) -> Vec<u64> {
self.events
.iter()
.filter_map(|event| match event {
Event::Asked(timeout) => Some(timeout.as_secs()),
_ => None,
})
.collect()
}
}
/// `dies_on_interrupt`: the child ends at the first signal, else it ignores all.
fn carry_with(seen: &Seen, steps: Vec<Step>, code: Option<i32>, dies_on_interrupt: bool) -> Ran {
carry_signalled(seen, steps, code, dies_on_interrupt.then_some(Stop::Interrupt), &[])
}
fn carry_signalled(seen: &Seen, steps: Vec<Step>, code: Option<i32>, dies_on: Option<Stop>, refuses: &[Stop]) -> Ran {
carry_scripted(seen, steps, code, dies_on, refuses, false)
}
fn carry_scripted(seen: &Seen, steps: Vec<Step>, code: Option<i32>, dies_on: Option<Stop>, refuses: &[Stop], hangs: bool) -> Ran {
carry_one(seen, steps, (code, dies_on, refuses, hangs))
}
fn carry_one(seen: &Seen, steps: Vec<Step>, (code, dies_on, refuses, hangs): (Option<i32>, Option<Stop>, &[Stop], bool)) -> Ran {
let log = Rc::new(RefCell::new(Vec::new()));
let clock = Rc::new(Fixed(Cell::new(at_start())));
let fake = Fake::with(Some(Scripted { steps: steps.into(), log: log.clone(), clock: clock.clone(), code, dies_on, refuses: refuses.to_vec(), hangs, exited: false, eof: false, asked: 0 }), false);
let lender = FakeLender::logging(log.clone(), Answer::Grant);
let mut out = Vec::new();
let outcome = carry(seen, &*clock, &lender, &fake, TEST_CAP_S, &mut out);
let all = log.borrow().clone();
// The child's own events; the lease's are in `all`, in order with them.
let events = all.iter().filter(|event| !matches!(event, Event::Lent | Event::Spawned | Event::Abandoned)).cloned().collect();
let spawns = fake.spawns.borrow().clone();
Ran { outcome, out: String::from_utf8(out).unwrap(), events, all, spawns, admits: seen.admits.get(), asked: lender.asked.borrow().clone() }
}
fn at_start() -> chrono::DateTime<chrono::Utc> {
crate::testkit::at(0)
}
/// A mind holding her one open Self run `selfrun-a` with budget 5.
fn with_run() -> TestMind {
let mind = TestMind::seeded();
mind.committed(vec![self_run("selfrun-a", "5")]);
mind
}
fn go(steps: Vec<Step>) -> Ran {
let mind = with_run();
carry_with(&Seen::of(&mind), steps, Some(0), false)
}
type Seed = Box<dyn Fn(&TestMind)>;
fn flushes(ran: &Ran) -> usize {
ran.written().iter().filter(|text| text.starts_with(FLUSH_MESSAGE)).count()
}
// ---- who is carried -----------------------------------------------------
#[test]
fn without_exactly_one_open_self_run_of_hers_it_is_no_run_and_no_child_is_spawned() {
let cases: Vec<(&str, Seed)> = vec![
("no run at all", Box::new(|_| {})),
("a Self run the operator operates", Box::new(|mind| mind.committed(vec![run("op", RunTurn::SelfRun, RunOperator::Operator, &[])]))),
("her Persona turn", Box::new(|mind| mind.committed(vec![run("persona", RunTurn::PersonaTurn, RunOperator::Mind, &[])]))),
(
"a Self run of hers that is closed",
Box::new(|mind| {
mind.committed(vec![self_run("selfrun-a", "5")]);
mind.committed(vec![close(run_ref("selfrun-a"), recorded())]);
}),
),
];
for (name, seed) in cases {
let mind = TestMind::seeded();
seed(&mind);
let ran = carry_with(&Seen::of(&mind), vec![Out(result_line())], Some(0), false);
assert_eq!(ran.outcome.unwrap(), SelfRunOutcome::NoRun, "{name}");
assert!(ran.spawns.is_empty() && ran.events.is_empty() && ran.out.is_empty(), "{name}");
}
// Another instance's mind holds none of this instance's runs.
let mind = with_run();
let mut other = Seen::of(&mind);
other.instance = Slug("another".into());
assert_eq!(carry_with(&other, vec![], Some(0), false).outcome.unwrap(), SelfRunOutcome::NoRun);
// Two open runs are not one: nothing is spawned for either.
let mut two = Seen::of(&mind);
two.doubled = true;
let ran = carry_with(&two, vec![], Some(0), false);
assert_eq!(ran.outcome.unwrap(), SelfRunOutcome::NoRun);
assert!(ran.spawns.is_empty());
}
#[test]
fn a_mind_that_cannot_be_read_or_a_child_that_cannot_start_is_an_error_not_an_outcome() {
let mind = with_run();
let clock = Rc::new(Fixed(Cell::new(at_start())));
let refusing = Fake::with(None, true);
assert!(carry(&Seen::of(&mind), &*clock, &FakeLender::new(Answer::Grant), &refusing, TEST_CAP_S, &mut Vec::new()).is_err());
assert_eq!(refusing.spawns.borrow().len(), 1, "it tried exactly once");
struct Down;
impl MindPort for Down {
fn instance(&self) -> &Slug {
static INSTANCE: std::sync::OnceLock<Slug> = std::sync::OnceLock::new();
INSTANCE.get_or_init(|| Slug("yggdrasil".into()))
}
fn query(&self, _selection: &Selection) -> Result<PipelineSelectionPage> {
Err(anyhow!("down"))
}
fn admit(&self, _agent: &str, _session: &str, _documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
unreachable!()
}
}
let never = Fake::with(None, false);
assert!(carry(&Down, &*clock, &FakeLender::new(Answer::Grant), &never, TEST_CAP_S, &mut Vec::new()).is_err());
assert!(never.spawns.borrow().is_empty());
}
// ---- what is started ------------------------------------------------------
#[test]
fn claude_args_is_exactly_the_fixed_list_for_a_session() {
let session = Uuid::parse_str(SESSION_OF_RUN_A).unwrap();
let args = claude_args(session);
assert_eq!(
args,
[
"-p",
"--input-format",
"stream-json",
"--output-format",
"stream-json",
"--verbose",
"--session-id",
SESSION_OF_RUN_A,
"--max-turns",
"500",
"--permission-mode",
"bypassPermissions",
"--permission-prompts",
"none",
"--setting-sources",
"user",
"--replay-user-messages",
]
);
// Those belong to the run measure or to the unit's managed settings.
for forbidden in ["--resume", "--continue", "--fork-session", "--model", "--dangerously-skip-permissions", "--system-prompt", "--max-budget"] {
assert!(!args.iter().any(|arg| arg.starts_with(forbidden) || arg.contains("dangerously")), "{forbidden}");
}
// Print mode starts in Manual without a mode, and prompts none then denies.
let mode = args.iter().position(|arg| arg == "--permission-mode").expect("mode flag");
assert_eq!(args[mode + 1], "bypassPermissions");
assert_eq!(MAX_TURNS, 500);
}
#[test]
fn the_session_id_is_uuid_v5_of_the_run_id_and_differs_per_run() {
assert_eq!(session_id(&foreign_run("eureka:run:mind-20261022T120000000Z")).to_string(), SESSION_OF_MIND_RUN);
assert_eq!(session_id(&run_ref("selfrun-a")).to_string(), SESSION_OF_RUN_A);
assert_eq!(session_id(&run_ref("selfrun-b")).to_string(), SESSION_OF_RUN_B);
}
#[test]
fn the_child_is_started_for_her_run_with_the_fixed_list_whatever_the_run_record_says() {
// The run's budget field is a dead record; nothing in what is started depends on it.
for budget in ["0", "0.5", "10", "100"] {
let mind = TestMind::seeded();
mind.committed(vec![self_run("selfrun-b", budget)]);
let ran = carry_with(&Seen::of(&mind), vec![Out(result_line())], Some(0), false);
assert_eq!(ran.spawns.len(), 1, "{budget}");
assert_eq!(ran.spawns[0], claude_args(Uuid::parse_str(SESSION_OF_RUN_B).unwrap()), "{budget}");
}
}
#[test]
fn the_start_message_is_the_first_thing_written_and_names_the_skill_the_instance_and_the_run() {
let ran = go(vec![Out(result_line())]);
assert!(matches!(&ran.events[0], Event::Wrote(_)), "the start precedes the first read: {:?}", ran.events[0]);
let written = ran.written();
for needle in ["Plan usage: five_hour 42.3% allowed, resets at unix ms 1793307000000", "about 90% of the weekly allowance", "nothing enforces it"] {
assert!(written[0].contains(needle), "the lease grant plan windows reach the child: {needle}: {}", written[0]);
}
for needle in ["Use the eureka skill.", "yggdrasil", "yggdrasil:run:selfrun-a", AGENT] {
assert!(written[0].contains(needle), "{needle}: {}", written[0]);
}
assert_eq!(written.len(), 1, "no flush without the ceiling");
}
#[test]
fn the_start_message_opens_with_the_skill_the_seed_ships() {
let message = start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a"), &[plan_window()], 1_793_306_100_000);
assert!(message.starts_with("Use the eureka skill. You are Self for the instance `yggdrasil`;"), "{message}");
assert!(!message.contains("mind-self skill"), "{message}");
}
#[cfg(unix)]
mod seed_links {
use super::*;
use std::os::unix::fs::symlink;
#[test]
fn a_relative_in_tree_link_is_copied_as_a_link_that_resolves() {
let seed = tempfile::tempdir().unwrap();
std::fs::create_dir_all(seed.path().join("skills/eureka/agents")).unwrap();
std::fs::write(seed.path().join("skills/eureka/agents/hands.md"), "hands").unwrap();
symlink("skills/eureka/agents", seed.path().join("agents")).unwrap();
std::fs::create_dir(seed.path().join("sub")).unwrap();
symlink("../skills/eureka", seed.path().join("sub/eureka")).unwrap();
let out = tempfile::tempdir().unwrap();
copy_tree(seed.path(), out.path()).unwrap();
assert!(out.path().join("agents").symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(std::fs::read_link(out.path().join("agents")).unwrap(), Path::new("skills/eureka/agents"));
assert_eq!(std::fs::read_to_string(out.path().join("agents/hands.md")).unwrap(), "hands");
assert_eq!(std::fs::read_to_string(out.path().join("sub/eureka/agents/hands.md")).unwrap(), "hands");
}
#[test]
fn an_absolute_or_escaping_link_is_refused_with_the_typed_error() {
for target in ["/etc/passwd", "../outside", "a/../../outside", "missing"] {
let seed = tempfile::tempdir().unwrap();
symlink(target, seed.path().join("bad")).unwrap();
let out = tempfile::tempdir().unwrap();
let error = copy_tree(seed.path(), out.path()).unwrap_err();
let refused = error.get_ref().and_then(|inner| inner.downcast_ref::<SeedLinkRefused>());
assert_eq!(refused.map(|r| r.target.as_path()), Some(Path::new(target)), "{target}: {error}");
assert!(out.path().join("bad").symlink_metadata().is_err(), "{target} was copied");
}
}
#[test]
fn a_link_through_another_in_tree_link_cannot_escape() {
let parent = tempfile::tempdir().unwrap();
let seed = parent.path().join("seed");
std::fs::create_dir(&seed).unwrap();
symlink(".", seed.join("c")).unwrap();
symlink("c/..", seed.join("a")).unwrap();
let out = tempfile::tempdir().unwrap();
let error = copy_tree(&seed, out.path()).unwrap_err();
let refused = error.get_ref().and_then(|inner| inner.downcast_ref::<SeedLinkRefused>());
assert_eq!(refused.map(|r| r.target.as_path()), Some(Path::new("c/..")), "{error}");
}
/// A seed whose parent is not the config dir's parent, and a sibling of the
/// config dir named `seed` holding the same file name: the copy's link text
/// resolves there, though in the source it lands under the seed root.
fn refused_in_the_copy(plant: impl Fn(&Path), expected: &str) {
let source = tempfile::tempdir().unwrap();
let seed = source.path().join("seed");
std::fs::create_dir(&seed).unwrap();
std::fs::write(seed.join("f"), "seed").unwrap();
plant(&seed);
let runtime = tempfile::tempdir().unwrap();
std::fs::create_dir(runtime.path().join("seed")).unwrap();
std::fs::write(runtime.path().join("seed/f"), "planted").unwrap();
let out = runtime.path().join("claude-config");
std::fs::create_dir(&out).unwrap();
let error = copy_tree(&seed, &out).unwrap_err();
let refused = error.get_ref().and_then(|inner| inner.downcast_ref::<SeedLinkRefused>());
assert_eq!(refused.map(|r| r.target.as_path()), Some(Path::new(expected)), "{error}");
}
#[test]
fn a_link_that_leaves_the_seed_and_returns_by_its_name_is_refused() {
refused_in_the_copy(|seed| symlink("../seed/f", seed.join("x")).unwrap(), "../seed/f");
}
#[test]
fn a_link_through_a_dot_link_to_the_seeds_name_is_refused() {
refused_in_the_copy(
|seed| {
symlink(".", seed.join("c")).unwrap();
symlink("c/../seed/f", seed.join("a")).unwrap();
},
"c/../seed/f",
);
}
}
#[test]
fn a_result_events_text_reaches_the_journal_with_its_subtype_and_error_flag() {
let line = json!({"type": "result", "subtype": "success", "is_error": false, "result": "all done"}).to_string();
let ran = go(vec![Out(line)]);
assert!(ran.out.contains("result subtype=success is_error=false: all done\n"), "{}", ran.out);
}
#[test]
fn a_long_result_is_cut_at_the_limit_on_a_character_boundary_and_marked() {
// One ASCII byte first, so the limit falls in the middle of a two-byte character.
let text = format!("a{}", "é".repeat(RESULT_TEXT_LIMIT));
let line = json!({"type": "result", "result": text}).to_string();
let ran = go(vec![Out(line)]);
let written = ran.out.lines().find(|l| l.starts_with("result")).unwrap();
assert_eq!(written, format!("result: a{}", "é".repeat(RESULT_TEXT_LIMIT / 2 - 1)));
assert!(ran.out.contains("\n[truncated]\n"), "{}", ran.out);
}
// ---- what is read -----------------------------------------------------------
#[test]
fn read_usage_sums_input_cache_read_and_cache_write_and_reads_absent_fields_as_zero() {
assert_eq!(read_usage(&request("m1", 1, 20, 300)), Usage::Request { id: "m1".into(), prefix: 321 });
// Each term alone, so dropping or doubling any one is seen.
assert_eq!(read_usage(&request("m", 7, 0, 0)), Usage::Request { id: "m".into(), prefix: 7 });
assert_eq!(read_usage(&request("m", 0, 7, 0)), Usage::Request { id: "m".into(), prefix: 7 });
assert_eq!(read_usage(&request("m", 0, 0, 7)), Usage::Request { id: "m".into(), prefix: 7 });
for absent in ["input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"] {
let mut event: Value = serde_json::from_str(&request("m", 1, 20, 300)).unwrap();
event["message"]["usage"].as_object_mut().unwrap().remove(absent);
let want = 321 - [("input_tokens", 1), ("cache_read_input_tokens", 20), ("cache_creation_input_tokens", 300)].iter().find(|(name, _)| *name == absent).unwrap().1;
assert_eq!(read_usage(&event.to_string()), Usage::Request { id: "m".into(), prefix: want }, "{absent} absent");
}
let mut no_usage: Value = serde_json::from_str(&request("m", 1, 1, 1)).unwrap();
no_usage["message"].as_object_mut().unwrap().remove("usage");
assert_eq!(read_usage(&no_usage.to_string()), Usage::Request { id: "m".into(), prefix: 0 });
}
#[test]
fn read_usage_classifies_the_owners_real_records() {
let sums = [84210, 100751, 117174, 118336, 126746, 127250, 129853];
let mut seen = Vec::new();
for line in RECORDS.lines() {
let Usage::Request { id, prefix } = read_usage(line) else { panic!("an assistant record of the main thread is a request: {line}") };
if !seen.iter().any(|(known, _)| *known == id) {
seen.push((id, prefix));
}
}
assert_eq!(seen.iter().map(|(_, prefix)| *prefix).collect::<Vec<_>>(), sums);
assert_eq!(read_usage(&result_line()), Usage::Result { ok: false, blocked: false });
assert_eq!(read_usage(&ok_result()), Usage::Result { ok: true, blocked: false });
let blocked = json!({"type": "result", "subtype": "success", "is_error": true, "terminal_reason": "blocking_limit", "result": "Prompt is too long"}).to_string();
assert_eq!(read_usage(&blocked), Usage::Result { ok: false, blocked: true });
// An echo is a main-thread user line the CLI replayed, with string content; nothing else is.
assert_eq!(read_usage(&echo()), Usage::Echo);
let tool_result = json!({"type": "user", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t", "content": "x"}]}}).to_string();
assert_eq!(read_usage(&tool_result), Usage::Other, "a tool result is not one of our lines");
let not_replayed = json!({"type": "user", "isReplay": false, "message": {"role": "user", "content": "a line"}}).to_string();
assert_eq!(read_usage(&not_replayed), Usage::Other);
let subagent_echo = json!({"type": "user", "isReplay": true, "parent_tool_use_id": "toolu_1", "message": {"role": "user", "content": "a line"}}).to_string();
assert_eq!(read_usage(&subagent_echo), Usage::Other, "a subagent's line is not ours");
assert_eq!(read_usage(&init_line()), Usage::Other);
assert_eq!(read_usage(&subagent("m", 5)), Usage::Other, "a subagent's request is not the main thread's");
let mut no_id: Value = serde_json::from_str(&at("m", 5)).unwrap();
no_id["message"].as_object_mut().unwrap().remove("id");
assert_eq!(read_usage(&no_id.to_string()), Usage::Other);
let mut null_parent: Value = serde_json::from_str(&at("m", 5)).unwrap();
null_parent["parent_tool_use_id"] = Value::Null;
assert_eq!(read_usage(&null_parent.to_string()), Usage::Request { id: "m".into(), prefix: 5 });
let mut no_parent: Value = serde_json::from_str(&at("m", 5)).unwrap();
no_parent.as_object_mut().unwrap().remove("parent_tool_use_id");
assert_eq!(read_usage(&no_parent.to_string()), Usage::Request { id: "m".into(), prefix: 5 });
for unreadable in ["", "not json", "[1,2]", "\"assistant\"", "12", "null", "{\"type\":"] {
assert_eq!(read_usage(unreadable), Usage::Unreadable, "{unreadable:?}");
}
}
#[test]
fn records_that_share_a_message_id_count_once_and_subagent_records_never_move_the_prefix() {
let mut steps: Vec<Step> = RECORDS.lines().map(|line| Out(line.to_string())).collect();
steps.insert(3, Out(subagent("msg_sub", 999_999)));
let ran = go(steps);
assert_eq!(
ran.out,
"request 1 prefix 84210
request 2 prefix 100751
request 3 prefix 117174
request 4 prefix 118336
request 5 prefix 126746
request 6 prefix 127250
request 7 prefix 129853
"
);
let (cause, _, requests, prefix) = ran.ended();
assert_eq!((cause, requests, prefix), (Cause::Own, 7, 129_853), "the newest prefix, 129853, not a subagent's 999999");
assert_eq!(flushes(&ran), 0);
}
#[test]
fn the_prefix_is_the_newest_requests_not_the_largest() {
let ran = go(vec![Out(at("m1", 200_000)), Out(at("m2", 150_000))]);
assert_eq!(ran.ended().3, 150_000);
}
// ---- the flush --------------------------------------------------------------
#[test]
fn below_the_ceiling_no_flush_is_written_and_at_it_exactly_one() {
let below = go(vec![Out(at("m1", CEILING_TOKENS - 1))]);
assert_eq!(flushes(&below), 0);
assert_eq!(below.ended().0, Cause::Own);
let at_it = go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS - 1)), Out(at("m2", CEILING_TOKENS)), Out(at("m3", CEILING_TOKENS + 10_000))]);
assert_eq!(flushes(&at_it), 1, "{:?}", at_it.written());
assert_eq!(at_it.written().len(), 2, "the start and the flush");
assert_eq!(at_it.ended().0, Cause::FlushRequested);
assert_eq!(CEILING_TOKENS, 300_000);
}
#[test]
fn a_closed_input_is_never_asked_for_a_flush_whatever_the_prefix_grows_to() {
// An error before any answer closes the input; a request that comes after it asks nothing.
let ran = go(vec![Out(echo()), Out(result_line()), Out(at("m2", CEILING_TOKENS + 1))]);
assert_eq!(ran.count(&Event::InputClosed), 1);
assert_eq!(flushes(&ran), 0);
assert_eq!(ran.ended().0, Cause::Own);
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 0);
}
#[test]
fn stdin_closes_once_at_the_result_after_the_flushs_echo_folded_or_queued_and_nothing_is_written_after() {
let writes_before_close = |ran: &Ran| {
assert_eq!(ran.count(&Event::InputClosed), 1, "{:?}", ran.events);
let closed_at = ran.events.iter().position(|event| *event == Event::InputClosed).unwrap();
assert_eq!(ran.events[..closed_at].iter().filter(|event| matches!(event, Event::Wrote(_))).count(), 2, "start and flush before the close");
assert!(ran.events[closed_at..].iter().all(|event| !matches!(event, Event::Wrote(_))));
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 0, "never a grace interrupt");
};
// Folded: the flush lands in the running turn; its one result answers it.
writes_before_close(&go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(echo()), Out(at("m2", 11)), Out(ok_result())]));
// Queued: the first result ends the turn it was queued behind; the second is the flush's own.
writes_before_close(&go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(ok_result()), Out(echo()), Out(at("m2", 11)), Out(ok_result())]));
// The first result of a queued flush closes nothing: the flush is still to have its turn.
let one = go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(ok_result())]);
assert_eq!(one.count(&Event::InputClosed), 0, "{:?}", one.events);
let never = go(vec![Out(at("m1", 10))]);
assert_eq!(never.count(&Event::InputClosed), 0, "no result, no close");
}
// ---- the interrupt ------------------------------------------------------------
#[test]
fn after_the_flush_the_grace_interrupts_exactly_once_and_not_a_second_early() {
let grace = FLUSH_GRACE_S;
// Just short of the grace: no interrupt, and the child ended on its own.
let early = go(vec![Out(at("m1", CEILING_TOKENS)), Idle(grace - 1)]);
assert_eq!(early.count(&Event::Stopped(Stop::Interrupt)), 0);
assert_eq!(early.ended().0, Cause::FlushRequested);
// At the grace, and long past it: one interrupt.
let on_time = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Idle(grace), Idle(grace), Idle(grace)], Some(130), true);
assert_eq!(on_time.count(&Event::Stopped(Stop::Interrupt)), 1);
assert_eq!(on_time.ended().0, Cause::Signalled(Stop::Interrupt));
// Exactly at the grace is the grace: the interrupt comes then, not on the next wait.
let exact = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Idle(grace)], Some(130), true);
assert_eq!(exact.ended().0, Cause::Signalled(Stop::Interrupt));
// A child that ignores it is interrupted once and still waited for.
let stubborn = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Idle(grace), Idle(grace), Idle(grace)], Some(0), false);
assert_eq!(stubborn.count(&Event::Stopped(Stop::Interrupt)), 1);
assert_eq!(stubborn.ended().1, Some(0), "its own end is waited for");
assert_eq!(FLUSH_GRACE_S, 900);
}
#[test]
fn the_wait_for_the_child_never_passes_the_grace_deadline_and_never_spins_after_the_interrupt() {
let ran = go(vec![Out(at("m1", CEILING_TOKENS)), Idle(840), Idle(59), Idle(1)]);
// The inbox poll's 30 until the grace is near, the one second left, then the escalation's own wait, then the drain after the child's exit.
assert_eq!(ran.asked(), [30, 30, 30, 1, 29, 2], "{:?}", ran.asked());
}
#[test]
fn the_headroom_counts_the_requests_after_the_first_result_that_follows_the_flush_and_one_short_of_it_does_not_interrupt() {
let hard = CEILING_TOKENS + FLUSH_HEADROOM_TOKENS;
let short = go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(ok_result()), Out(echo()), Out(at("m2", hard - 1))]);
assert_eq!(short.count(&Event::Stopped(Stop::Interrupt)), 0);
assert_eq!(short.ended().0, Cause::FlushRequested);
let at_hard = carry_with(&Seen::of(&with_run()), vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(ok_result()), Out(echo()), Out(at("m2", hard)), Out(at("m3", hard + 1))], Some(130), true);
assert_eq!(at_hard.count(&Event::Stopped(Stop::Interrupt)), 1);
assert_eq!(at_hard.ended().0, Cause::Signalled(Stop::Interrupt));
assert_eq!(FLUSH_HEADROOM_TOKENS, 60_000);
// Before the flush's echo the running turn's own requests are not the flush's turn: however far they grow, no interrupt.
let same_turn = go(vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(at("m2", hard)), Out(at("m3", hard + 500_000))]);
assert_eq!(same_turn.count(&Event::Stopped(Stop::Interrupt)), 0);
}
#[test]
fn a_jump_past_the_ceiling_plus_the_headroom_writes_the_flush_and_the_running_turn_cannot_use_up_the_flushs_turn() {
let jump = carry_with(
&Seen::of(&with_run()),
vec![Out(echo()), Out(at("m1", 290_000)), Out(at("m2", 400_000)), Out(at("m3", 470_000)), Out(ok_result()), Out(echo()), Out(at("m4", 459_999)), Out(at("m5", 460_000)), Out(at("m6", 470_000))],
Some(130),
true,
);
let kinds: Vec<&str> = jump
.events
.iter()
.filter_map(|event| match event {
Event::Wrote(line) if line.contains("context ceiling") => Some("flush"),
Event::Stopped(_) => Some("interrupt"),
Event::InputClosed => Some("closed"),
Event::Asked(_) => Some("read"),
_ => None,
})
.collect();
// Reads: the start echo, m1, m2 (the jump, which writes the flush), then m3 (the running turn's 470k, past the headroom: no interrupt).
assert_eq!(kinds[..6], ["read", "read", "read", "flush", "read", "read"], "{kinds:?}");
// The headroom is measured from the prefix the flush was asked at (400k) and counts the requests after the
// flush's echo only: 459_999 does not interrupt; 460_000 does, once, and m6 is never read.
assert_eq!(jump.count(&Event::Stopped(Stop::Interrupt)), 1, "{kinds:?}");
assert_eq!(jump.lines(), "request 1 prefix 290000
request 2 prefix 400000
request 3 prefix 470000
result subtype=success is_error=false: The answer.
request 4 prefix 459999
request 5 prefix 460000
");
assert_eq!(jump.ended().0, Cause::Signalled(Stop::Interrupt));
assert_eq!(flushes(&jump), 1);
}
#[test]
fn a_child_that_ends_before_the_grace_is_never_interrupted() {
let ran = go(vec![Out(at("m1", CEILING_TOKENS)), Out(result_line())]);
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 0);
assert_eq!(ran.ended().0, Cause::FlushRequested);
}
#[test]
fn the_grace_is_the_full_grace_from_the_moment_the_flush_was_asked_whichever_request_that_was() {
let grace = FLUSH_GRACE_S;
// Five requests, time passing before the ceiling, then the flush on the sixth.
let before: Vec<Step> = vec![Out(echo()), Out(at("m1", 10)), Idle(500), Out(at("m2", 20)), Out(at("m3", 30)), Out(at("m4", 40)), Out(at("m5", 50)), Out(at("m6", CEILING_TOKENS))];
let early = carry_with(&Seen::of(&with_run()), [before.clone(), vec![Idle(grace - 1)]].concat(), Some(0), true);
assert_eq!((flushes(&early), early.count(&Event::Stopped(Stop::Interrupt))), (1, 0), "one second short of the grace, flush on request 6");
let on_time = carry_with(&Seen::of(&with_run()), [before, vec![Idle(grace - 1), Idle(1)]].concat(), Some(0), true);
assert_eq!(on_time.count(&Event::Stopped(Stop::Interrupt)), 1, "the grace is the same after six requests as after one");
}
#[test]
fn the_message_ids_of_a_long_run_are_all_remembered() {
let ids: Vec<String> = (1..=12).map(|n| format!("m{n}")).collect();
let mut steps: Vec<Step> = ids.iter().map(|id| Out(at(id, 1_000))).collect();
// Every earlier request seen again, the oldest and the newest among them: none counts twice.
steps.extend(ids.iter().map(|id| Out(at(id, 2_000))));
let ran = go(steps);
let (_, _, requests, prefix) = ran.ended();
assert_eq!((requests, prefix), (12, 1_000));
assert_eq!(ran.out.lines().filter(|line| line.starts_with("request ")).count(), 12);
}
#[test]
fn a_usage_sum_that_overflows_is_at_or_over_the_ceiling_not_a_panic_or_a_wrap() {
let line = request("m", u64::MAX, 1, 0);
assert_eq!(read_usage(&line), Usage::Request { id: "m".into(), prefix: u64::MAX });
assert_eq!(read_usage(&request("m", u64::MAX, u64::MAX, u64::MAX)), Usage::Request { id: "m".into(), prefix: u64::MAX });
assert_eq!(read_usage(&request("m", 1, u64::MAX, 1)), Usage::Request { id: "m".into(), prefix: u64::MAX });
// Through the actuator: the flush is asked for, and the headroom beyond a saturated prefix does not overflow
// either, and the pass that wrote the flush does not interrupt.
let ran = go(vec![Out(echo()), Out(request("m1", u64::MAX, 7, 0)), Out(request("m2", 0, 0, 0))]);
assert_eq!(flushes(&ran), 1);
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 0);
}
#[test]
fn an_error_result_before_any_answer_closes_the_input_at_once_and_the_run_ends_with_a_typed_outcome() {
// The shape of an API or authentication failure: an error result before any answer. A flush would fail the same way.
let ran = carry_with(&Seen::of(&with_run()), vec![Out(echo()), Out(result_line())], Some(1), false);
assert_eq!(ran.count(&Event::InputClosed), 1);
assert_eq!(flushes(&ran), 0);
assert_eq!(ran.ended(), (Cause::Own, Some(1), 0, 0));
assert_eq!(ran.outcome.as_ref().unwrap().exit_status(), 1);
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 0);
}
#[test]
fn without_a_flush_nothing_is_ever_signalled_however_large_the_prefix_grows() {
// An error before any answer closed the input: a request far over the ceiling asks for no flush and nothing is signalled.
let ran = go(vec![Out(echo()), Out(result_line()), Out(at("m2", 900_000)), Idle(100_000)]);
assert_eq!(ran.events.iter().filter(|event| matches!(event, Event::Stopped(_))).count(), 0);
assert_eq!(ran.ended().0, Cause::Own);
}
#[test]
fn a_child_that_ignores_a_signal_gets_the_next_one_after_the_bound_and_the_outcome_names_the_one_that_ended_it() {
let hung = |dies_on: Option<Stop>, refuses: &[Stop], rest: Vec<Step>| {
carry_signalled(&Seen::of(&with_run()), [vec![Out(at("m1", CEILING_TOKENS)), Idle(FLUSH_GRACE_S)], rest].concat(), Some(0), dies_on, refuses)
};
let stops = |ran: &Ran| -> Vec<Event> { ran.events.iter().filter(|event| matches!(event, Event::Stopped(_))).cloned().collect() };
let after = ESCALATE_AFTER_S;
// Ignores SIGINT and SIGTERM: each next signal waits out the bound, SIGKILL ends it.
let killed = hung(Some(Stop::Kill), &[], vec![Idle(after - 1), Idle(1), Idle(after - 1), Idle(1), Idle(after)]);
assert_eq!(stops(&killed), [Event::Stopped(Stop::Interrupt), Event::Stopped(Stop::Terminate), Event::Stopped(Stop::Kill)]);
assert_eq!(killed.ended().0, Cause::Signalled(Stop::Kill));
// One second short of the bound there is no second signal.
let waiting = hung(None, &[], vec![Idle(after - 1)]);
assert_eq!(stops(&waiting), [Event::Stopped(Stop::Interrupt)]);
// SIGTERM ends it: the outcome says terminated, and nothing stronger is sent.
let terminated = hung(Some(Stop::Terminate), &[], vec![Idle(after), Idle(after)]);
assert_eq!(stops(&terminated), [Event::Stopped(Stop::Interrupt), Event::Stopped(Stop::Terminate)]);
assert_eq!(terminated.outcome.as_ref().unwrap().to_string(), "ended: terminated code 0 requests 1 prefix 300000");
}
#[test]
fn a_signal_that_could_not_be_delivered_is_never_reported_as_sent() {
let hung = |refuses: &[Stop], rest: Vec<Step>| {
carry_signalled(&Seen::of(&with_run()), [vec![Out(at("m1", CEILING_TOKENS)), Idle(FLUSH_GRACE_S)], rest].concat(), Some(0), None, refuses)
};
// SIGINT is refused and the child ends by itself afterwards: the flush was requested, nothing interrupted it.
let refused = hung(&[Stop::Interrupt], vec![]);
assert_eq!(refused.count(&Event::Stopped(Stop::Interrupt)), 1, "it was attempted");
assert_eq!(refused.ended().0, Cause::FlushRequested);
// SIGINT delivered but ignored, SIGTERM refused, the child ends on its own: the strongest delivered is SIGINT.
let partly = hung(&[Stop::Terminate], vec![Idle(ESCALATE_AFTER_S)]);
assert_eq!(partly.ended().0, Cause::Signalled(Stop::Interrupt));
// Not even SIGKILL is delivered and the child never ends: after the bound the run is over without it, and the label says so.
let stuck = carry_scripted(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Idle(FLUSH_GRACE_S)], Some(0), None, &[Stop::Interrupt, Stop::Terminate, Stop::Kill], true);
assert_eq!(stuck.ended(), (Cause::Unstoppable, None, 1, CEILING_TOKENS));
assert_eq!(stuck.events.iter().filter(|event| matches!(event, Event::Stopped(_))).count(), 3);
assert_eq!(stuck.outcome.unwrap().exit_status(), 1);
}
/// A clock that counts its reads.
struct Counting(Fixed, Cell<u32>);
impl Clock for Counting {
fn now(&self) -> chrono::DateTime<chrono::Utc> {
self.1.set(self.1.get() + 1);
self.0.now()
}
}
#[test]
fn the_clock_is_read_once_per_event_so_a_step_of_the_wall_clock_cannot_split_a_pass() {
let mind = with_run();
let log = Rc::new(RefCell::new(Vec::new()));
let clock = Rc::new(Counting(Fixed(Cell::new(at_start())), Cell::new(0)));
// The child's reads move the clock by far more than the grace, as a stepped wall clock would.
struct Stepped(Rc<Counting>);
impl Clock for Stepped {
fn now(&self) -> chrono::DateTime<chrono::Utc> {
let now = self.0.now();
self.0.0.0.set(now + ChronoDuration::seconds(FLUSH_GRACE_S + 1));
now
}
}
let steps: VecDeque<Step> = vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(at("m2", CEILING_TOKENS + 1)), Idle(0)].into();
let child = Scripted { steps, log: log.clone(), clock: Rc::new(Fixed(Cell::new(at_start()))), code: Some(0), dies_on: None, refuses: vec![], hangs: false, exited: false, eof: false, asked: 0 };
let fake = Fake::with(Some(child), false);
let outcome = carry(&Seen::of(&mind), &Stepped(clock.clone()), &FakeLender::new(Answer::Grant), &fake, TEST_CAP_S, &mut Vec::new()).unwrap();
let events = log.borrow().clone();
let reads = events.iter().filter(|event| matches!(event, Event::Asked(_))).count() as u32;
// One read for the lease's expiry, one before the first wait, then one per fact heard, and one for the closure:
// never a second read inside a pass.
assert_eq!(clock.1.get(), 3 + reads, "{events:?}");
// The flush was written on the first request at time 0; the very next pass sees the stepped clock.
// The flush pass itself has no interrupt, whatever the step: the interrupt is on a later event.
let flush_at = events.iter().position(|event| matches!(event, Event::Wrote(line) if line.contains("context ceiling"))).unwrap();
let stop_at = events.iter().position(|event| matches!(event, Event::Stopped(_))).unwrap();
assert!(events[flush_at..stop_at].iter().any(|event| matches!(event, Event::Asked(_))), "a read between the flush and the interrupt: {events:?}");
assert_eq!(outcome.exit_status(), 0);
}
#[test]
fn a_deadline_that_came_during_a_line_is_answered_without_waiting_for_the_next_poll() {
// The result arrives after the grace has run out: the input closes, and the interrupt is due at once.
let ran = carry_scripted(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Step::After(FLUSH_GRACE_S + 5, ok_result())], Some(130), Some(Stop::Interrupt), &[], true);
assert_eq!(ran.count(&Event::Stopped(Stop::Interrupt)), 1);
assert_eq!(ran.count(&Event::InputClosed), 0, "the flush is queued behind the turn that just ended: its echo has not come");
let stop_at = ran.events.iter().position(|event| matches!(event, Event::Stopped(_))).unwrap();
let reads_before = ran.events[..stop_at].iter().filter(|event| matches!(event, Event::Asked(_))).count();
assert_eq!(reads_before, 2, "the interrupt is in the pass that heard the result, not a poll away: {:?}", ran.events);
}
#[test]
fn a_child_that_exited_is_never_signalled_however_long_its_stream_stays_open() {
// Flush asked for, the child exits, then the grace and more pass with its stream still open.
let ran = carry_scripted(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Exit(Some(0)), Idle(1)], Some(0), None, &[], true);
assert_eq!(ran.events.iter().filter(|event| matches!(event, Event::Stopped(_))).count(), 0);
assert_eq!(ran.ended(), (Cause::FlushRequested, Some(0), 1, CEILING_TOKENS));
// The stream ending first and the exit after: the same end.
let eof_first = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", 10)), Eof], Some(0), false);
assert_eq!(eof_first.ended(), (Cause::Own, Some(0), 1, 10));
}
// ---- how it ends --------------------------------------------------------------
#[test]
fn the_outcome_names_how_it_ended_and_the_exit_status_follows_the_childs_code() {
let own = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", 10)), Out(result_line())], Some(0), false);
assert_eq!(own.outcome.as_ref().unwrap(), &SelfRunOutcome::Ended { cause: Cause::Own, code: Some(0), requests: 1, prefix: 10 });
assert_eq!(own.outcome.as_ref().unwrap().to_string(), "ended: own code 0 requests 1 prefix 10");
assert_eq!(own.outcome.unwrap().exit_status(), 0);
let flushed = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Out(result_line())], Some(0), false);
assert_eq!(flushed.outcome.as_ref().unwrap().to_string(), "ended: flush-requested code 0 requests 1 prefix 300000");
assert_eq!(flushed.outcome.unwrap().exit_status(), 0);
let nonzero = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", 10))], Some(2), false);
assert_eq!(nonzero.outcome.as_ref().unwrap().to_string(), "ended: own code 2 requests 1 prefix 10");
assert_eq!(nonzero.outcome.unwrap().exit_status(), 1);
let signalled = carry_with(&Seen::of(&with_run()), vec![Out(at("m1", CEILING_TOKENS)), Idle(FLUSH_GRACE_S)], None, true);
assert_eq!(signalled.outcome.as_ref().unwrap().to_string(), "ended: interrupted signal requests 1 prefix 300000");
assert_eq!(signalled.outcome.unwrap().exit_status(), 1, "a child killed by a signal has no code");
assert_eq!(SelfRunOutcome::NoRun.exit_status(), 3);
assert_eq!(SelfRunOutcome::NoRun.to_string(), "no run: she has no single open Self run");
}
// ---- the real child -----------------------------------------------------------
#[test]
fn unreadable_and_foreign_lines_are_skipped_and_never_printed() {
let canary = "canary-stream-line-8c41";
let mut foreign: Value = serde_json::from_str(&init_line()).unwrap();
foreign["cwd"] = json!(canary);
foreign["type"] = json!(canary);
let ran = go(vec![
Out(format!("not json {canary}")),
Out(foreign.to_string()),
Out(format!("[\"{canary}\"]")),
Out(at("m1", 10)),
Out(result_line()),
]);
assert_eq!(ran.lines(), "request 1 prefix 10
result subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed
");
let outcome = ran.outcome.as_ref().unwrap().to_string();
assert!(!ran.out.contains(canary) && !outcome.contains(canary));
assert!(!ran.written().iter().any(|text| text.contains(canary)), "nothing from the stream is written back");
}
#[cfg(unix)]
mod process {
mod lease_child;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use super::*;
use crate::launch::SystemClock;
/// A fake `claude`: the shell script `body`, in a directory that outlives the child.
fn script(dir: &tempfile::TempDir, body: &str) -> PathBuf {
let path = dir.path().join("claude");
std::fs::write(&path, format!("#!/bin/bash\n{body}\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
/// Spawns it, retrying the one error a freshly written executable can give
/// while another test's fork still holds its write handle.
fn start(program: &Path, args: &[String]) -> Box<dyn Child> {
start_with(program, args, &token_holding("tok-canary"))
}
/// What the unit would hold: what a shell needs, and credential-shaped variables
/// that live only here, never in the test process, so only the spawner can pass them on.
fn unit_environment() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut unit: Vec<(std::ffi::OsString, std::ffi::OsString)> = vec![("PATH".into(), "/usr/local/bin:/usr/bin:/bin".into())];
for name in CREDENTIAL_CANARIES {
unit.push((name.into(), format!("canary-{name}").into()));
}
unit
}
const CREDENTIAL_CANARIES: [&str; 6] = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_BASE_URL", "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", "UNLISTED_VARIABLE"];
fn start_with(program: &Path, args: &[String], token: &TokenFd) -> Box<dyn Child> {
for _ in 0..50 {
let config = FreshConfig::create(program.parent().unwrap(), None).unwrap();
match spawn_program(program, args, token, &unit_environment(), config) {
Ok(child) => return child,
Err(error) if format!("{error:#}").contains("busy") => std::thread::sleep(StdDuration::from_millis(100)),
Err(error) => panic!("{error:#}"),
}
}
panic!("the script stayed busy");
}
/// Everything the child gives until it has been reaped and its stdout has ended.
fn drain(child: &mut dyn Child) -> (Vec<String>, Option<i32>) {
let (mut lines, mut code, mut eof) = (Vec::new(), None, false);
loop {
match child.next(StdDuration::from_secs(20)) {
Read::Line(line) => lines.push(line),
Read::Eof => eof = true,
Read::Exited(status) => code = Some(status),
Read::Timeout => panic!("the fake claude hung"),
}
if let (true, Some(status)) = (eof, code) {
return (lines, status);
}
}
}
/// Whether a process of this id still runs. A zombie is dead: with no init to reap
/// it (cargo as PID 1 in a one-command job) it stays in the table after it is killed.
fn alive(pid: &str) -> bool {
let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else { return false };
stat.rsplit_once(')').is_some_and(|(_, rest)| !rest.trim_start().starts_with('Z'))
}
#[test]
fn the_child_gets_its_arguments_and_lines_in_order_and_ends_when_its_input_closes() {
let dir = tempfile::tempdir().unwrap();
let program = script(&dir, "echo \"args: $*\"\nwhile read line; do echo \"got: $line\"; done\necho bye\nexit 7");
let mut child = start(&program, &["-p".to_string(), "--max-turns".to_string(), "500".to_string()]);
child.write_line("first");
child.write_line("second");
child.close_input();
assert_eq!(drain(&mut *child), (["args: -p --max-turns 500", "got: first", "got: second", "bye"].map(String::from).to_vec(), Some(7)));
}
#[test]
fn a_silent_child_times_out_instead_of_blocking_and_a_signalled_child_has_no_code() {
let dir = tempfile::tempdir().unwrap();
let program = script(&dir, "kill -9 $$");
let mut child = start(&program, &[]);
assert_eq!(drain(&mut *child), (Vec::new(), None));
let quiet = script(&dir, "exec sleep 5");
let mut child = start(&quiet, &[]);
assert_eq!(child.next(StdDuration::from_millis(100)), Read::Timeout);
child.stop(Stop::Interrupt);
assert_eq!(drain(&mut *child), (Vec::new(), None), "SIGINT ended the unprepared child");
}
#[test]
fn interrupt_is_sigint_the_childs_own_handler_can_catch() {
let dir = tempfile::tempdir().unwrap();
let program = script(&dir, "trap 'echo interrupted; exit 130' INT\necho ready\nwhile :; do sleep 0.05; done");
let mut child = start(&program, &[]);
assert_eq!(child.next(StdDuration::from_secs(20)), Read::Line("ready".into()));
child.stop(Stop::Interrupt);
assert_eq!(drain(&mut *child), (vec!["interrupted".to_string()], Some(130)));
}
#[test]
fn a_program_that_cannot_start_is_an_error_that_names_no_argument() {
let canary = "canary-argument-6b0d";
let scratch = tempfile::tempdir().unwrap();
let config = FreshConfig::create(scratch.path(), None).unwrap();
let error = spawn_program(Path::new("/nonexistent/claude"), &[canary.to_string()], &token_holding("tok-canary"), &[], config).err().unwrap();
assert!(!format!("{error:#}").contains(canary) && !format!("{error:#}").contains("nonexistent"), "{error:#}");
}
/// A read that times out here means the stream stopped, never a wait worth 60 s:
/// fail at once instead of hanging the suite.
struct Prompt(Box<dyn Child>);
impl Child for Prompt {
fn write_line(&mut self, line: &str) {
self.0.write_line(line)
}
fn close_input(&mut self) {
self.0.close_input()
}
fn next(&mut self, timeout: StdDuration) -> Read {
match self.0.next(StdDuration::from_secs(20).min(timeout)) {
Read::Timeout => panic!("the fake claude went quiet"),
read => read,
}
}
fn stop(&mut self, how: Stop) -> bool {
self.0.stop(how)
}
}
struct Spawn(PathBuf);
impl Spawner for Spawn {
fn spawn(&self, args: &[String], token: &TokenFd) -> Result<Box<dyn Child>> {
Ok(Box::new(Prompt(start_with(&self.0, args, token))))
}
}
/// The real child as it is: a read may time out, which is how the drain after an exit is waited.
/// A run that has not ended by 15 s is a failure, not a hang.
struct Bounded(Box<dyn Child>, std::time::Instant);
impl Child for Bounded {
fn write_line(&mut self, line: &str) {
self.0.write_line(line)
}
fn close_input(&mut self) {
self.0.close_input()
}
fn next(&mut self, timeout: StdDuration) -> Read {
assert!(self.1.elapsed() < StdDuration::from_secs(15), "the run did not end");
self.0.next(StdDuration::from_secs(1).min(timeout))
}
fn stop(&mut self, how: Stop) -> bool {
self.0.stop(how)
}
}
struct Patient(PathBuf);
impl Spawner for Patient {
fn spawn(&self, args: &[String], token: &TokenFd) -> Result<Box<dyn Child>> {
Ok(Box::new(Bounded(start_with(&self.0, args, token), std::time::Instant::now())))
}
}
#[test]
fn a_whole_run_through_the_real_child_flushes_at_the_ceiling_and_ends_on_the_flushs_own_result() {
let dir = tempfile::tempdir().unwrap();
let lines = [at("m1", 10), at("m2", CEILING_TOKENS)].join("
");
let (echo, result) = (echo(), ok_result());
// Prints two requests, waits for the start and the flush on stdin, echoes both, then answers, as a stand-in for a Self that obeys.
let body = format!("cat <<'EOF'
{lines}
EOF
read -t 5 start
read -t 5 flush
cat <<'EOF'
{echo}
{echo}
{result}
EOF
while read more; do :; done
exit 0");
let program = script(&dir, &body);
let mind = with_run();
let mut out = Vec::new();
let outcome = carry(&Seen::of(&mind), &SystemClock, &FakeLender::new(Answer::Grant), &Spawn(program), TEST_CAP_S, &mut out).unwrap();
assert_eq!(outcome, SelfRunOutcome::Ended { cause: Cause::FlushRequested, code: Some(0), requests: 2, prefix: 300_000 });
let printed: String = String::from_utf8(out).unwrap().lines().filter(|line| !line.starts_with("decision ")).map(|line| format!("{line}\n")).collect();
assert_eq!(printed, "request 1 prefix 10
request 2 prefix 300000
result subtype=success is_error=false: The answer.
");
}
#[test]
fn a_child_that_ends_after_a_failure_result_ends_the_run_promptly_through_the_real_child() {
let dir = tempfile::tempdir().unwrap();
let result = result_line();
// An authentication or API failure: the error result is the first thing it prints, then it exits 1.
let program = script(&dir, &format!("read -t 5 start\ncat <<'EOF'\n{result}\nEOF\nexit 1"));
let started = std::time::Instant::now();
let mut out = Vec::new();
let outcome = carry(&Seen::of(&with_run()), &SystemClock, &FakeLender::new(Answer::Grant), &Spawn(program), TEST_CAP_S, &mut out).unwrap();
assert_eq!(outcome, SelfRunOutcome::Ended { cause: Cause::Own, code: Some(1), requests: 0, prefix: 0 });
assert_eq!(outcome.exit_status(), 1);
assert!(!String::from_utf8_lossy(&out).contains("request "), "no request was heard");
assert!(started.elapsed() < StdDuration::from_secs(20), "it ended promptly");
}
/// The Yggdrasil probe shape: the child prints its result and exits, and a background
/// process it started still holds its stdout, so the stream never ends. The run is over
/// when the child is, and the actuator signals nothing: the child is gone.
#[test]
fn a_child_that_exits_while_a_descendant_holds_its_stdout_ends_the_run_without_a_signal() {
for flushed in [false, true] {
let dir = tempfile::tempdir().unwrap();
let pids = dir.path().join("holder.pid");
let (result, ceiling) = (result_line(), at("m1", CEILING_TOKENS));
let before = if flushed { format!("cat <<'EOF'\n{ceiling}\nEOF\nread -t 5 start\nread -t 5 flush\n") } else { "read -t 5 start\n".to_string() };
let program = script(&dir, &format!("{before}cat <<'EOF'\n{result}\nEOF\nsleep 30 &\necho $! > {}\nexit 0", pids.display()));
let started = std::time::Instant::now();
let mut out = Vec::new();
let outcome = carry(&Seen::of(&with_run()), &SystemClock, &FakeLender::new(Answer::Grant), &Patient(program), TEST_CAP_S, &mut out).unwrap();
let holder = std::fs::read_to_string(&pids).unwrap().trim().to_string();
let cause = if flushed { Cause::FlushRequested } else { Cause::Own };
let (requests, prefix) = if flushed { (1, CEILING_TOKENS) } else { (0, 0) };
assert_eq!(outcome, SelfRunOutcome::Ended { cause, code: Some(0), requests, prefix }, "flushed: {flushed}");
assert!(alive(&holder), "the actuator signals no one after the child is gone");
assert!(started.elapsed() < StdDuration::from_secs(20), "it ended on the child's exit: {:?}", started.elapsed());
Command::new("kill").arg(&holder).status().unwrap();
}
}
#[test]
fn each_signal_reaches_a_child_that_ignores_the_weaker_ones_and_one_for_a_gone_child_is_not_delivered() {
let dir = tempfile::tempdir().unwrap();
let program = script(&dir, "trap '' INT TERM\necho ready\nwhile :; do sleep 0.05; done");
let mut child = start(&program, &[]);
assert_eq!(child.next(StdDuration::from_secs(20)), Read::Line("ready".into()));
assert!(child.stop(Stop::Interrupt) && child.stop(Stop::Terminate), "delivered, and ignored");
assert_eq!(child.next(StdDuration::from_millis(300)), Read::Timeout, "still running");
assert!(child.stop(Stop::Kill));
assert_eq!(drain(&mut *child), (Vec::new(), None), "SIGKILL leaves no code");
let quick = script(&dir, "exit 0");
let mut gone = start(&quick, &[]);
assert_eq!(drain(&mut *gone), (Vec::new(), Some(0)));
assert!(!gone.stop(Stop::Interrupt), "no such process");
}
#[test]
fn a_signal_reaches_the_whole_process_group_the_child_started() {
// Not SIGINT: a background job of a non-interactive shell ignores it, which is the shell's rule, not ours.
for how in [Stop::Terminate, Stop::Kill] {
let dir = tempfile::tempdir().unwrap();
// The child starts a descendant of its own and waits for it.
let program = script(&dir, "sleep 300 &\necho $!\nwait");
let mut child = start(&program, &[]);
let Read::Line(holder) = child.next(StdDuration::from_secs(20)) else { panic!("no pid") };
assert!(alive(&holder));
assert!(child.stop(how), "{how:?} delivered to the group");
drain(&mut *child);
let mut waited = 0;
while alive(&holder) && waited < 100 {
std::thread::sleep(StdDuration::from_millis(50));
waited += 1;
}
assert!(!alive(&holder), "{how:?} reached the descendant too");
}
}
}
mod lease;
mod closure;
// ---- the task in the start message ---------------------------------------
fn follow_up_saying(label: &str, item: &str) -> (PipelineDocument, PipelineRef) {
let PipelineDocument::FollowUp(mut document) = follow_up(label, &spec_ref("x")) else { unreachable!() };
document.item = item.into();
(PipelineDocument::FollowUp(document), reference(K::FollowUp, &id("follow_up", label)))
}
fn claiming(claims: &[PipelineRef], documents: Vec<PipelineDocument>) -> TestMind {
let mind = TestMind::seeded();
mind.committed(vec![spec("x")]);
if !documents.is_empty() {
mind.committed(documents);
}
let PipelineDocument::Run(mut document) = self_run("selfrun-a", "5") else { unreachable!() };
document.claims = claims.to_vec();
mind.committed(vec![PipelineDocument::Run(document)]);
mind
}
#[test]
fn the_start_message_carries_the_claimed_follow_ups_item_and_id_verbatim() {
let (brief, claim) = follow_up_saying("brief-a", "Fix the thing.\nThen report \u{e9}.");
let mind = claiming(std::slice::from_ref(&claim), vec![brief]);
let ran = carry_with(&Seen::of(&mind), vec![Out(result_line())], Some(0), false);
let first = &ran.written()[0];
assert!(first.starts_with(&start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a"), &[plan_window()], 1_793_306_000_000)), "{first}");
for needle in ["Fix the thing.\nThen report \u{e9}.", claim.id.0.as_str(), "Use the eureka skill."] {
assert!(first.contains(needle), "{needle}: {first}");
}
assert!(first.contains(&format!("(campaign `{}`)", follow_up_campaign())), "{first}");
}
fn follow_up_campaign() -> String {
let PipelineDocument::FollowUp(document) = follow_up("c", &spec_ref("x")) else { unreachable!() };
document.campaign.0
}
#[test]
fn a_claim_that_is_no_brief_is_named_not_dropped() {
// A claimed cut spec is the task: named with its id, to be read with huginn view.
let mind = claiming(&[spec_ref("x")], vec![]);
let ran = carry_with(&Seen::of(&mind), vec![Out(result_line())], Some(0), false);
let first = &ran.written()[0];
assert!(first.contains(&format!("claimed cut spec `{}`", spec_ref("x").id.0)) && first.contains("huginn view"), "{first}");
assert!(!first.contains("unknown"), "{first}");
// A claim the mind does not hold at all.
let absent = reference(K::FollowUp, &id("follow_up", "absent"));
let task = task_section(&mind, std::slice::from_ref(&absent)).unwrap();
assert!(task.contains(&absent.id.0) && task.contains("does not hold"), "{task}");
// A document of another kind is named by id and kind.
let other = run_ref("selfrun-a");
let task = task_section(&mind, std::slice::from_ref(&other)).unwrap();
assert!(task.contains(&other.id.0) && task.contains("no brief"), "{task}");
}
#[test]
fn every_claim_of_sixty_four_full_items_is_named_by_id() {
let big = "\u{e9}".repeat(500);
let docs: Vec<_> = (0..64).map(|n| follow_up_saying(&format!("big-{n}"), &big)).collect();
let claims: Vec<_> = docs.iter().map(|(_, claim)| claim.clone()).collect();
let mind = claiming(&claims, docs.into_iter().map(|(document, _)| document).collect());
let task = task_section(&mind, &claims).unwrap();
for claim in &claims {
assert!(task.contains(&claim.id.0), "{}", claim.id.0);
}
assert_eq!(task.matches(&big).count(), 64, "every item verbatim");
assert!(!task.contains("truncated"));
// Claim order is kept.
assert!(task.find(&claims[0].id.0) < task.find(&claims[63].id.0));
}
#[test]
fn a_mind_query_failure_on_a_claim_fails_the_run_with_the_cause_and_starts_no_child() {
let (brief, claim) = follow_up_saying("brief-a", "Fix the thing.");
let mind = claiming(std::slice::from_ref(&claim), vec![brief]);
let mut seen = Seen::of(&mind);
seen.query_fails_on = Some(claim.id.0.clone());
let ran = carry_with(&seen, vec![Out(result_line())], Some(0), false);
let error = format!("{:#}", ran.outcome.as_ref().unwrap_err());
assert!(error.contains("mind-down-canary") && error.contains(&claim.id.0), "{error}");
assert!(ran.spawns.is_empty() && ran.events.is_empty() && !ran.all.iter().any(|event| matches!(event, Event::Lent)), "no lease, no child");
assert!(!ran.out.contains("unknown"));
}
#[test]
fn a_run_without_claims_gets_the_identity_lines_alone() {
let ran = go(vec![Out(result_line())]);
assert_eq!(ran.written()[0], start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a"), &[plan_window()], 1_793_306_000_000));
}
// ---- the live session -----------------------------------------------------
use eureka_pipeline::PipelineResolution;
fn live_mind() -> Rc<TestMind> {
let mind = Rc::new(with_run());
mind.committed(vec![spec("x"), run("persona", RunTurn::PersonaTurn, RunOperator::Mind, &[])]);
mind
}
/// The mind gains these documents as her Persona turn's admission: the provenance that makes a self-run follow-up a brief.
fn persona_commit(mind: &TestMind, documents: Vec<PipelineDocument>) {
let outcome = mind.admit(crate::persona::AGENT, "persona-a", documents).unwrap();
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
}
/// Her Persona admits these briefs, `seconds` into the run.
fn briefed(mind: &Rc<TestMind>, seconds: i64, documents: Vec<PipelineDocument>) -> Step {
let mind = mind.clone();
Step::Do(seconds, Rc::new(move || persona_commit(&mind, documents.clone())))
}
/// A brief from her Persona: a follow-up with owner `self-run`, sourced from the Persona turn's run.
fn brief(label: &str, item: &str) -> PipelineDocument {
let PipelineDocument::FollowUp(mut document) = follow_up(label, &run_ref("persona")) else { unreachable!() };
document.item = item.into();
document.owner = inbox::OWNER.into();
PipelineDocument::FollowUp(document)
}
fn brief_ref(label: &str) -> PipelineRef {
reference(K::FollowUp, &id("follow_up", label))
}
/// The mind gains these documents, `seconds` into the run.
fn admits(mind: &Rc<TestMind>, seconds: i64, documents: Vec<PipelineDocument>) -> Step {
let mind = mind.clone();
Step::Do(seconds, Rc::new(move || mind.committed(documents.clone())))
}
/// Her Self resolves the brief herself, through her mind tool.
fn resolves(mind: &Rc<TestMind>, label: &str) -> Step {
admits(mind, 0, vec![close(brief_ref(label), recorded())])
}
fn resolutions_of(mind: &TestMind, subject: &PipelineRef) -> Vec<PipelineResolution> {
mind.ids(K::Resolution)
.iter()
.filter_map(|resolution| match mind.view(K::Resolution, resolution)?.document {
PipelineDocument::Resolution(document) if document.subject == *subject => Some(document),
_ => None,
})
.collect()
}
fn five_minute_request(id: &str, prefix: u64) -> String {
let mut event: Value = serde_json::from_str(&at(id, prefix)).unwrap();
event["message"]["usage"]["cache_creation"] = json!({"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 100});
event.to_string()
}
fn tells(ran: &Ran) -> Vec<String> {
ran.written().into_iter().filter(|text| text.contains("A brief from your Persona")).collect()
}
fn mind_run(mind: &Rc<TestMind>, steps: Vec<Step>) -> Ran {
carry_with(&Seen::of(mind), steps, Some(0), false)
}
#[test]
fn a_result_never_ends_the_session_the_child_ends_it_or_the_cache_does() {
let ran = go(vec![Out(echo()), Out(at("m1", 10)), Out(ok_result())]);
assert_eq!(ran.count(&Event::InputClosed), 0, "a result is the end of a turn: {:?}", ran.events);
assert_eq!(ran.ended().0, Cause::Own);
}
#[test]
fn a_brief_derived_while_a_turn_runs_is_written_at_once_counted_at_its_echo_and_never_told_twice() {
let mind = live_mind();
let steps = vec![
Out(echo()),
Out(at("m1", 10)),
// The brief arrives while the first turn runs: the next poll writes it at once, mid-turn.
briefed(&mind, 31, vec![brief("b1", "Survey the noise functions.")]),
Out(echo()),
// Later polls do not tell it again.
Idle(31),
Idle(31),
Out(ok_result()),
];
let ran = mind_run(&mind, steps);
let told = tells(&ran);
assert_eq!(told.len(), 1, "{:?}", ran.written());
assert!(told[0].contains(&brief_ref("b1").id.0) && told[0].contains("Survey the noise functions."), "{}", told[0]);
assert!(told[0].contains("resolve follow-up") && told[0].contains("Recorded"), "she is told how to close it: {}", told[0]);
assert_eq!(ran.written().len(), 2, "the start message and one tell: {:?}", ran.written());
assert!(ran.out.contains("decision Write { tells: true, flush: None } prefix 10 ttl_s 0 idle_s working inbox true written 2 consumed 1\n"), "{}", ran.out);
assert_eq!(ran.count(&Event::InputClosed), 0);
// A result is not an answer: nothing closes the brief by its timing.
assert!(resolutions_of(&mind, &brief_ref("b1")).is_empty());
}
#[test]
fn a_brief_derived_while_the_session_is_idle_is_written_and_the_session_is_busy_until_its_echo() {
let mind = live_mind();
let steps = vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), briefed(&mind, 31, vec![brief("b2", "Wire the shader uniforms.")]), Out(echo()), Out(ok_result())];
let ran = mind_run(&mind, steps);
assert_eq!(tells(&ran).len(), 1, "{:?}", ran.written());
assert!(ran.out.contains("idle_s 31 inbox true written 2 consumed 1\n"), "idle when it was written: {}", ran.out);
assert_eq!(ran.count(&Event::InputClosed), 0);
assert!(resolutions_of(&mind, &brief_ref("b2")).is_empty());
}
#[test]
fn a_brief_open_at_start_is_in_the_start_message_with_how_to_resolve_it_and_is_not_told_again() {
let mind = live_mind();
persona_commit(&mind, vec![brief("p1", "Still open.")]);
let ran = mind_run(&mind, vec![Out(echo()), Idle(31), Idle(31)]);
let start = &ran.written()[0];
assert!(start.contains("Also open for you") && start.contains(&brief_ref("p1").id.0) && start.contains("Still open."), "{start}");
assert!(start.contains(&format!("resolve follow-up {} Recorded", brief_ref("p1").id.0)), "{start}");
assert_eq!(ran.written().len(), 1, "never told again: {:?}", ran.written());
}
#[test]
fn a_follow_up_her_self_filed_for_herself_is_never_told() {
let mind = live_mind();
let PipelineDocument::FollowUp(mut hers) = follow_up("hers", &run_ref("selfrun-a")) else { unreachable!() };
hers.owner = inbox::OWNER.into();
// Naming the Persona turn's run as the source does not make it hers to be told: the receipt records who admitted it.
let PipelineDocument::FollowUp(mut citing) = follow_up("citing", &run_ref("persona")) else { unreachable!() };
citing.owner = inbox::OWNER.into();
mind.committed(vec![PipelineDocument::FollowUp(hers), PipelineDocument::FollowUp(citing)]);
let ran = mind_run(&mind, vec![Out(echo()), Idle(31)]);
assert_eq!(ran.written().len(), 1, "{:?}", ran.written());
assert!(!ran.written()[0].contains("Also open"), "{}", ran.written()[0]);
}
#[test]
fn an_idle_end_closes_no_brief_the_ones_she_left_open_stay_open_and_her_resolutions_stand() {
let mind = live_mind();
persona_commit(&mind, vec![brief("p1", "Left open."), brief("p2", "Answered by her.")]);
let steps = vec![Out(echo()), Out(at("m1", 10)), resolves(&mind, "p2"), Out(ok_result()), Idle(3539), Idle(1), Out(echo()), Out(ok_result())];
let ran = mind_run(&mind, steps);
assert_eq!(ran.count(&Event::InputClosed), 1, "{:?}", ran.events);
assert!(resolutions_of(&mind, &brief_ref("p1")).is_empty(), "the actuator resolved nothing she left open");
assert_eq!(resolutions_of(&mind, &brief_ref("p2")).len(), 1, "her own resolution stands alone");
}
#[test]
fn a_rotation_a_ceiling_an_error_or_a_crash_leaves_the_told_briefs_open_for_the_successor() {
let cap = i64::try_from(TEST_CAP_S).unwrap();
let ends: Vec<(&str, Vec<Step>)> = vec![
("rotation", vec![Out(echo()), After(cap - 1400, at("m1", 10)), Out(echo()), Out(ok_result())]),
("ceiling", vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), Out(echo()), Out(ok_result())]),
("error before an answer", vec![Out(echo()), Out(result_line())]),
("idle flush answered by an error", vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), Idle(3539), Idle(1), Out(echo()), Out(result_line())]),
("crash", vec![Out(echo()), Out(at("m1", 10)), Exit(Some(137))]),
];
for (name, steps) in ends {
let mind = live_mind();
persona_commit(&mind, vec![brief("p1", "Left open.")]);
let ran = mind_run(&mind, steps);
assert!(ran.outcome.is_ok(), "{name}");
assert!(resolutions_of(&mind, &brief_ref("p1")).is_empty(), "{name}: the brief stays open");
}
}
#[test]
fn a_brief_admitted_after_the_flush_is_written_stays_in_force_and_untold() {
let mind = live_mind();
let steps = vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), briefed(&mind, 31, vec![brief("late", "Too late.")]), Idle(31), Out(echo()), Out(ok_result())];
let ran = mind_run(&mind, steps);
assert!(tells(&ran).is_empty(), "{:?}", ran.written());
assert_eq!(ran.written().len(), 2, "the start and the flush; no line after the flush");
assert!(resolutions_of(&mind, &brief_ref("late")).is_empty());
}
#[test]
fn the_session_ends_where_the_cache_would_be_evicted_with_a_flush_and_a_waiting_brief_keeps_it_open() {
// An hour's cache: warm at 3539 s, the flush at 3540, the input closed at the result after the flush's echo.
let hour = go(vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), Idle(3539), Idle(1), Out(echo()), Out(ok_result()), Idle(1)]);
assert_eq!(flushes(&hour), 1);
let flush_at = hour.events.iter().position(|event| matches!(event, Event::Wrote(line) if line.contains(FLUSH_MESSAGE))).unwrap();
let closed_at = hour.events.iter().position(|event| *event == Event::InputClosed).unwrap();
assert!(flush_at < closed_at && hour.count(&Event::InputClosed) == 1, "{:?}", hour.events);
assert!(hour.out.contains("decision Write { tells: false, flush: Some(Idle) } prefix 10 ttl_s 3600 idle_s 3540 inbox false written 2 consumed 1\n"), "{}", hour.out);
assert!(hour.written().iter().any(|text| text.contains("nothing is waiting and its cache would lapse") && text.contains("Resolve each brief you answered")), "{:?}", hour.written());
// Five minutes' cache: the flush at 240 s.
let short = go(vec![Out(echo()), Out(five_minute_request("m1", 10)), Out(ok_result()), Idle(239), Idle(1), Out(echo()), Out(ok_result()), Idle(1)]);
assert_eq!(flushes(&short), 1);
// Not a second early.
let early = go(vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), Idle(3539)]);
assert_eq!(flushes(&early), 0);
// A brief waiting at the cache's end is told, not flushed: the session stays open.
let mind = live_mind();
let steps = vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), briefed(&mind, 3539, vec![brief("b5", "One more.")]), Idle(1), Out(echo()), Out(ok_result()), Idle(1)];
let ran = mind_run(&mind, steps);
assert_eq!((flushes(&ran), tells(&ran).len(), ran.count(&Event::InputClosed)), (0, 1, 0), "{:?}
{}", ran.written(), ran.out);
}
#[test]
fn a_tell_that_is_not_yet_echoed_keeps_the_session_busy_so_the_cache_end_does_not_flush_over_it() {
let mind = live_mind();
let steps = vec![Out(echo()), Out(at("m1", 10)), Out(ok_result()), briefed(&mind, 31, vec![brief("b6", "Pending.")]), Idle(3600)];
let ran = mind_run(&mind, steps);
assert_eq!((tells(&ran).len(), flushes(&ran)), (1, 0), "{:?}", ran.written());
}
#[test]
fn near_the_cap_the_session_is_asked_for_its_flush_idle_or_working_and_closes_at_the_result_after_its_echo() {
let cap = i64::try_from(TEST_CAP_S).unwrap();
// Idle at cap - 1700 s: the flush at cap - 1500, not a second before.
let before = go(vec![Out(echo()), After(cap - 1700, at("m1", 10)), Out(ok_result()), Idle(199)]);
assert_eq!(flushes(&before), 0);
let idle = go(vec![Out(echo()), After(cap - 1700, at("m1", 10)), Out(ok_result()), Idle(199), Idle(1), Out(echo()), Out(ok_result())]);
assert_eq!((flushes(&idle), idle.count(&Event::InputClosed)), (1, 1), "{:?}", idle.events);
assert!(idle.written().iter().any(|text| text.contains("the unit's time is nearly up")), "{:?}", idle.written());
// Mid-turn: the flush at cap - 1500, once.
let working = go(vec![Out(echo()), After(cap - 1500, at("m1", 10))]);
assert_eq!(flushes(&working), 1);
let not_yet = go(vec![Out(echo()), After(cap - 1501, at("m1", 10))]);
assert_eq!(flushes(&not_yet), 0);
}
#[test]
fn no_stdin_line_is_written_after_the_flush_and_nothing_but_the_writer_writes() {
let mind = live_mind();
persona_commit(&mind, vec![brief("b7", "Early.")]);
let steps = vec![Out(echo()), Out(at("m1", CEILING_TOKENS)), briefed(&mind, 31, vec![brief("b8", "After the flush.")]), Idle(31), Idle(31), Out(echo()), Out(at("m2", 11)), Out(ok_result())];
let ran = mind_run(&mind, steps);
let written = ran.written();
assert_eq!(written.len(), 2, "{written:?}");
assert!(written[1].starts_with(FLUSH_MESSAGE), "{}", written[1]);
assert_eq!(ran.spawns.len(), 1);
assert!(ran.spawns[0].iter().all(|arg| arg != "--settings"), "no hooks on argv: {:?}", ran.spawns[0]);
}
/// The committed 2.1.312 streams, replayed through the real parser and the decision.
mod live_session {
use crate::self_run::decide::{Action as Do, Event as Fact, Why};
use super::*;
fn flush(why: Why) -> Do {
Do::Write { tells: false, flush: Some(why) }
}
/// One recorded line as the loop would hear it: a label for the stream, the decision's action.
fn replay(jsonl: &str) -> Vec<(String, Do)> {
replay_state(jsonl).0
}
/// The same, with the state the decision ended in. A `want` entry (the probe asked its writer to send a line that waits) is a brief waiting; the flush is the decision's own.
fn replay_state(jsonl: &str) -> (Vec<(String, Do)>, State) {
let mut state = State::started(crate::testkit::at(0), 100_000);
let mut ttl_1h = false;
let mut trace = Vec::new();
for entry in jsonl.lines() {
let entry: Value = serde_json::from_str(entry).unwrap();
let at = crate::testkit::at((entry["t"].as_f64().unwrap() * 1000.0) as i64);
if let Some(text) = entry.get("want").and_then(Value::as_str) {
let (label, fact) = if text.starts_with(FLUSH_MESSAGE) { ("flush-asked", Fact::Tick) } else { ("want", Fact::Inbox { pending: true }) };
let (next, action) = decide(state, fact, at);
state = next;
trace.push((label.to_string(), action));
continue;
}
let Some(line) = entry.get("line").and_then(Value::as_str) else { continue };
if let Some(hour) = writes_hour_cache(line) {
ttl_1h = hour;
}
let (label, fact) = match read_usage(line) {
Usage::Request { id, prefix } => (format!("request:{prefix}"), Fact::Request { id, prefix }),
Usage::Echo => ("echo".to_string(), Fact::Echo),
Usage::Result { ok, blocked } => (format!("result:{}", if blocked { "blocked" } else if ok { "ok" } else { "error" }), Fact::Result { ok, blocked, ttl_1h }),
Usage::Other | Usage::Unreadable => ("other".to_string(), Fact::Tick),
};
let (next, action) = decide(state, fact, at);
state = next;
trace.push((label, action));
}
(trace, state)
}
fn kinds(trace: &[(String, Do)]) -> Vec<&str> {
trace.iter().map(|(label, _)| label.as_str()).collect()
}
/// The indexes at which the decision acted, and how.
fn acted(trace: &[(String, Do)]) -> Vec<(usize, Do)> {
trace.iter().enumerate().filter(|(_, (_, action))| *action != Do::None).map(|(at, (_, action))| (at, action.clone())).collect()
}
const FOLDED: &str = include_str!("claude-2.1.312-live-session-folded.jsonl");
const QUEUED: [&str; 2] = [include_str!("claude-2.1.312-live-session-queued.jsonl"), include_str!("claude-2.1.312-live-session-queued-scrub.jsonl")];
const THREE_QUEUED: [&str; 2] = [include_str!("claude-2.1.312-live-session-three-queued.jsonl"), include_str!("claude-2.1.312-live-session-three-queued-scrub.jsonl")];
const SERIALIZED: [&str; 2] = [include_str!("claude-2.1.312-live-session-serialized.jsonl"), include_str!("claude-2.1.312-live-session-serialized-scrub.jsonl")];
const CEILING_FOLDED: &str = include_str!("claude-2.1.312-live-session-ceiling-flush-folded.jsonl");
const CEILING_QUEUED: [&str; 2] = [include_str!("claude-2.1.312-live-session-ceiling-flush-queued.jsonl"), include_str!("claude-2.1.312-live-session-ceiling-flush-queued-scrub.jsonl")];
const API_ERROR: [&str; 2] = [include_str!("claude-2.1.312-live-session-api-error-first.jsonl"), include_str!("claude-2.1.312-live-session-api-error-first-scrub.jsonl")];
const BLOCKING: [&str; 2] = [include_str!("claude-2.1.312-live-session-blocking-limit.jsonl"), include_str!("claude-2.1.312-live-session-blocking-limit-scrub.jsonl")];
const COMPACTION_OFF: [&str; 2] = [include_str!("claude-2.1.312-live-session-compaction-off-360k.jsonl"), include_str!("claude-2.1.312-live-session-compaction-off-360k-scrub.jsonl")];
#[test]
fn a_line_written_during_a_tool_call_is_folded_one_result_and_its_echo_before_the_request_that_carries_it() {
let trace = replay(FOLDED);
// The tool_result's own user line is no echo: it is a foreign owner's array content.
assert_eq!(kinds(&trace), ["other", "echo", "request:10", "other", "other", "other", "echo", "request:10", "result:ok"]);
assert!(acted(&trace).is_empty(), "{:?}", acted(&trace));
}
#[test]
fn a_line_written_while_the_last_request_is_in_flight_is_queued_two_results_and_its_echo_after_the_first() {
for stream in QUEUED {
let trace = replay(stream);
assert_eq!(kinds(&trace), ["other", "echo", "request:10", "result:ok", "other", "echo", "request:10", "result:ok"]);
assert!(acted(&trace).is_empty(), "{:?}", acted(&trace));
}
}
fn sent(jsonl: &str) -> usize {
jsonl.lines().filter(|entry| serde_json::from_str::<Value>(entry).unwrap().get("sent").is_some()).count()
}
#[test]
fn lines_written_behind_an_in_flight_request_come_back_as_one_echo_which_is_why_one_write_is_outstanding() {
for stream in THREE_QUEUED {
let trace = replay(stream);
let echoes = kinds(&trace).iter().filter(|kind| **kind == "echo").count();
assert_eq!((sent(stream), echoes), (3, 2), "three lines written, two echoes: counting echoes against lines written drifts for good");
}
}
#[test]
fn with_one_write_outstanding_every_write_is_echoed_once_and_the_flush_is_reached_and_answered() {
for stream in SERIALIZED {
let (trace, state) = replay_state(stream);
assert_eq!(
kinds(&trace),
["other", "want", "want", "echo", "request:10", "result:ok", "other", "echo", "request:300000", "result:ok", "flush-asked", "other", "echo", "request:10", "result:ok"]
);
// Both briefs wait for the start message's echo and go as one line; the flush is written at the ceiling request; the result after its echo closes stdin.
assert_eq!(acted(&trace), vec![(3, Do::Write { tells: true, flush: None }), (8, flush(Why::Ceiling)), (14, Do::CloseStdin)]);
assert_eq!((sent(stream), state.written, state.consumed), (3, 3, 3), "every line written has its own echo");
}
}
#[test]
fn the_flush_folded_into_a_running_turn_is_answered_at_its_one_result() {
let trace = replay(CEILING_FOLDED);
assert_eq!(kinds(&trace), ["other", "echo", "request:300000", "other", "other", "other", "echo", "request:10", "result:ok"]);
assert_eq!(acted(&trace), vec![(2, flush(Why::Ceiling)), (8, Do::CloseStdin)], "never a grace interrupt");
}
#[test]
fn the_flush_queued_behind_the_turn_that_reached_the_ceiling_is_answered_at_its_own_second_result() {
for stream in CEILING_QUEUED {
let trace = replay(stream);
assert_eq!(kinds(&trace), ["other", "echo", "request:300000", "result:ok", "other", "echo", "request:10", "result:ok"]);
// The first result ends the turn the flush was queued behind: it closes nothing.
assert_eq!(acted(&trace), vec![(2, flush(Why::Ceiling)), (7, Do::CloseStdin)], "never a grace interrupt");
}
}
#[test]
fn an_api_error_before_any_answer_closes_the_session_at_once_with_no_flush() {
for stream in API_ERROR {
let trace = replay(stream);
assert_eq!(kinds(&trace), ["other", "echo", "other", "result:error"]);
assert_eq!(acted(&trace), vec![(3, Do::CloseStdin)]);
}
}
#[test]
fn a_context_over_the_blocking_limit_is_a_blocked_result_that_closes_the_session() {
for stream in BLOCKING {
let trace = replay(stream);
assert_eq!(kinds(&trace), ["other", "echo", "request:990000", "result:ok", "other", "echo", "other", "result:blocked"]);
assert_eq!(acted(&trace), vec![(2, flush(Why::Ceiling)), (7, Do::CloseStdin)]);
}
}
#[test]
fn with_compaction_off_a_request_at_360k_is_sent_and_answered_not_compacted_or_blocked() {
for stream in COMPACTION_OFF {
let trace = replay(stream);
assert_eq!(kinds(&trace), ["other", "echo", "request:360000", "result:ok", "other", "echo", "request:10", "result:ok"]);
assert_eq!(acted(&trace), vec![(2, flush(Why::Ceiling)), (7, Do::CloseStdin)]);
}
}
}
#[test]
fn a_brief_the_run_claims_is_in_the_task_once_and_not_again_under_also_open() {
let mind = TestMind::seeded();
mind.committed(vec![spec("x"), run("persona", RunTurn::PersonaTurn, RunOperator::Mind, &[])]);
persona_commit(&mind, vec![brief("claimed", "Do the claimed thing.")]);
mind.committed(vec![run("selfrun-a", RunTurn::SelfRun, RunOperator::Mind, &[brief_ref("claimed")])]);
let ran = carry_with(&Seen::of(&mind), vec![Out(echo())], Some(0), false);
let start = &ran.written()[0];
assert_eq!(start.matches("claimed follow-up").count(), 1, "{start}");
assert!(start.contains("Do the claimed thing.") && !start.contains("Also open for you"), "{start}");
}
/// The Persona's boundary: the bubblewrap wrapper `ClaudeSpawner` runs when it has a sandbox.
#[cfg(unix)]
mod sandbox {
use std::os::unix::fs::PermissionsExt;
use super::*;
#[test]
fn the_sandbox_arguments_are_pinned() {
let args = sandbox_args(Path::new("/var/lib/mind-x/sandbox"), Path::new("/run/mind-x"));
let expected: Vec<String> = [
"--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", "--tmpfs", "/run", "--ro-bind-try", "/run/systemd/resolve", "/run/systemd/resolve", "--bind", "/run/mind-x", "/run/mind-x", "--tmpfs",
"/etc/gamecult", "--tmpfs", "/var/lib", "--bind", "/var/lib/mind-x/sandbox", "/var/lib/mind-x/sandbox", "--unshare-pid", "--die-with-parent", "--new-session", "--chdir", "/var/lib/mind-x/sandbox", "--",
]
.map(String::from)
.to_vec();
assert_eq!(args, expected);
// The credentials are hidden before the sandbox and the scratch are bound back, and the network is not unshared.
let at = |needle: &str| args.iter().position(|arg| arg == needle).unwrap();
assert!(at("/run") < at("/run/mind-x") && at("/var/lib") < at("/var/lib/mind-x/sandbox"));
assert!(!args.iter().any(|arg| arg == "--unshare-net" || arg == "--unshare-all"));
}
#[test]
fn a_sandboxed_spawner_runs_bwrap_with_its_arguments_then_claude_and_the_child_sees_no_credential() {
let dir = tempfile::tempdir().unwrap();
let (bin, scratch, sandbox) = (dir.path().join("bin"), dir.path().join("scratch"), dir.path().join("sandbox"));
for made in [&bin, &scratch, &sandbox] {
std::fs::create_dir(made).unwrap();
}
let (argv, environment) = (dir.path().join("argv"), dir.path().join("environment"));
let fake = bin.join("bwrap");
std::fs::write(&fake, format!("#!/bin/sh\nprintf '%s\n' \"$@\" > {}\nenv > {}\n", argv.display(), environment.display())).unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let mut unit: Vec<(std::ffi::OsString, std::ffi::OsString)> = ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "UNLISTED_VARIABLE"].iter().map(|name| ((*name).into(), format!("canary-{name}").into())).collect();
unit.push(("PATH".into(), format!("{}:/usr/bin:/bin", bin.display()).into()));
unit.push((SCRATCH_VARIABLE.into(), scratch.clone().into_os_string()));
let spawner = ClaudeSpawner::in_sandbox(unit, "MIND_PERSONA_CONFIG_SEED", Some(sandbox.clone())).unwrap();
let mut child = spawner.spawn(&["--flag".to_string(), "value".to_string()], &token_holding("tok-canary")).unwrap();
while !matches!(child.next(StdDuration::from_secs(20)), Read::Exited(_) | Read::Timeout) {}
let recorded: Vec<String> = std::fs::read_to_string(&argv).unwrap().lines().map(String::from).collect();
let mut expected = sandbox_args(&sandbox, &scratch);
expected.extend(["claude", "--flag", "value"].map(String::from));
assert_eq!(recorded, expected, "bwrap, its arguments, then claude and its own");
let seen = std::fs::read_to_string(&environment).unwrap();
assert!(!seen.contains("canary") && !seen.contains("UNLISTED"), "the child's environment is the allow-list: {seen}");
}
}