diff --git a/crates/mind-body/src/control/tests.rs b/crates/mind-body/src/control/tests.rs index d87b3dc..19dcdd2 100644 --- a/crates/mind-body/src/control/tests.rs +++ b/crates/mind-body/src/control/tests.rs @@ -235,9 +235,13 @@ fn an_old_swarm_store_holding_a_dial_or_a_share_reads_as_held_undecodable_and_is cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(swarm_path(dir.path()))).unwrap(); cache.pull_all_backing_stores().unwrap(); match old { - "dial" => cache.put("dial", &OldDial { heat: "1".into() }).unwrap(), - _ => cache.put("eureka", &OldShare { weight: "1".into() }).unwrap(), - }; + "dial" => { + cache.put("dial", &OldDial { heat: "1".into() }).unwrap(); + } + _ => { + cache.put("eureka", &OldShare { weight: "1".into() }).unwrap(); + } + } assert_eq!(effective(&path), held(HeldReason::Undecodable), "{old}: the brake in the same file does not rescue it"); assert_eq!(read_swarm_for_test(dir.path()), SwarmEffective::Held(HeldReason::Undecodable)); } @@ -261,3 +265,9 @@ fn the_stores_are_written_world_readable_and_root_only_writable() { #[cfg(not(unix))] let _ = path; } + +/// A named instance store with its brake released, and a released swarm. +fn release(root: &Path, name: &str) { + ControlWriter::open(root.join(name).join("control.cc")).unwrap().set_brake(true, at(0), "op").unwrap(); + release_swarm(root); +} diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index b6986da..8ce6193 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -782,7 +782,7 @@ fn with_no_doctrine_configured_the_instructions_begin_with_the_persona() { fn doctrine_comes_from_the_environment_as_absolute_paths_in_order() { let env = |doctrine: &'static str| { move |name: &str| match name { - ENV_ROOMS | ENV_BURN_ROOM => Some("swarm".to_string()), + ENV_ROOMS => Some("swarm".to_string()), ENV_OPERATOR => Some("@op:chat".to_string()), ENV_SELF_SENDER => Some("@eureka:chat".to_string()), ENV_DOCTRINE => Some(doctrine.to_string()), diff --git a/crates/mind-body/src/self_run/tests.rs b/crates/mind-body/src/self_run/tests.rs index a84a475..44a2987 100644 --- a/crates/mind-body/src/self_run/tests.rs +++ b/crates/mind-body/src/self_run/tests.rs @@ -13,6 +13,7 @@ use eureka_pipeline::{PipelineDocument, PipelineKind as K, PipelineRef, RunOpera use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage}; use super::*; +use cultnet_rs::Selection; use crate::lease_client::{Lease, LeaseRefused, token_holding}; use model_connector::PlanQuota; use crate::testkit::*; diff --git a/crates/mind-body/src/self_run/tests/lease.rs b/crates/mind-body/src/self_run/tests/lease.rs index 7675821..37d931a 100644 --- a/crates/mind-body/src/self_run/tests/lease.rs +++ b/crates/mind-body/src/self_run/tests/lease.rs @@ -23,14 +23,6 @@ fn never_spawns() -> Fake { Fake { child: RefCell::new(None), spawns: RefCell::new(Vec::new()), refuse: false } } -#[test] -fn a_lease_is_asked_for_before_claude_starts_for_the_runs_budget_and_the_units_runtime_cap() { - let ran = go(vec![Out(result_line())]); - assert_eq!(ran.asked, vec![LeaseRequest { expires_at_unix_ms: u64::try_from(at_start().timestamp_millis()).unwrap() + TEST_CAP_S * 1000 }], "the connector sets the reservation: the request is only the expiry, the cap the unit gave"); - assert_eq!(ran.all.first(), Some(&Event::Lent), "nothing happens before the lease"); - assert_eq!(ran.all.get(1), Some(&Event::Spawned), "the child is the next thing"); -} - #[test] fn no_lease_is_asked_for_when_there_is_no_run_to_carry() { let ran = carry_with(&Seen::of(&TestMind::seeded()), vec![], Some(0), false); @@ -38,33 +30,6 @@ fn no_lease_is_asked_for_when_there_is_no_run_to_carry() { assert!(ran.asked.is_empty() && ran.all.is_empty()); } -#[test] -fn refused_lease_spawns_nothing() { - for kind in [LeaseRefusalKind::SpendCeiling, LeaseRefusalKind::CostBound, LeaseRefusalKind::UnknownCaller, LeaseRefusalKind::Busy] { - let mind = with_run(); - let lender = FakeLender::new(Answer::Refuse(kind)); - let never = never_spawns(); - let clock = Rc::new(Fixed(Cell::new(at_start()))); - let mut out = Vec::new(); - let outcome = carry(&mind, &*clock, &lender, &never, TEST_CAP_S, &mut out).unwrap(); - assert_eq!(outcome, SelfRunOutcome::LeaseRefused { kind }); - assert_eq!(outcome.exit_status(), 4); - assert!(never.spawns.borrow().is_empty(), "{kind:?}: the spawner is never called"); - assert!(!format!("{outcome} {}", String::from_utf8_lossy(&out)).contains("detail-canary"), "the connector's words are not echoed"); - - // The run is closed Recorded, with the kind, so a run that cannot be paid for counts as an empty one. - let closure = mind.view(K::Resolution, &format!("{INSTANCE}:resolution:run.selfrun-a.n1")).expect("the run is closed"); - let PipelineDocument::Resolution(closure) = closure.document else { panic!("not a resolution") }; - assert_eq!(closure.subject, run_ref("selfrun-a")); - assert!(matches!(&closure.outcome, ResolutionOutcome::Recorded { reason } if reason.0.contains(&format!("{kind:?}"))), "{:?}", closure.outcome); - - // Closed, so it is no longer her open run: nothing is carried or leased for it again. - let again = FakeLender::new(Answer::Grant); - assert_eq!(carry(&mind, &*clock, &again, &never, TEST_CAP_S, &mut Vec::new()).unwrap(), SelfRunOutcome::NoRun); - assert!(again.asked.borrow().is_empty()); - } -} - #[test] fn a_lease_refused_for_its_length_is_a_configuration_error_that_starts_nothing_and_withdraws_the_run() { let mind = with_run(); @@ -99,82 +64,6 @@ fn a_connector_that_cannot_be_reached_is_an_error_that_starts_nothing_and_closes assert!(matches!(&closure.outcome, ResolutionOutcome::Recorded { reason } if reason.0.contains("the actuator failed")), "{:?}", closure.outcome); } -#[test] -fn settle_sends_carry_usage() { - let haiku = |line: String| { - let mut event: Value = serde_json::from_str(&line).unwrap(); - event["message"]["model"] = json!("claude-haiku-5-1"); - event.to_string() - }; - let ran = go(vec![ - Out(billed("m1", 1, 100, 10, 5)), - // Records that share a message id are one request, at the usage it had grown to. - Out(billed("m1", 1, 100, 10, 9)), - // A subagent's request is billed too, under its own model. - Out(haiku(subagent_billed("m2", 2, 0, 0, 4))), - Out(billed("m3", 3, 0, 0, 1)), - Out("not json".to_string()), - Out(result_line()), - ]); - let used = |model: &str, input, read, write, output| ModelUsage { model: model.into(), usage: TokenUsage { input_tokens: input, cache_read_tokens: read, cache_write_tokens: write, output_tokens: output, reasoning_tokens: None } }; - let expected = Settlement { usage: vec![used("claude-haiku-5-1", 2, 0, 0, 4), used("claude-opus-5-5", 4, 100, 10, 10)] }; - let settled: Vec<&Event> = ran.all.iter().filter(|event| matches!(event, Event::Settled(_))).collect(); - assert_eq!(settled, vec![&Event::Settled(expected)], "settled exactly once, with the stream's sums per model"); - assert!(matches!(ran.all.last(), Some(Event::Settled(_))), "settled after the child ended, not before"); - assert_eq!(ran.count_all(&Event::Abandoned), 0); -} - -#[test] -fn a_run_that_reported_nothing_settles_an_empty_report_which_the_connector_bills_as_the_reservation() { - // Not free: an empty report is the connector's cue to bill the reservation, exactly as for a dropped lease. - let ran = go(vec![Out(result_line())]); - assert!(ran.all.contains(&Event::Settled(Settlement::default()))); -} - -#[test] -fn a_child_that_cannot_be_stopped_is_left_to_the_connector_to_bill_in_full() { - // Not even SIGKILL is delivered: the child may still be spending, so the lease is dropped unsettled. - 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().0, Cause::Unstoppable); - assert_eq!(stuck.count_all(&Event::Abandoned), 1); - assert!(!stuck.all.iter().any(|event| matches!(event, Event::Settled(_)))); -} - -#[test] -fn a_run_cut_off_before_its_child_ended_is_billed_the_reservation_not_a_guess() { - // The output stream fails mid-run: `carry` unwinds with the child still alive. The lease is dropped - // unsettled, as when the brake stops the unit, and the connector bills the reservation. - struct Broken; - impl Write for Broken { - fn write(&mut self, _: &[u8]) -> std::io::Result { - Err(std::io::Error::other("closed")) - } - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } - } - let mind = with_run(); - let log = Rc::new(RefCell::new(Vec::new())); - let clock = Rc::new(Fixed(Cell::new(at_start()))); - let child = Scripted { steps: vec![Out(at("m1", 10))].into(), log: log.clone(), clock: clock.clone(), code: Some(0), dies_on: None, refuses: vec![], hangs: true, exited: false, eof: false, asked: 0 }; - let fake = Fake { child: RefCell::new(Some(child)), spawns: RefCell::new(Vec::new()), refuse: false }; - let lender = FakeLender::logging(log.clone(), Answer::Grant); - assert!(carry(&Seen::of(&mind), &*clock, &lender, &fake, TEST_CAP_S, &mut Broken).is_err()); - assert_eq!(log.borrow().last(), Some(&Event::Abandoned)); - assert!(!log.borrow().iter().any(|event| matches!(event, Event::Settled(_)))); -} - -#[test] -fn a_child_that_cannot_start_settles_an_empty_report_which_the_connector_bills_as_the_reservation() { - let mind = with_run(); - let log = Rc::new(RefCell::new(Vec::new())); - let lender = FakeLender::logging(log.clone(), Answer::Grant); - let refusing = Fake { child: RefCell::new(None), spawns: RefCell::new(Vec::new()), refuse: true }; - let clock = Rc::new(Fixed(Cell::new(at_start()))); - assert!(carry(&Seen::of(&mind), &*clock, &lender, &refusing, TEST_CAP_S, &mut Vec::new()).is_err()); - assert_eq!(*log.borrow(), vec![Event::Lent, Event::Settled(Settlement::default())]); -} - fn result_stating(models: Value) -> String { let mut event: Value = serde_json::from_str(&result_line()).unwrap(); event["modelUsage"] = models; @@ -185,80 +74,6 @@ fn stated(input: u64, read: u64, write: u64, output: u64) -> Value { json!({"inputTokens": input, "cacheReadInputTokens": read, "cacheCreationInputTokens": write, "outputTokens": output, "costUSD": 0.5, "contextWindow": 200000}) } -#[test] -fn claudes_own_per_model_total_in_the_result_is_what_settles_and_the_last_result_wins() { - let used = |model: &str, input, read, write, output| ModelUsage { model: model.into(), usage: TokenUsage { input_tokens: input, cache_read_tokens: read, cache_write_tokens: write, output_tokens: output, reasoning_tokens: None } }; - let ran = go(vec![ - Out(billed("m1", 1, 100, 10, 5)), - // An earlier result is superseded by the cumulative one after it (the flush turn's). - Out(result_stating(json!({"claude-opus-5-5": stated(7, 8, 9, 10)}))), - Out(billed("m2", 3, 0, 0, 1)), - Out(result_stating(json!({"claude-opus-5-5": stated(11, 12, 13, 14), "claude-haiku-5-1": stated(1, 2, 3, 4)}))), - ]); - let expected = Settlement { usage: vec![used("claude-haiku-5-1", 1, 2, 3, 4), used("claude-opus-5-5", 11, 12, 13, 14)] }; - let settled: Vec<&Event> = ran.all.iter().filter(|event| matches!(event, Event::Settled(_))).collect(); - assert_eq!(settled, vec![&Event::Settled(expected)], "the stream's own sums are not added to claude's total"); -} - -#[test] -fn a_result_that_states_no_model_usage_leaves_the_streams_sums_to_settle() { - let ran = go(vec![Out(billed("m1", 1, 100, 10, 5)), Out(result_stating(json!({}))), Out(result_stating(json!("not a map")))]); - let settled: Vec<&Event> = ran.all.iter().filter(|event| matches!(event, Event::Settled(_))).collect(); - let used = ModelUsage { model: UNNAMED_MODEL.into(), usage: TokenUsage { input_tokens: 1, cache_read_tokens: 100, cache_write_tokens: 10, output_tokens: 5, reasoning_tokens: None } }; - assert!(matches!(settled.as_slice(), [Event::Settled(Settlement { usage })] if usage.len() == 1 && usage[0].usage == used.usage), "{settled:?}"); -} - -#[test] -fn the_journal_says_what_was_billed_and_that_a_failed_settle_leaves_the_reservation_billed() { - let run = |answer: Answer| { - let mind = with_run(); - let log = Rc::new(RefCell::new(Vec::new())); - let clock = Rc::new(Fixed(Cell::new(at_start()))); - let child = Scripted { steps: vec![Out(at("m1", 10)), Out(result_line())].into(), log: log.clone(), clock: clock.clone(), code: Some(0), dies_on: None, refuses: vec![], hangs: false, exited: false, eof: false, asked: 0 }; - let fake = Fake { child: RefCell::new(Some(child)), spawns: RefCell::new(Vec::new()), refuse: false }; - let mut out = Vec::new(); - let outcome = carry(&Seen::of(&mind), &*clock, &FakeLender::logging(log, answer), &fake, TEST_CAP_S, &mut out).unwrap(); - (outcome, String::from_utf8(out).unwrap()) - }; - let (outcome, out) = run(Answer::Grant); - assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n"); - let (failed, out) = run(Answer::GrantSettleFails); - assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettle failed: the connector bills the reservation\n", "the connector's words are not echoed"); - assert_eq!(failed, outcome, "the run's own outcome stands"); -} - fn read_total(settlement: &Settlement) -> u64 { settlement.usage.iter().map(|used| used.usage.cache_read_tokens).sum() } - -#[test] -fn requests_after_the_last_result_are_settled_on_top_of_claudes_total() { - // The flush turn runs after the first result and is stopped before it states a result of its own. - let ran = go(vec![ - Out(billed("m1", 1, 100, 10, 5)), - Out(result_stating(json!({"claude-opus-5-5": stated(41, 812_345, 20_480, 9_123)}))), - Out(billed("m2", 1000, 900_000, 0, 500)), - Out(billed("m2", 1000, 900_000, 0, 700)), - ]); - let total = |input, read, write, output| Settlement { usage: vec![ModelUsage { model: "claude-opus-5-5".into(), usage: TokenUsage { input_tokens: input, cache_read_tokens: read, cache_write_tokens: write, output_tokens: output, reasoning_tokens: None } }] }; - let settled: Vec<&Event> = ran.all.iter().filter(|event| matches!(event, Event::Settled(_))).collect(); - assert_eq!(settled, vec![&Event::Settled(total(1_041, 1_712_345, 20_480, 9_823))], "claude's total plus the tail request once"); -} - -#[test] -fn two_results_and_a_tail_settle_the_last_total_plus_only_what_came_after_it() { - let mut spent = Spent::default(); - spent.hear(&billed("m1", 1, 100, 0, 1)); - spent.hear(&result_stating(json!({"claude-opus-5-5": stated(1, 100, 0, 1)}))); - spent.hear(&billed("m2", 2, 200, 0, 2)); - spent.hear(&result_stating(json!({"claude-opus-5-5": stated(3, 300, 0, 3)}))); - assert_eq!(read_total(&spent.settlement()), 300, "m2 is inside the second result's total"); - spent.hear(&billed("m3", 4, 400, 0, 4)); - let settled = spent.settlement(); - assert_eq!(read_total(&settled), 700); - assert_eq!(settled.usage.len(), 1, "the tail joins its model's total"); - assert_eq!(settled.usage[0].usage.input_tokens, 7); - // A later empty result neither erases the total nor folds the tail in. - spent.hear(&result_stating(json!({}))); - assert_eq!(read_total(&spent.settlement()), 700); -} diff --git a/crates/mind-body/src/self_run/tests/process/lease_child.rs b/crates/mind-body/src/self_run/tests/process/lease_child.rs index a64c422..85695fd 100644 --- a/crates/mind-body/src/self_run/tests/process/lease_child.rs +++ b/crates/mind-body/src/self_run/tests/process/lease_child.rs @@ -26,7 +26,7 @@ fn granted_fd_reaches_child_env_var() { let dir = tempfile::tempdir().unwrap(); let program = script(&dir, READS_ITS_CREDENTIAL); let token = token_holding("tok-canary-7f3a"); - let mut child = start_with(&program, &claude_args(session_id(&run_ref("selfrun-a")), "5"), &token); + let mut child = start_with(&program, &claude_args(session_id(&run_ref("selfrun-a"))), &token); child.close_input(); let (lines, code) = drain(&mut *child); assert_eq!(code, Some(0), "{lines:?}"); diff --git a/crates/mind-body/tests/cli.rs b/crates/mind-body/tests/cli.rs index 8fd8d7d..8ae92c2 100644 --- a/crates/mind-body/tests/cli.rs +++ b/crates/mind-body/tests/cli.rs @@ -43,9 +43,8 @@ fn show_reports_every_rung_from_an_absent_store_to_released() { assert!(out.contains("brake: released") && out.contains("set_by alice") && out.contains("BrakeAbsent"), "the swarm is still unreleased: {out}"); go(root.path(), &["swarm", "brake", "release"], "bob", 3).0.unwrap(); - go(root.path(), &["swarm", "dial", "set", "--heat", "0.5", "--base-rate", "4", "--base-run", "5", "--k", "2"], "bob", 3).0.unwrap(); let (_, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 4); - assert!(out.contains("Released") && out.contains("run_cap_usd: 2.5"), "{out}"); + assert!(out.contains("Released"), "{out}"); go(root.path(), &["--instance", "eureka", "brake", "hold"], "alice", 5).0.unwrap(); let (_, out) = go(root.path(), &["--instance", "eureka", "show"], "op", 6); @@ -125,7 +124,6 @@ fn the_binary_exits_2_for_check_while_held_and_0_after_release_with_a_swarm() { std::process::Command::new(env!("CARGO_BIN_EXE_mind-control")).arg("--root").arg(root.path()).arg("swarm").args(args).output().unwrap() }; assert!(swarm(&["brake", "release"]).status.success()); - assert!(swarm(&["dial", "set", "--heat", "0.5", "--base-rate", "4", "--base-run", "5", "--k", "2"]).status.success()); let open = run(&["check"]); assert_eq!(open.status.code(), Some(0)); assert!(String::from_utf8_lossy(&open.stdout).contains("Released")); @@ -204,7 +202,6 @@ fn check_opens_on_release_with_a_swarm_and_is_per_instance() { let root = tempfile::tempdir().unwrap(); go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 0).0.unwrap(); go(root.path(), &["swarm", "brake", "release"], "op", 1).0.unwrap(); - go(root.path(), &["swarm", "dial", "set", "--heat", "0.5", "--base-rate", "4", "--base-run", "5", "--k", "2"], "op", 1).0.unwrap(); let (result, out) = check_out(root.path()); result.unwrap(); assert!(out.contains("Released"), "{out}"); @@ -218,7 +215,6 @@ fn check_holds_for_the_named_instance_with_a_released_prefix_neighbour() { let root = tempfile::tempdir().unwrap(); go(root.path(), &["--instance", "eureka", "brake", "hold"], "op", 0).0.unwrap(); go(root.path(), &["swarm", "brake", "release"], "op", 0).0.unwrap(); - go(root.path(), &["swarm", "dial", "set", "--heat", "0.5", "--base-rate", "4", "--base-run", "5", "--k", "2"], "op", 0).0.unwrap(); // Neighbours that share the held instance's text as a prefix, or only an edge of it, are released with a dial. for (i, neighbour) in ["eureka-two", "eureka2", "xeureka"].into_iter().enumerate() { let at = 1 + 2 * i as i64; @@ -280,10 +276,8 @@ fn brake_hold_stops_once_with_the_instance_and_the_store_already_held() { #[test] fn only_brake_hold_calls_the_stopper() { let root = tempfile::tempdir().unwrap(); - let dial = ["swarm", "dial", "set", "--heat", "1", "--base-rate", "4", "--base-run", "1", "--k", "2"]; let commands: &[&[&str]] = &[ &["--instance", "eureka", "brake", "release"], - &dial, &["--instance", "eureka", "show"], &["--instance", "eureka", "brake"], &["--instance", "eureka", "brake", "toggle"], @@ -441,24 +435,22 @@ fn swarm_show(root: &std::path::Path) -> String { fn swarm_verbs_round_trip() { let root = tempfile::tempdir().unwrap(); let shown = swarm_show(root.path()); - assert!(shown.contains("brake: absent") && shown.contains("dial: absent") && shown.contains("BrakeAbsent"), "{shown}"); + assert!(shown.contains("brake: absent") && shown.contains("BrakeAbsent"), "{shown}"); for args in [ &["swarm", "brake", "release"][..], - &["swarm", "dial", "set", "--heat", "1.5", "--base-rate", "4", "--base-run", "2", "--k", "3"], - &["swarm", "share", "set", "weksa", "2.5"], ] { go(root.path(), args, "alice", 5).0.unwrap(); } let shown = swarm_show(root.path()); assert!(shown.contains("brake: released") && shown.contains("set_by alice"), "{shown}"); - assert!(shown.contains("dial: heat 1.5 base_rate_usd_per_h 4 base_run_usd 2 k 3"), "{shown}"); - assert!(shown.contains("share: weksa 2.5"), "{shown}"); - assert!(shown.contains("Released") && shown.contains("rate_usd_per_h: 6.0") && shown.contains("run_cap_usd: 3.0"), "{shown}"); + assert!(shown.contains("Released"), "{shown}"); + assert!(!shown.contains("dial") && !shown.contains("share"), "the swarm store holds a brake and nothing else: {shown}"); - // Heat 0 stops the swarm and is one command away; the hold stops it too. - go(root.path(), &["swarm", "dial", "set", "--heat", "0", "--base-rate", "4", "--base-run", "2", "--k", "3"], "alice", 6).0.unwrap(); - assert!(swarm_show(root.path()).contains("HeatZero")); + // The dollar-metering verbs are gone: they are refused, not read as something else. + for args in [&["swarm", "dial", "set", "--heat", "0", "--base-rate", "4", "--base-run", "2", "--k", "3"][..], &["swarm", "share", "set", "weksa", "2.5"]] { + assert!(go(root.path(), args, "alice", 6).0.is_err(), "{args:?}"); + } go(root.path(), &["swarm", "brake", "hold"], "alice", 7).0.unwrap(); assert!(swarm_show(root.path()).contains("BrakeHeld")); }