Huginn/crates/eureka-state/tests/cli.rs
Meta Vens 02cb3dadcd huginn CLI over HuginnClient: whoami, view, query, recipes, admit with read-back, schema
A new bin of eureka-state, crates/eureka-state/src/bin/huginn/: the agents'
shell door to the mind. Text out, typed exit codes, strict decode of
agent-written JSON (unknown key paths refused, never values), the named
campaign-state recipes, and a read-back after every committed admission.
lib.rs, main.rs and tests/mcp.rs are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:01:01 +02:00

951 lines
39 KiB
Rust

//! The `huginn` binary as an agent drives it: a real process, its stdout, its
//! stderr and its exit code, against a real daemon in-process or a scripted
//! server where the daemon cannot be made to misbehave.
use std::io::Write;
use std::net::{SocketAddr, UdpSocket};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use cultnet_rs::Selection;
use huginn_mind::envelope::{OperationFailure, decode_request, encode_failure, encode_response};
use huginn_mind::eureka_pipeline::{
CodeLocation, CommitRange, CutVerification, Date, Evidence, EvidenceKind, MutationRecord, OrgRepo, PipelineCutReport,
PipelineCutSpec, PipelineDocument, PipelineKind, PipelineRef, PipelineRuling, Promise, ReportCommit, RulingAuthority, Short,
StructuralDelta, pipeline_key,
};
use huginn_mind::{
AdmissionFacts, HuginnMindRequest, HuginnMindResponse, IndexStatus, MindStatus, PipelineAdmissionOutcome, PipelineDocumentView,
PipelinePageItems, PipelineProvenance, PipelineSelectionPage, PipelineStatus,
};
use serde_json::{Value, json};
use tempfile::TempDir;
mod common;
use common::*;
struct Run {
code: i32,
out: String,
err: String,
}
fn run(env: &[(&str, &str)], args: &[&str], stdin: Option<&str>) -> Run {
let mut command = Command::new(env!("CARGO_BIN_EXE_huginn"));
command
.env_remove("EUREKA_INSTANCE")
.env_remove("HUGINN_ENDPOINT")
.envs(env.iter().copied())
.args(args)
.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().expect("the binary starts");
if let Some(text) = stdin {
child.stdin.take().unwrap().write_all(text.as_bytes()).unwrap();
}
let output = child.wait_with_output().unwrap();
Run {
code: output.status.code().expect("exited, not killed"),
out: String::from_utf8(output.stdout).unwrap(),
err: String::from_utf8(output.stderr).unwrap(),
}
}
/// Configured for the test instance against a daemon at `addr`.
fn at(addr: SocketAddr, args: &[&str], stdin: Option<&str>) -> Run {
run(&[("EUREKA_INSTANCE", INSTANCE), ("HUGINN_ENDPOINT", &format!("rudp://{addr}"))], args, stdin)
}
/// A loopback UDP port nothing listens on.
fn closed_port() -> SocketAddr {
UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap()
}
fn write(dir: &TempDir, name: &str, value: &Value) -> String {
let path = dir.path().join(name);
std::fs::write(&path, value.to_string()).unwrap();
path.to_str().unwrap().to_string()
}
fn campaign_json() -> Value {
serde_json::to_value(campaign()).unwrap()
}
fn live() -> Server {
let (root, daemon) = mind(vec![vec![stewardship()]]);
serve(root, daemon)
}
fn admit_args(file: &str) -> Vec<&str> {
vec!["admit", "--faculty", "Hands", "--agent", "claude", "--session", "session-7", file]
}
fn delta() -> StructuralDelta {
StructuralDelta {
lines_added: 0,
lines_removed: 0,
dependencies_added: vec![],
dependencies_removed: vec![],
formats_added: vec![],
formats_removed: vec![],
targets_added: vec![],
targets_removed: vec![],
}
}
fn spec(cut: &str) -> Value {
serde_json::to_value(PipelineDocument::CutSpec(PipelineCutSpec {
campaign: slug(CAMPAIGN),
cut: cut.into(),
revision: 1,
title: "A cut".into(),
repo: OrgRepo(REPO.into()),
branch: "codex/eureka-pipeline-state".into(),
base: sha(),
depends_on: vec![],
first: vec![],
deletes: vec![],
keeps_moves: vec![],
adds: vec![],
file_changes: vec![],
reads: vec![],
authority_map: None,
verification: CutVerification { builds: vec![], tests: vec![], negative: vec![], operator: vec![] },
estimate: delta(),
rulings: vec![],
questions: vec![],
}))
.unwrap()
}
fn report(cut: &str) -> Value {
serde_json::to_value(PipelineDocument::CutReport(PipelineCutReport {
campaign: slug(CAMPAIGN),
cut_spec: Short(format!("{CAMPAIGN}:cut_spec:cut-{cut}.r1")),
attempt: 1,
repo: OrgRepo(REPO.into()),
branch: "codex/eureka-pipeline-state".into(),
commits: vec![ReportCommit { sha: sha(), subject: "Land the cut".into(), builds: true }],
range: CommitRange { base: sha(), head: sha() },
verification: vec![Evidence { kind: EvidenceKind::Test, locator: "cargo test".into(), result: "ok".into() }],
mutations: vec![MutationRecord {
label: "M1".into(),
rule: "the rule".into(),
location: CodeLocation { path: Short("crates/x.rs".into()), line: 1, end_line: None, symbol: None },
before: "before".into(),
after: "after".into(),
commit: sha(),
failed_as_expected: true,
}],
deviations: vec![],
forks: vec![],
structural_delta: delta(),
landed_names: vec![],
undone: vec![],
promises: vec![Promise { label: "P1".into(), text: "One derived key per document.".into() }],
}))
.unwrap()
}
fn ruling(quote: Option<&str>) -> PipelineDocument {
PipelineDocument::Ruling(PipelineRuling {
campaign: slug(CAMPAIGN),
label: "R1".into(),
title: "An instance owns its mind".into(),
answers: None,
choice: None,
ruling: "An instance owns its mind.".into(),
operator_quote: quote.map(Into::into),
ruled_on: Date("2026-10-10".into()),
precedents: vec![],
authority: RulingAuthority::Standing,
})
}
fn empty_page() -> PipelineSelectionPage {
PipelineSelectionPage { matched: 0, as_of: 1, next: None, items: PipelinePageItems::Headers(vec![]), edges: None }
}
fn answer(operation: &str, response: &HuginnMindResponse) -> cultnet_rs::CultNetMessage {
encode_response("eureka-state-call", operation, response, "scripted").unwrap()
}
/// A scripted server that records every request it is sent and answers a
/// query with an empty page.
fn recording() -> (Server, Arc<Mutex<Vec<HuginnMindRequest>>>) {
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&seen);
let server = scripted(move |message| {
let (_, request) = decode_request(message).expect("a mind request");
sink.lock().unwrap().push(request);
answer("query", &HuginnMindResponse::Query(empty_page()))
});
(server, seen)
}
/// The selection a command sent, and its semantic part.
fn sent(args: &[&str]) -> (Selection, Option<(String, u32)>) {
let (server, seen) = recording();
let ran = at(server.addr, args, None);
assert_eq!(ran.code, 0, "{args:?}: {}", ran.err);
assert!(ran.out.contains("matched 0 as_of 1 next -"), "{}", ran.out);
let seen = seen.lock().unwrap();
let [HuginnMindRequest::Query { instance, selection, semantic }] = &seen[..] else {
panic!("one query expected: {seen:?}");
};
assert_eq!(instance.0, INSTANCE);
(selection.clone(), semantic.as_ref().map(|asked| (asked.text.0.clone(), asked.top_k)))
}
/// The page's literal as the selection it spells, with the CLI's default page.
fn page_literal(literal: Value) -> Selection {
let mut selection: Selection = serde_json::from_value(literal).unwrap();
selection.limit = Some(50);
selection
}
fn counting(replies: HuginnMindResponse, operation: &'static str) -> (Server, Arc<Mutex<usize>>) {
let count = Arc::new(Mutex::new(0));
let counter = Arc::clone(&count);
let server = scripted(move |_| {
*counter.lock().unwrap() += 1;
answer(operation, &replies)
});
(server, count)
}
#[test]
fn without_configuration_whoami_reports_it_and_every_other_command_refuses() {
let dir = tempfile::tempdir().unwrap();
let file = write(&dir, "campaign.json", &json!([campaign_json()]));
let whoami = run(&[], &["whoami"], None);
assert_eq!(whoami.code, 2);
assert!(whoami.out.contains("instance unset\nendpoint unset\nclient "), "{}", whoami.out);
assert!(whoami.out.contains("reachable no"), "{}", whoami.out);
assert!(whoami.err.starts_with("Misconfigured: EUREKA_INSTANCE is not set"), "{}", whoami.err);
for args in [
vec!["view", "eureka-state:campaign:self"],
vec!["query", "{}"],
vec!["rulings", "eureka-state"],
admit_args(&file),
] {
let ran = run(&[], &args, None);
assert_eq!(ran.code, 2, "{args:?}");
assert!(ran.err.starts_with("Misconfigured:"), "{args:?}: {}", ran.err);
assert_eq!(ran.out, "", "{args:?}");
}
}
#[test]
fn a_bad_environment_is_misconfigured_with_its_own_reason() {
let cases: [(&[(&str, &str)], &str); 4] = [
(&[("HUGINN_ENDPOINT", "rudp://127.0.0.1:1")], "EUREKA_INSTANCE is not set"),
(&[("EUREKA_INSTANCE", "Not A Slug"), ("HUGINN_ENDPOINT", "rudp://127.0.0.1:1")], "EUREKA_INSTANCE is not a valid"),
(&[("EUREKA_INSTANCE", "eureka")], "HUGINN_ENDPOINT is not set"),
(&[("EUREKA_INSTANCE", "eureka"), ("HUGINN_ENDPOINT", "127.0.0.1:1")], "HUGINN_ENDPOINT is not rudp://"),
];
for (env, expected) in cases {
let ran = run(env, &["whoami"], None);
assert_eq!(ran.code, 2, "{expected}");
assert!(ran.err.starts_with("Misconfigured: "), "{}", ran.err);
assert!(ran.err.contains(expected), "{expected}: {}", ran.err);
}
let named = run(&[("EUREKA_INSTANCE", "eureka"), ("HUGINN_ENDPOINT", "rudp://not-an-address")], &["whoami"], None);
assert_eq!(named.code, 2);
assert!(named.out.contains("endpoint rudp://not-an-address"), "{}", named.out);
assert!(named.out.contains("instance eureka"), "{}", named.out);
}
#[test]
fn whoami_reads_the_live_mind() {
let server = live();
let ran = at(server.addr, &["whoami"], None);
assert_eq!(ran.code, 0, "{}", ran.err);
let lines: Vec<&str> = ran.out.lines().collect();
assert_eq!(lines[0], format!("instance {INSTANCE}"));
assert_eq!(lines[1], format!("endpoint rudp://{}", server.addr));
assert!(lines[2].starts_with("client "), "{lines:?}");
assert_eq!(lines[3], "reachable yes");
assert_eq!(lines[4], "documents 2 receipts 2 index Current epoch epiphany.pipeline.epoch.v2");
assert_eq!(lines.len(), 5);
assert_eq!(ran.err, "");
}
#[test]
fn an_index_that_is_behind_shows_its_count() {
let status = MindStatus {
instance: slug(INSTANCE),
schema_epoch: "epoch".into(),
documents: 7,
receipts: 9,
index: IndexStatus::Behind { pending: 3 },
};
let (server, _) = counting(HuginnMindResponse::Whoami(status), "whoami");
let ran = at(server.addr, &["whoami"], None);
assert_eq!(ran.code, 0);
assert!(ran.out.contains("documents 7 receipts 9 index Behind pending=3 epoch epoch"), "{}", ran.out);
}
#[test]
fn admit_round_trips_and_is_idempotent() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let file = write(&dir, "campaign.json", &json!([campaign_json()]));
let first = at(server.addr, &admit_args(&file), None);
assert_eq!(first.code, 0, "{}", first.err);
let lines: Vec<&str> = first.out.lines().collect();
assert!(lines[0].starts_with("Committed mind-commit-"), "{}", first.out);
assert!(lines[0].contains(" at "), "{}", first.out);
assert_eq!(lines[1], format!("wrote {CAMPAIGN}:campaign:self"));
assert_eq!(lines[2], "read back 1 equal");
assert_eq!(lines.len(), 3, "{}", first.out);
let again = at(server.addr, &admit_args(&file), None);
assert_eq!(again.code, 0, "{}", again.err);
let lines: Vec<&str> = again.out.lines().collect();
assert!(lines[0].starts_with("AlreadyAdmitted mind-commit-"), "{}", again.out);
assert_eq!(lines[1], "read back 1 equal");
let id = format!("{CAMPAIGN}:campaign:self");
let viewed = at(server.addr, &["view", &id], None);
assert_eq!(viewed.code, 0, "{}", viewed.err);
let lines: Vec<&str> = viewed.out.lines().collect();
assert_eq!(lines[0], format!("{id} InForce"));
assert!(lines[1].starts_with("admitted "), "{}", viewed.out);
assert!(lines[1].contains(" ordinal 3 Hands/claude session session-7 tool huginn receipt mind-commit-"), "{}", lines[1]);
let shown: Value = serde_json::from_str(&lines[2..].join("\n")).unwrap();
assert_eq!(shown, campaign_json());
let raw = at(server.addr, &["view", &id, "--raw"], None);
assert_eq!(raw.code, 0);
assert_eq!(serde_json::from_str::<Value>(&raw.out).unwrap(), campaign_json());
assert!(raw.out.starts_with('{'), "{}", raw.out);
let fed_back = at(server.addr, &admit_args("-"), Some(&raw.out));
assert_eq!(fed_back.code, 0, "{}", fed_back.err);
assert!(fed_back.out.starts_with("AlreadyAdmitted "), "{}", fed_back.out);
let status = at(server.addr, &["whoami"], None);
assert!(status.out.contains("documents 3 receipts 3"), "{}", status.out);
}
#[test]
fn unknown_keys_are_refused_by_path_and_never_by_value() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let mut nested = campaign_json();
nested["value"]["target_doc"]["pathh"] = json!("CANARY-NESTED");
let mut top = campaign_json();
top["surplus"] = json!("CANARY-TOP");
let file = write(&dir, "bad.json", &json!([nested, top]));
let ran = at(server.addr, &admit_args(&file), None);
assert_eq!(ran.code, 2);
assert_eq!(ran.out, "");
assert!(ran.err.starts_with("InvalidInput: "), "{}", ran.err);
assert!(ran.err.contains("documents[0].value.target_doc.pathh"), "{}", ran.err);
assert!(ran.err.contains("documents[1].surplus"), "{}", ran.err);
assert!(!ran.err.contains("CANARY"), "{}", ran.err);
let status = at(server.addr, &["whoami"], None);
assert!(status.out.contains("receipts 2 "), "nothing was sent: {}", status.out);
let selection = at(server.addr, &["query", r#"{"limt": 5}"#], None);
assert_eq!(selection.code, 2);
assert!(selection.err.contains("selection.limt"), "{}", selection.err);
}
#[test]
fn an_explicitly_empty_unknown_key_is_not_a_loss() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let mut doc = campaign_json();
doc["value"]["ghost"] = Value::Null;
doc["value"]["ghosts"] = json!([]);
doc["value"]["ghost_map"] = json!({});
doc["value"]["ghost_text"] = json!("");
let file = write(&dir, "ghosts.json", &json!([doc]));
let ran = at(server.addr, &admit_args(&file), None);
assert_eq!(ran.code, 0, "{}", ran.err);
assert!(ran.out.contains("read back 1 equal"), "{}", ran.out);
}
#[test]
fn a_falsy_unknown_value_is_still_a_loss() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let mut doc = campaign_json();
doc["value"]["zero"] = json!(0);
doc["value"]["no"] = json!(false);
let file = write(&dir, "falsy.json", &json!([doc]));
let ran = at(server.addr, &admit_args(&file), None);
assert_eq!(ran.code, 2);
assert!(ran.err.contains("documents[0].value.zero") && ran.err.contains("documents[0].value.no"), "{}", ran.err);
}
#[test]
fn a_document_that_does_not_decode_is_invalid_input() {
let dir = tempfile::tempdir().unwrap();
let cases = [
(json!([{ "kind": "nonsense", "value": {} }]), "documents[0] does not decode"),
(json!("text"), "JSON array"),
(json!([]), "1 to 64"),
(json!((0..65).map(|_| campaign_json()).collect::<Vec<_>>()), "1 to 64"),
];
for (n, (value, expected)) in cases.into_iter().enumerate() {
let file = write(&dir, &format!("case{n}.json"), &value);
let ran = at(closed_port(), &admit_args(&file), None);
assert_eq!(ran.code, 2, "{expected}: {}", ran.err);
assert!(ran.err.starts_with("InvalidInput: "), "{}", ran.err);
assert!(ran.err.contains(expected), "{expected}: {}", ran.err);
}
let broken = at(closed_port(), &admit_args("-"), Some("{ not json"));
assert_eq!(broken.code, 2);
assert!(broken.err.contains("not JSON"), "{}", broken.err);
let missing = at(closed_port(), &admit_args("/no/such/file.json"), None);
assert_eq!(missing.code, 2);
assert!(missing.err.contains("cannot read /no/such/file.json"), "{}", missing.err);
}
fn view_of(document: &PipelineDocument) -> PipelineDocumentView {
PipelineDocumentView {
id: PipelineRef { kind: document.kind(), id: Short(pipeline_key(document).unwrap()) },
document: document.clone(),
admission: AdmissionFacts {
receipt_id: "mind-commit-x".into(),
admitted_at: "2026-10-10T00:00:00Z".into(),
provenance: PipelineProvenance {
faculty: huginn_mind::Faculty::Hands,
agent: Short("a".into()),
session: Short("s".into()),
tool: Short("huginn".into()),
},
ordinal: 1,
},
status: PipelineStatus::InForce,
}
}
/// Admits `Committed` and answers the keys query with `stored`.
fn committing(stored: Vec<PipelineDocumentView>) -> Server {
scripted(move |message| {
let (_, request) = decode_request(message).expect("a mind request");
match request {
HuginnMindRequest::Admit(_) => answer(
"admit",
&HuginnMindResponse::Admit(PipelineAdmissionOutcome::Committed {
receipt_id: "mind-commit-x".into(),
committed_at: "2026-10-10T00:00:00Z".into(),
writes: vec![],
}),
),
HuginnMindRequest::Query { selection, .. } => {
assert_eq!(selection.projection, "document");
assert_eq!(selection.limit, Some(u32::try_from(selection.keys.as_ref().unwrap().len()).unwrap()));
let page = PipelineSelectionPage {
matched: u32::try_from(stored.len()).unwrap(),
as_of: 2,
next: None,
items: PipelinePageItems::Documents(stored.clone()),
edges: None,
};
answer("query", &HuginnMindResponse::Query(page))
}
other => panic!("unexpected {other:?}"),
}
})
}
#[test]
fn a_field_the_mind_dropped_is_reported_with_the_receipt_printed() {
let dir = tempfile::tempdir().unwrap();
let sent = ruling(Some("CANARY-QUOTE"));
let file = write(&dir, "ruling.json", &json!([sent]));
let key = pipeline_key(&sent).unwrap();
let server = committing(vec![view_of(&ruling(None))]);
let ran = at(server.addr, &admit_args(&file), None);
assert_eq!(ran.code, 5);
assert!(ran.out.starts_with("Committed mind-commit-x at 2026-10-10T00:00:00Z"), "{}", ran.out);
assert!(!ran.out.contains("read back"), "{}", ran.out);
assert!(ran.err.starts_with(&format!("ReadBackDiffers: {key}: value.operator_quote;")), "{}", ran.err);
assert!(ran.err.contains("supersede"), "{}", ran.err);
let missing = committing(vec![]);
let ran = at(missing.addr, &admit_args(&file), None);
assert_eq!(ran.code, 5);
assert!(ran.err.contains(&format!("{key}: missing from the read-back")), "{}", ran.err);
let equal = committing(vec![view_of(&sent)]);
let ran = at(equal.addr, &admit_args(&file), None);
assert_eq!(ran.code, 0, "{}", ran.err);
assert!(ran.out.ends_with("read back 1 equal\n"), "{}", ran.out);
}
#[test]
fn a_difference_deep_in_a_list_names_its_path() {
let dir = tempfile::tempdir().unwrap();
let sent = serde_json::from_value::<PipelineDocument>(spec("A")).unwrap();
let mut stored = sent.clone();
if let PipelineDocument::CutSpec(value) = &mut stored {
value.rulings = vec![Short("x:ruling:R1".into())];
value.title = "Another".into();
}
let file = write(&dir, "spec.json", &json!([sent]));
let server = committing(vec![view_of(&stored)]);
let ran = at(server.addr, &admit_args(&file), None);
assert_eq!(ran.code, 5);
assert!(ran.err.contains("value.rulings[0]") || ran.err.contains("value.rulings"), "{}", ran.err);
assert!(ran.err.contains("value.title"), "{}", ran.err);
}
#[test]
fn exit_codes_follow_the_answer() {
let dir = tempfile::tempdir().unwrap();
let file = write(&dir, "campaign.json", &json!([campaign_json()]));
// Nothing listening: Unavailable, naming the endpoint, whoami included.
let port = closed_port();
let down = at(port, &["view", "eureka-state:campaign:self"], None);
assert_eq!(down.code, 3);
assert!(down.err.starts_with("Unavailable: "), "{}", down.err);
assert!(down.err.contains(&format!("rudp://{port}")), "{}", down.err);
let whoami = at(port, &["whoami"], None);
assert_eq!(whoami.code, 3);
assert!(whoami.out.contains("reachable no"), "{}", whoami.out);
// The daemon rejecting the envelope.
let rejecting = scripted(|_| {
encode_failure(
"eureka-state-call",
"view",
&OperationFailure { code: "wrong-service".into(), message: "not huginn.mind".into() },
"scripted",
)
});
let rejected = at(rejecting.addr, &["view", "eureka-state:campaign:self"], None);
assert_eq!(rejected.code, 4);
assert!(rejected.err.starts_with("Rejected: "), "{}", rejected.err);
assert!(rejected.err.contains("wrong-service"), "{}", rejected.err);
// An answer of another operation's shape.
let status = MindStatus {
instance: slug(INSTANCE),
schema_epoch: "e".into(),
documents: 0,
receipts: 0,
index: IndexStatus::Current,
};
let (odd, _) = counting(HuginnMindResponse::Whoami(status), "view");
let internal = at(odd.addr, &["view", "eureka-state:campaign:self"], None);
assert_eq!(internal.code, 4);
assert!(internal.err.starts_with("Internal: "), "{}", internal.err);
// A batch no single send carries, refused before sending.
let mut huge = campaign_json();
huge["value"]["working_branch"] = json!("b".repeat(90_000));
let big = write(&dir, "huge.json", &json!([huge]));
let too_large = at(closed_port(), &admit_args(&big), None);
assert_eq!(too_large.code, 2);
assert!(too_large.err.starts_with("TooLarge: "), "{}", too_large.err);
assert!(too_large.err.contains(&eureka_state::MAX_REQUEST_BYTES.to_string()), "{}", too_large.err);
// The mind saying no: a refusal is an answer on stdout.
let live = live();
let refused = at(live.addr, &["admit", "--faculty", "Hands", "--agent", "a", "--session", "", &file], None);
assert_eq!(refused.code, 1);
assert!(refused.out.starts_with("Refused "), "{}", refused.out);
assert!(refused.out.contains("provenance.session"), "{}", refused.out);
assert_eq!(refused.err, "");
// Absent: the mind answered null.
let absent = at(live.addr, &["view", "eureka-state:cut_spec:cut-99.r1"], None);
assert_eq!(absent.code, 1);
assert_eq!(absent.out, "absent eureka-state:cut_spec:cut-99.r1\n");
// Another instance's mind: refused on the read side too.
let foreign = run(
&[("EUREKA_INSTANCE", "thought-cage"), ("HUGINN_ENDPOINT", &format!("rudp://{}", live.addr))],
&["query", "{}"],
None,
);
assert_eq!(foreign.code, 1);
assert!(foreign.out.starts_with("Refused "), "{}", foreign.out);
}
#[test]
fn a_call_that_is_wrong_exits_two_without_sending() {
let (server, count) = counting(HuginnMindResponse::View(None), "view");
for args in [
vec!["view", "eureka-state:nonsense:x"],
vec!["view", "no-kind-segment"],
vec!["history", "eureka-state:nonsense:x"],
vec!["query", "{ not json"],
vec!["query", "[]"],
vec!["admit", "--faculty", "Nope", "--agent", "a", "--session", "s", "x.json"],
vec!["schema", "no-such-kind"],
vec!["bogus"],
vec!["view"],
vec![],
] {
let ran = at(server.addr, &args, None);
assert_eq!(ran.code, 2, "{args:?}");
assert!(ran.err.starts_with("InvalidInput: "), "{args:?}: {}", ran.err);
assert_eq!(ran.out, "", "{args:?}");
}
assert_eq!(*count.lock().unwrap(), 0);
}
#[test]
fn the_recipes_send_exactly_the_pages_selections() {
let schema = |kind: &str| format!("epiphany.pipeline.{kind}.v2");
let root = |values: &[&str]| json!({ "index": "root", "op": "any_of", "values": values });
let in_force = json!({ "index": "in_force", "op": "any_of", "values": ["true"] });
let c = "eureka-substrate";
let cases = [
(vec!["campaigns"], json!({ "schemas": [schema("campaign")] })),
(
vec!["target", c],
json!({ "schemas": [schema("target")], "fields": [root(&[c]), in_force], "projection": "document" }),
),
(
vec!["rulings", c, "eureka-body"],
json!({ "schemas": [schema("ruling")], "fields": [root(&[c, "eureka-body"]), in_force] }),
),
(
vec!["open", c],
json!({ "schemas": [schema("question"), schema("follow_up")], "fields": [root(&[c]), in_force] }),
),
(
vec!["findings", c],
json!({
"schemas": [schema("finding")],
"fields": [root(&[c]), in_force, { "index": "faculty", "op": "any_of", "values": ["Soul"] }],
}),
),
(
vec!["findings", c, "--blocking"],
json!({
"schemas": [schema("finding")],
"fields": [
root(&[c]),
in_force,
{ "index": "faculty", "op": "any_of", "values": ["Soul"] },
{ "index": "severity", "op": "any_of", "values": ["Blocker", "High"] },
],
}),
),
(
vec!["unreported", c],
json!({
"schemas": [schema("cut_spec")],
"fields": [root(&[c]), in_force],
"cited": { "role": "cut_spec", "exists": false },
}),
),
(
vec!["blocked", "eureka-substrate:cut_spec:cut-x.r1"],
json!({
"schemas": [schema("question")],
"fields": [in_force],
"cites": {
"target": { "schemaId": schema("cut_spec"), "recordKey": "eureka-substrate:cut_spec:cut-x.r1" },
"role": "raised_in",
},
}),
),
(
vec!["unjudged", c],
json!({
"schemas": [schema("cut_report")],
"fields": [root(&[c])],
"cited": { "role": "cut_report", "exists": false },
}),
),
(
vec!["history", "eureka-substrate:ruling:R1"],
json!({
"schemas": [schema("resolution")],
"cites": { "target": { "schemaId": schema("ruling"), "recordKey": "eureka-substrate:ruling:R1" }, "role": "subject" },
"descending": true,
}),
),
(
vec!["cut", c, "huginn-cli"],
json!({
"schemas": [schema("cut_spec"), schema("cut_report"), schema("verdict"), schema("finding"), schema("resolution")],
"fields": [root(&[c]), { "index": "cut", "op": "any_of", "values": ["huginn-cli"] }],
}),
),
(
vec!["ledger", c],
json!({ "schemas": [schema("cut_spec"), schema("cut_report")], "fields": [root(&[c])], "projection": "document" }),
),
(
vec!["precedent", c, "the question in plain words"],
json!({ "schemas": [schema("ruling"), schema("resolution"), schema("finding")], "fields": [root(&[c])] }),
),
(
vec!["gaps"],
json!({ "schemas": [schema("follow_up")], "fields": [root(&["eureka-substrate"]), in_force] }),
),
];
for (args, literal) in cases {
let (selection, semantic) = sent(&args);
assert_eq!(selection, page_literal(literal), "{args:?}");
let expected = (args[0] == "precedent").then(|| ("the question in plain words".to_string(), 10));
assert_eq!(semantic, expected, "{args:?}");
}
}
#[test]
fn paging_and_query_flags_shape_the_selection() {
let (selection, semantic) = sent(&["gaps", "--cursor", "abc", "--documents"]);
assert_eq!(selection.cursor.as_deref(), Some("abc"));
assert_eq!(selection.projection, "document");
assert_eq!(selection.limit, Some(50));
assert_eq!(semantic, None);
let (selection, _) = sent(&["gaps"]);
assert_eq!(selection.cursor, None);
assert_eq!(selection.projection, "header");
let (selection, semantic) = sent(&["query", "{}", "--near", "drift", "--top-k", "7"]);
assert_eq!(selection, page_literal(json!({})));
assert_eq!(semantic, Some(("drift".to_string(), 7)));
let (_, semantic) = sent(&["query", "{}", "--near", "drift"]);
assert_eq!(semantic, Some(("drift".to_string(), 10)));
let (selection, _) = sent(&["query", r#"{"limit": 5, "descending": true}"#]);
assert_eq!(selection.limit, Some(5));
assert!(selection.descending);
let (selection, _) = sent(&["query", r#"{"projection": "header"}"#, "--documents"]);
assert_eq!(selection.projection, "document");
let ran = at(closed_port(), &["query", "{}", "--top-k", "3"], None);
assert_eq!(ran.code, 2, "--top-k needs --near");
let dir = tempfile::tempdir().unwrap();
let file = write(&dir, "selection.json", &json!({ "schemas": ["epiphany.pipeline.campaign.v2"] }));
let (selection, _) = sent(&["query", &format!("@{file}")]);
assert_eq!(selection.schemas, Some(vec!["epiphany.pipeline.campaign.v2".to_string()]));
let (server, seen) = recording();
let ran = at(server.addr, &["query", "-"], Some(r#"{"keys": ["a:campaign:self"]}"#));
assert_eq!(ran.code, 0, "{}", ran.err);
let seen = seen.lock().unwrap();
let [HuginnMindRequest::Query { selection, .. }] = &seen[..] else { panic!("{seen:?}") };
assert_eq!(selection.keys, Some(vec!["a:campaign:self".to_string()]));
}
#[test]
fn recipes_list_what_the_mind_holds() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let campaign = write(&dir, "campaign.json", &json!([campaign_json()]));
let ran = at(server.addr, &admit_args(&campaign), None);
assert_eq!(ran.code, 0, "{}", ran.err);
let specs = write(&dir, "specs.json", &json!([spec("A"), spec("B")]));
let ran = at(server.addr, &admit_args(&specs), None);
assert_eq!(ran.code, 0, "{}", ran.err);
let unreported = at(server.addr, &["unreported", CAMPAIGN], None);
assert_eq!(unreported.code, 0, "{}", unreported.err);
let mut lines: Vec<&str> = unreported.out.lines().collect();
assert_eq!(lines.pop().unwrap().split(' ').take(2).collect::<Vec<_>>(), ["matched", "2"]);
assert_eq!(lines.len(), 2);
let reports = write(&dir, "report.json", &json!([report("B")]));
let ran = at(server.addr, &admit_args(&reports), None);
assert_eq!(ran.code, 0, "{}", ran.err);
let unreported = at(server.addr, &["unreported", CAMPAIGN], None);
let lines: Vec<&str> = unreported.out.lines().collect();
assert_eq!(lines.len(), 2, "{}", unreported.out);
let tokens: Vec<&str> = lines[0].split(' ').collect();
let id = format!("{CAMPAIGN}:cut_spec:cut-A.r1");
assert_eq!(tokens[..5], [id.as_str(), "InForce", "Hands", "A", "cut"]);
for fact in ["cut=A", "repo=GameCult/Epiphany", "revision=1", "base=5f98228d", "branch=codex/eureka-pipeline-state"] {
assert!(tokens.contains(&fact), "{fact}: {}", lines[0]);
}
assert!(lines[1].starts_with("matched 1 as_of "), "{}", lines[1]);
assert!(lines[1].ends_with(" next -"), "{}", lines[1]);
assert!(!unreported.out.contains("cut-B.r1"), "{}", unreported.out);
let unjudged = at(server.addr, &["unjudged", CAMPAIGN], None);
assert!(unjudged.out.contains(&format!("{CAMPAIGN}:cut_report:cut-B.h1")), "{}", unjudged.out);
assert!(unjudged.out.contains("matched 1 "), "{}", unjudged.out);
let campaigns = at(server.addr, &["campaigns"], None);
assert!(campaigns.out.starts_with(&format!("{CAMPAIGN}:campaign:self InForce Hands Eureka pipeline state repos={REPO}\n")), "{}", campaigns.out);
let whole = at(server.addr, &["ledger", CAMPAIGN], None);
assert_eq!(whole.code, 0, "{}", whole.err);
assert_eq!(whole.out.matches("admitted ").count(), 3, "{}", whole.out);
assert!(whole.out.contains("\n\n"), "documents are separated by a blank line: {}", whole.out);
let documents = at(server.addr, &["campaigns", "--documents"], None);
assert!(documents.out.contains("\nadmitted "), "{}", documents.out);
assert!(documents.out.contains("\"kind\": \"campaign\""), "{}", documents.out);
}
#[test]
fn a_long_header_is_cut() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let mut doc = campaign_json();
doc["value"]["title"] = json!("t".repeat(200));
doc["value"]["working_branch"] = json!("b");
let file = write(&dir, "long.json", &json!([doc]));
assert_eq!(at(server.addr, &admit_args(&file), None).code, 0);
let ran = at(server.addr, &["campaigns"], None);
let header = ran.out.lines().next().unwrap();
assert_eq!(header.chars().count(), 240);
}
#[test]
fn a_resolved_document_shows_its_closing_record() {
let server = live();
let dir = tempfile::tempdir().unwrap();
let question = json!({
"kind": "question",
"value": {
"campaign": CAMPAIGN,
"label": "Q1",
"title": "Who owns the state?",
"question": "Who owns the state?",
"options": [{ "label": "A", "text": "an instance" }, { "label": "B", "text": "a repo" }],
"recommended": "A",
"depends": [],
"raised_in": null,
"asked_on": "2026-10-10",
},
});
let setup = write(&dir, "setup.json", &json!([campaign_json(), question]));
let ran = at(server.addr, &admit_args(&setup), None);
assert_eq!(ran.code, 0, "{}{}", ran.out, ran.err);
let resolution = json!({
"kind": "resolution",
"value": {
"subject": { "kind": "question", "id": format!("{CAMPAIGN}:question:Q1") },
"sequence": 1,
"outcome": { "Withdrawn": { "reason": "moot" } },
"rationale": "Resolved.",
"resolved_on": "2026-10-10",
},
});
let closing = write(&dir, "closing.json", &json!([resolution]));
let ran = at(server.addr, &admit_args(&closing), None);
assert_eq!(ran.code, 0, "{}{}", ran.out, ran.err);
let id = format!("{CAMPAIGN}:question:Q1");
let closer = format!("{CAMPAIGN}:resolution:question.Q1.n1");
let viewed = at(server.addr, &["view", &id], None);
assert_eq!(viewed.out.lines().next().unwrap(), format!("{id} Resolved Withdrawn by {closer}"));
let history = at(server.addr, &["history", &id], None);
assert_eq!(history.code, 0, "{}", history.err);
assert!(history.out.starts_with(&format!("{closer} InForce Hands ")), "{}", history.out);
assert!(history.out.contains("sequence=1"), "{}", history.out);
let open = at(server.addr, &["open", CAMPAIGN], None);
assert!(open.out.contains("matched 0 "), "a resolved question is not open: {}", open.out);
let header = at(server.addr, &["query", r#"{"schemas": ["epiphany.pipeline.question.v2"]}"#], None);
assert!(header.out.starts_with(&format!("{id} Resolved:Withdrawn Hands Who owns the state? ")), "{}", header.out);
assert!(header.out.contains("options=A,B") && header.out.contains("recommended=A"), "{}", header.out);
}
#[test]
fn schema_prints_the_published_files() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
for kind in ["cut_spec", "campaign", "hand_off", "run"] {
let ran = run(&[], &["schema", kind], None);
assert_eq!(ran.code, 0);
let published = std::fs::read_to_string(manifest.join(format!("../../schemas/cultnet/epiphany.pipeline.{kind}.v2.schema.json"))).unwrap();
assert_eq!(ran.out, published, "{kind}");
}
let selection = run(&[], &["schema", "selection"], None);
assert_eq!(selection.out, std::fs::read_to_string(manifest.join("schemas/cultnet.selection.schema.json")).unwrap());
let names = run(&[], &["schema"], None);
assert_eq!(names.code, 0);
let names: Vec<&str> = names.out.lines().collect();
assert_eq!(names.len(), 15, "{names:?}");
assert_eq!(names[0], "campaign");
assert_eq!(names[14], "selection");
for kind in PipelineKind::ALL {
assert!(names.contains(&kind.name()), "{}", kind.name());
}
let unknown = run(&[], &["schema", "no-such-kind"], None);
assert_eq!(unknown.code, 2);
assert!(unknown.err.starts_with("InvalidInput: "), "{}", unknown.err);
}
#[test]
fn help_is_the_schema_surface() {
let rulings = run(&[], &["rulings", "--help"], None);
assert_eq!(rulings.code, 0);
let (_, printed) = rulings.out.split_once("selection:\n").expect("the selection is in the help");
let selection: Value = serde_json::from_str(printed.trim()).unwrap();
assert_eq!(
selection,
json!({
"schemas": ["epiphany.pipeline.ruling.v2"],
"fields": [
{ "index": "root", "op": "any_of", "values": ["<c>"] },
{ "index": "in_force", "op": "any_of", "values": ["true"] },
],
})
);
let history = run(&[], &["history", "--help"], None);
assert!(history.out.contains("epiphany.pipeline.<kind>.v2"), "{}", history.out);
let findings = run(&[], &["findings", "--help"], None);
assert!(findings.out.contains("\"severity\"") && findings.out.contains("Blocker"), "{}", findings.out);
let precedent = run(&[], &["precedent", "--help"], None);
assert!(precedent.out.contains("\"top_k\": 10"), "{}", precedent.out);
let admit = run(&[], &["admit", "--help"], None);
let admit_text = admit.out.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(admit_text.contains("{\"kind\": <kind>, \"value\": {...}}"), "{admit_text}");
assert!(admit_text.contains("exit 5"), "{admit_text}");
let version = run(&[], &["--version"], None);
assert_eq!(version.code, 0);
assert!(version.out.starts_with(&format!("huginn {} (", env!("CARGO_PKG_VERSION"))), "{}", version.out);
}
#[test]
fn every_recipe_has_help() {
for recipe in [
"campaigns", "target", "rulings", "open", "findings", "unreported", "blocked", "unjudged", "history", "cut", "ledger",
"precedent", "gaps",
] {
let ran = run(&[], &[recipe, "--help"], None);
assert_eq!(ran.code, 0, "{recipe}");
assert!(ran.out.contains("epiphany.pipeline."), "{recipe}: {}", ran.out);
assert!(ran.out.contains("--cursor"), "{recipe}: {}", ran.out);
}
}
#[test]
fn a_conflicting_batch_is_the_minds_no() {
let conflict = scripted(|message| {
let (_, request) = decode_request(message).expect("a mind request");
assert!(matches!(request, HuginnMindRequest::Admit(_)));
answer(
"admit",
&HuginnMindResponse::Admit(PipelineAdmissionOutcome::Conflict {
identities: vec![PipelineRef { kind: PipelineKind::Campaign, id: Short(format!("{CAMPAIGN}:campaign:self")) }],
}),
)
});
let dir = tempfile::tempdir().unwrap();
let file = write(&dir, "campaign.json", &json!([campaign_json()]));
let ran = at(conflict.addr, &admit_args(&file), None);
assert_eq!(ran.code, 1);
assert_eq!(ran.out, format!("Conflict\nconflicts {CAMPAIGN}:campaign:self\n"));
}