Huginn/crates/mind-body/tests/self_run.rs
Meta Vens c76e37280d huginn-mind: a Moved resolution freezes its campaign's root and the receiver replays the history as an import naming its source
The giver: (Campaign, Moved) joins the matrix; a Moved to this mind refuses MoveToSelf and a withdrawn Moved MoveIsFinal; any staged document rooted at a moved campaign, derived ones included, refuses CampaignMoved; Docs::closing_resolution reads every other document under the root closed by the Moved, and the image fans status_changed out so an earlier as_of still reads in force. The oracle derives the same fallback independently.

The receiver: PipelineProvenance.imported_from (optional, absent from every other receipt and from the digest) names the source receipt and the externals the batch cites. An import derives nothing, refuses its own instance, a Moved outcome, and an external this mind holds; a listed external stands in for an absent referent. mind-body persona.rs gains one outcome_words arm for the new variant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:47:43 +01:00

452 lines
24 KiB
Rust

//! The `mind-self-run` binary as a process: its arguments, its exit codes and
//! what it never echoes. The carrying itself is tested in the library against a
//! real mind and a scripted child. One test, `#[ignore]`d outside a container,
//! puts the real binary, the real daemon and a fake `claude` first on `PATH`
//! together, which is where the real child's argument list and stream can be
//! seen end to end.
// The test hands the binary its environment, and reads the one variable the
// harness gives it. That is test plumbing: it reaches no library code.
#![allow(clippy::disallowed_methods, reason = "the suite sets the binary's environment")]
use std::process::{Command, Output};
const NO_SUCH_INSTANCE: &str = "self-run-test-no-such-instance";
fn self_run(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_mind-self-run")).args(args).env_remove("HUGINN_ENDPOINT").output().unwrap()
}
fn self_run_with_env_endpoint(args: &[&str], endpoint: &str) -> Output {
Command::new(env!("CARGO_BIN_EXE_mind-self-run")).args(args).env("HUGINN_ENDPOINT", endpoint).output().unwrap()
}
fn all_output(output: &Output) -> String {
format!("{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr))
}
#[test]
fn the_run_cap_must_be_a_positive_number_of_seconds_and_the_runtime_directory_must_be_named() {
let with_cap = |cap: Option<&str>| {
let mut command = Command::new(env!("CARGO_BIN_EXE_mind-self-run"));
command.args(["--instance", NO_SUCH_INSTANCE, "--endpoint", "rudp://127.0.0.1:9"]).env_remove("HUGINN_ENDPOINT").env_remove("RUNTIME_DIRECTORY").env("CLAUDE_LEASE_SOCKET", "/nonexistent/lease.sock");
match cap {
Some(cap) => command.env("MIND_SELF_RUN_CAP_S", cap),
None => command.env_remove("MIND_SELF_RUN_CAP_S"),
};
let output = command.output().unwrap();
assert_eq!(output.status.code(), Some(2));
all_output(&output)
};
for bad in [None, Some("0"), Some(""), Some("abc"), Some("-5")] {
assert!(with_cap(bad).contains("MIND_SELF_RUN_CAP_S is not set to"), "{bad:?}");
}
// A positive cap passes that check and meets the next: the unit names no scratch directory.
let message = with_cap(Some("1"));
assert!(!message.contains("MIND_SELF_RUN_CAP_S") && message.contains("RUNTIME_DIRECTORY is not set"), "{message}");
}
#[test]
fn without_an_instance_or_with_a_stray_argument_it_exits_2_with_the_usage_and_echoes_nothing() {
let canary = "canary-argument-3f9a";
for args in [
vec![],
vec!["--endpoint", "rudp://127.0.0.1:9"],
vec!["--instance", NO_SUCH_INSTANCE, canary],
vec!["--instance", NO_SUCH_INSTANCE, "--instance", canary],
vec![canary],
vec!["--resume", canary],
// The hook subcommand is gone: it is a stray argument now.
vec!["inject", "--spool", canary],
] {
let output = self_run(&args);
assert_eq!(output.status.code(), Some(2), "{args:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("usage: mind-self-run"), "{args:?}: {stderr}");
assert!(!all_output(&output).contains(canary) && output.stdout.is_empty(), "{args:?}");
}
}
#[test]
fn an_instance_that_is_not_a_slug_is_exit_2_and_is_not_echoed() {
let canary = "Canary/../Instance-5e2b";
let output = self_run(&["--instance", canary, "--endpoint", "rudp://127.0.0.1:9"]);
assert_eq!(output.status.code(), Some(2));
assert!(!all_output(&output).contains("anary"), "{}", all_output(&output));
assert!(output.stdout.is_empty());
}
#[test]
fn a_malformed_endpoint_or_none_is_exit_2_and_the_endpoint_is_not_echoed() {
let canary = "canary-endpoint-77c1";
let bad = self_run(&["--instance", NO_SUCH_INSTANCE, "--endpoint", canary]);
assert_eq!(bad.status.code(), Some(2));
assert!(!all_output(&bad).contains(canary) && bad.stdout.is_empty());
let none = self_run(&["--instance", NO_SUCH_INSTANCE]);
assert_eq!(none.status.code(), Some(2));
assert!(none.stdout.is_empty());
let from_env = self_run_with_env_endpoint(&["--instance", NO_SUCH_INSTANCE], "canary-env-endpoint-4d02");
assert_eq!(from_env.status.code(), Some(2));
assert!(!all_output(&from_env).contains("canary-env-endpoint-4d02") && from_env.stdout.is_empty());
}
#[test]
fn a_mind_that_cannot_be_reached_is_exit_2_before_any_child_and_names_no_value() {
// Port 9 on loopback answers no one; PATH is empty, so a child could not start either.
let output = Command::new(env!("CARGO_BIN_EXE_mind-self-run"))
.args(["--instance", NO_SUCH_INSTANCE, "--endpoint", "rudp://127.0.0.1:9"])
.env("PATH", "")
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2), "{}", all_output(&output));
assert!(output.stdout.is_empty());
assert!(!all_output(&output).contains("127.0.0.1"), "{}", all_output(&output));
}
#[cfg(unix)]
mod lane {
use std::io::{IoSlice, Read, Write};
use std::mem::MaybeUninit;
use std::net::UdpSocket;
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::{Child, Command, Output, Stdio};
use std::time::Duration;
use chrono::Utc;
use eureka_pipeline::{
Date, DocRef, Label, PipelineCampaign, PipelineDocument, PipelineInstance, PipelineRun, PipelineStewardship, PipelineTarget, RunOperator, RunTurn, Sha,
Slug, TargetInvariant,
};
use huginn_mind::{Faculty, Mind, PipelineAdmissionBatch, PipelineAdmissionOutcome, PipelineProvenance};
use mind_body::self_run::FLUSH_MESSAGE;
use super::all_output;
use tempfile::TempDir;
const INSTANCE: &str = "yggdrasil";
const CAMPAIGN: &str = "eureka-body";
const REPO: &str = "GameCult/Huginn";
// Pinned against Python's uuid.uuid5(uuid.NAMESPACE_OID, "yggdrasil:run:selfrun-lane").
const LANE_SESSION: &str = "6bbfe181-08ee-5c29-8e32-0ebe48ec4b33";
fn doc_ref() -> DocRef {
DocRef { path: "docs/eureka-body-target.md".into(), start_line: 1, end_line: 9, commit: Sha("df7507b".into()) }
}
/// The identity, stewardship, campaign and target a run cites, and her one
/// open Self run with budget 7.25.
fn seed() -> Vec<PipelineDocument> {
let date = || Date("2026-10-22".into());
vec![
PipelineDocument::Instance(PipelineInstance { instance: Slug(INSTANCE.into()), display_name: "test mind".into(), created_at: date(), host: "test".into() }),
PipelineDocument::Stewardship(PipelineStewardship { instance: Slug(INSTANCE.into()), repo: REPO.into(), sequence: 1, assigned_on: date(), note: "assigned".into(), names: vec![] }),
PipelineDocument::Campaign(PipelineCampaign {
slug: Slug(CAMPAIGN.into()),
title: "Eureka body".into(),
repos: vec![REPO.into()],
working_branch: "main".into(),
target_doc: doc_ref(),
}),
PipelineDocument::Target(PipelineTarget {
campaign: Slug(CAMPAIGN.into()),
revision: 1,
invariants: vec![TargetInvariant { label: "run-is-the-grant".into(), statement: "A run is the grant.".into() }],
not_in_scope: vec![],
canonical_implementations: vec![],
doc: doc_ref(),
repos: vec![],
}),
PipelineDocument::Run(PipelineRun {
instance: Slug(INSTANCE.into()),
label: Label("selfrun-lane".into()),
operated_by: RunOperator::Mind,
turn: RunTurn::SelfRun,
host: "test".into(),
started_on: date(),
budget_usd: "0".into(),
claims: vec![],
campaigns: vec![Slug(CAMPAIGN.into())],
}),
]
}
/// The daemon, ended with the test whether it passes or not.
struct Daemon(Child);
impl Drop for Daemon {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
/// The daemon over a store seeded with `documents`, and the endpoint it listens on.
/// It needs a built `huginn-daemon` beside the binary (`cargo build -p huginn-daemon`).
fn serve(documents: Vec<PipelineDocument>) -> (Daemon, TempDir, String) {
let state = TempDir::new().unwrap();
admit(state.path(), documents);
let (daemon, endpoint) = daemon_over(state.path());
(daemon, state, endpoint)
}
/// Admits `documents` into the store at `state`, which no daemon holds open.
fn admit(state: &Path, documents: Vec<PipelineDocument>) {
let slug = Slug(INSTANCE.into());
{
let mut mind = Mind::open(state, &slug).unwrap();
let batch = PipelineAdmissionBatch {
instance: slug.clone(),
provenance: PipelineProvenance { faculty: Faculty::Hands, agent: "test".into(), session: "test".into(), tool: "test".into(), imported_from: None },
documents,
};
assert!(matches!(mind.admit(batch, Utc::now()), PipelineAdmissionOutcome::Committed { .. }));
}
}
/// A daemon over the store at `state`, and the endpoint it listens on.
fn daemon_over(state: &Path) -> (Daemon, String) {
let port = UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
let exe = Path::new(env!("CARGO_BIN_EXE_mind-self-run"));
let daemon_exe = exe.with_file_name("huginn-daemon");
assert!(daemon_exe.exists(), "build huginn-daemon first: {}", daemon_exe.display());
let daemon = Daemon(
Command::new(&daemon_exe)
.args(["--state-root", state.to_str().unwrap(), "--instance", INSTANCE, "--bind", &format!("127.0.0.1:{port}")])
.args(["--qdrant-url", "http://127.0.0.1:1", "--ollama-url", "http://127.0.0.1:1", "--embedding-model", "x"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap(),
);
(daemon, format!("rudp://127.0.0.1:{port}"))
}
/// The binary with `bin` first on `PATH`, until the daemon answers (exit 2 before that
/// has started nothing). `SELF_RUN_ENV_CANARY` is a variable of the unit's environment.
fn ask(endpoint: &str, bin: &Path, socket: &Path) -> Output {
ask_in(endpoint, bin, socket, Some(Path::new(&format!("{0}/state:{0}/kept", bin.display()))))
}
/// The same, in a unit whose `StateDirectory=` is `state_directory`.
fn ask_in(endpoint: &str, bin: &Path, socket: &Path, state_directory: Option<&Path>) -> Output {
let exe = Path::new(env!("CARGO_BIN_EXE_mind-self-run"));
let path = format!("{}:/usr/bin:/bin", bin.display());
for _ in 0..40 {
let mut command = Command::new(exe);
if let Some(state_directory) = state_directory {
command.env("STATE_DIRECTORY", state_directory);
}
let attempt = command
.args(["--instance", INSTANCE, "--endpoint", endpoint])
.env("PATH", &path)
.env("SELF_RUN_ENV_CANARY", ENV_CANARY)
.env("CLAUDE_LEASE_SOCKET", socket)
.env("MIND_SELF_RUN_CAP_S", "14400")
.env("RUNTIME_DIRECTORY", bin)
.output()
.unwrap();
if attempt.status.code() != Some(2) {
return attempt;
}
std::thread::sleep(Duration::from_millis(500));
}
panic!("the daemon never answered");
}
/// The connector grant frame (lease v2): a length, then the named-map MessagePack of the grant with one plan window.
fn grant_frame() -> Vec<u8> {
let body = rmp_serde::to_vec_named(&serde_json::json!({"kind": "claude_lease_grant", "value": {"lease_id": "lease:0123456789abcdef0123456789abcdef", "plan_quota": [{
"window": "five_hour", "utilization_permille": 80, "resets_at_unix_ms": 1_793_307_000_000_u64, "status": "allowed", "observed_at_unix_ms": 1_793_306_000_000_u64
}]}})).unwrap();
let mut frame = u32::try_from(body.len()).unwrap().to_be_bytes().to_vec();
frame.extend_from_slice(&body);
frame
}
const LANE_TOKEN: &str = "tok-lane-canary-51c7";
/// The connector's lease socket: every connection gets the grant with a pipe holding
/// `LANE_TOKEN` attached as SCM_RIGHTS, and is then held open until the run lets go.
struct FakeConnector {
_dir: TempDir,
path: std::path::PathBuf,
}
fn read_frame(stream: &mut std::os::unix::net::UnixStream) -> std::io::Result<Vec<u8>> {
let mut length = [0_u8; 4];
stream.read_exact(&mut length)?;
let mut frame = length.to_vec();
frame.resize(4 + u32::from_be_bytes(length) as usize, 0);
stream.read_exact(&mut frame[4..])?;
Ok(frame)
}
fn fake_connector() -> FakeConnector {
let dir = TempDir::new().unwrap();
let path = dir.path().join("lease.sock");
let listener = UnixListener::bind(&path).unwrap();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { return };
std::thread::spawn(move || {
if read_frame(&mut stream).is_err() {
return;
}
let (read, mut write) = std::io::pipe().unwrap();
write.write_all(LANE_TOKEN.as_bytes()).unwrap();
drop(write);
let read: std::os::fd::OwnedFd = read.into();
let fds = [std::os::fd::AsFd::as_fd(&read)];
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
let mut control = rustix::net::SendAncillaryBuffer::new(&mut space);
assert!(control.push(rustix::net::SendAncillaryMessage::ScmRights(&fds)));
rustix::net::sendmsg(&stream, &[IoSlice::new(&grant_frame())], &mut control, rustix::net::SendFlags::empty()).unwrap();
// The lease lasts while this connection is open: the run drops it to end it.
let _ = read_frame(&mut stream);
});
}
});
FakeConnector { _dir: dir, path }
}
const ENV_CANARY: &str = "canary-unit-environment-b94e";
/// Writes an executable `claude` into `bin` and returns the path of the record it keeps.
fn fake_claude(bin: &Path, body: &str) -> std::path::PathBuf {
use std::os::unix::fs::PermissionsExt;
let record = bin.join("record");
let script = format!("#!/bin/bash\necho \"$@\" > {record}\n{body}\n", record = record.display());
let claude = bin.join("claude");
std::fs::write(&claude, script).unwrap();
std::fs::set_permissions(&claude, std::fs::Permissions::from_mode(0o755)).unwrap();
record
}
/// The real binary against the real daemon, with a fake `claude` first on `PATH`.
/// The fake prints two requests (the second reaches the ceiling), waits for the start
/// and the flush on stdin, records its argument list, what it read and the unit
/// variable it inherited, prints a result and exits 0. The bin finds her run, starts
/// the child for it, asks for the flush and exits 0 without echoing the inherited
/// variable. It runs in the container lane only; `cargo test` and `cargo mutants`
/// skip it. Run it with the README's lane command.
#[test]
#[ignore = "container only: a built huginn-daemon beside the binary, and a POSIX shell"]
fn the_real_bin_finds_her_run_starts_claude_for_it_asks_for_the_flush_and_exits_0() {
let (daemon, _state, endpoint) = serve(seed());
let bin = TempDir::new().unwrap();
let request = |id: &str, tokens: u64| {
format!(
r#"{{"type":"assistant","parent_tool_use_id":null,"message":{{"id":"{id}","usage":{{"input_tokens":1,"cache_read_input_tokens":{tokens},"cache_creation_input_tokens":0}}}}}}"#
)
};
let body = format!(
"read -r -u \"$CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR\" cred
echo journal-line >&2
echo '{first}'\necho '{second}'\necho '{echo}'\nread -t 5 start\nread -t 5 flush\necho \"$start\" >> {record}\necho \"$flush\" >> {record}\necho \"$SELF_RUN_ENV_CANARY\" >> {record}\necho \"$cred\" >> {record}\necho '{echo}'\necho '{{\"type\":\"result\"}}'\nwhile read more; do :; done\nexit 0",
first = request("msg_1", 100),
second = request("msg_2", 299_999),
echo = r#"{"type":"user","isReplay":true,"message":{"role":"user","content":"x"}}"#,
record = bin.path().join("record").display(),
);
let record = fake_claude(bin.path(), &body);
let connector = fake_connector();
let output = ask(&endpoint, bin.path(), &connector.path);
drop(daemon);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert_eq!(output.status.code(), Some(0), "{stdout}\n{}", String::from_utf8_lossy(&output.stderr));
let printed: String = stdout.lines().filter(|line| !line.starts_with("decision ") && !line.starts_with("result") && !line.starts_with("session ")).map(|line| format!("{line}\n")).collect();
assert_eq!(printed, "request 1 prefix 101\nrequest 2 prefix 300000\nended: flush-requested code 0 requests 2 prefix 300000\n");
assert!(!all_output(&output).contains(ENV_CANARY), "the unit environment is not echoed");
assert!(String::from_utf8_lossy(&output.stderr).contains("journal-line"), "the child's stderr is the unit's journal");
let recorded = std::fs::read_to_string(&record).unwrap();
let mut lines = recorded.lines();
let argv = lines.next().unwrap();
assert!(argv.contains(&format!("--session-id {LANE_SESSION}")) && !argv.contains("--max-budget-usd"), "{argv}");
assert!(argv.starts_with("-p --input-format stream-json") && argv.contains("--replay-user-messages") && !argv.contains("--settings"), "{argv}");
assert!(lines.next().unwrap().contains("yggdrasil:run:selfrun-lane"), "the start message names her run");
let flush = lines.next().unwrap();
assert!(flush.contains(FLUSH_MESSAGE) && flush.contains("\"type\":\"user\""), "the flush went to the child's stdin: {flush}");
assert_eq!(lines.next().unwrap(), "", "the child gets the allow-listed variables only, not the rest of the unit environment");
assert_eq!(lines.next().unwrap(), LANE_TOKEN, "the child read its credential from the lease's descriptor");
assert!(!all_output(&output).contains(LANE_TOKEN), "the token is never in the bin's output");
}
/// With no Self run of hers in the mind the bin starts no child and exits 3.
#[test]
#[ignore = "container only: a built huginn-daemon beside the binary, and a POSIX shell"]
fn the_real_bin_with_no_self_run_of_hers_exits_3_and_never_starts_claude() {
let mut documents = seed();
documents.pop();
let (daemon, _state, endpoint) = serve(documents);
let bin = TempDir::new().unwrap();
let record = fake_claude(bin.path(), "exit 0");
let connector = fake_connector();
let output = ask(&endpoint, bin.path(), &connector.path);
drop(daemon);
assert_eq!(output.status.code(), Some(3), "{}", all_output(&output));
assert_eq!(String::from_utf8_lossy(&output.stdout), "no run: she has no single open Self run\n");
assert!(!record.exists(), "claude was started");
}
/// Two real `mind-self-run` processes share one `STATE_DIRECTORY`. The fake `claude`
/// writes its transcript where the CLI would, prints a request that wrote an hour's
/// cache and an ok result, and exits 0: a clean end inside the cache's life. The second
/// process, under a second run, resumes the first's kept transcript by its path, forked.
#[test]
#[ignore = "container only: a built huginn-daemon beside the binary, and a POSIX shell"]
fn two_real_processes_sharing_a_state_directory_resume_across_the_unit_boundary() {
use eureka_pipeline::{PipelineKind, PipelineRef};
use mind_body::self_run::session_id;
let (daemon, state, endpoint) = serve(seed());
let units_state = TempDir::new().unwrap();
let bin = TempDir::new().unwrap();
let argv_log = bin.path().join("argv-log");
let body = format!(
"sid=''; prev=''; for a in \"$@\"; do [ \"$prev\" = --session-id ] && sid=$a; prev=$a; done
echo \"$@\" >> {argv_log}
mkdir -p \"$CLAUDE_CONFIG_DIR/projects/p\"; echo '{{}}' > \"$CLAUDE_CONFIG_DIR/projects/p/$sid.jsonl\"
read -t 5 start
echo '{request}'\necho '{echo}'\necho '{result}'\nexit 0",
argv_log = argv_log.display(),
request = r#"{"type":"assistant","parent_tool_use_id":null,"message":{"id":"msg_1","usage":{"input_tokens":1,"cache_read_input_tokens":100,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_1h_input_tokens":5}}}}"#,
echo = r#"{"type":"user","isReplay":true,"message":{"role":"user","content":"x"}}"#,
result = r#"{"type":"result","subtype":"success","is_error":false,"result":""}"#,
);
fake_claude(bin.path(), &body);
let connector = fake_connector();
let first = ask_in(&endpoint, bin.path(), &connector.path, Some(Path::new(&format!("{0}/work:{0}/kept", units_state.path().display()))));
assert_eq!(first.status.code(), Some(0), "{}", all_output(&first));
let kept = units_state.path().join("kept");
let mut names: Vec<String> = std::fs::read_dir(&kept).unwrap().map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()).collect();
names.sort();
assert_eq!(names, [format!("{LANE_SESSION}.jsonl"), "kept.json".to_string()], "the first process kept its conversation");
// Her second run: the first closed itself, so the store is quiet while the next is admitted.
drop(daemon);
let mut second_run = seed().pop().unwrap();
let PipelineDocument::Run(run) = &mut second_run else { unreachable!() };
run.label = eureka_pipeline::Label("selfrun-lane-two".into());
admit(state.path(), vec![second_run]);
let (daemon, endpoint) = daemon_over(state.path());
let second = ask_in(&endpoint, bin.path(), &connector.path, Some(Path::new(&format!("{0}/work:{0}/kept", units_state.path().display()))));
drop(daemon);
assert_eq!(second.status.code(), Some(0), "{}", all_output(&second));
let second_session = session_id(&PipelineRef { kind: PipelineKind::Run, id: format!("{INSTANCE}:run:selfrun-lane-two").as_str().into() });
let argvs = std::fs::read_to_string(&argv_log).unwrap();
let second_argv = argvs.lines().nth(1).expect("the second process started");
assert!(second_argv.contains(&format!("--session-id {second_session}")), "{second_argv}");
assert!(second_argv.contains(&format!("--resume {}/{LANE_SESSION}.jsonl --fork-session", kept.display())), "{second_argv}");
assert!(!argvs.lines().next().unwrap().contains("--resume"), "the first process had nothing to resume");
let stdout = String::from_utf8_lossy(&second.stdout).to_string();
assert!(stdout.contains(&format!("session resumed {LANE_SESSION}")), "{stdout}");
let mut names: Vec<String> = std::fs::read_dir(&kept).unwrap().map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()).collect();
names.sort();
assert_eq!(names, [format!("{second_session}.jsonl"), "kept.json".to_string()], "one transcript, the newer");
}
}