mind-body: the reader takes the instance only; the control root is a constant a caller cannot supply
read_effective(instance) reads CONTROL_ROOT/<instance>/control.cc. The root override is a thread-local under cfg(test) inside the crate; the reader tests and the Grant launch tests move in-crate to use it. The operator CLI's show derives its display line from the store it already read. An external forge probe and compile_fail examples pin the rule; the instance gate is pinned with same-length, prefix and extension forgeries, and the Persona refusal with a claim of every kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7d85db3af3
commit
3fac90076d
7 changed files with 576 additions and 407 deletions
|
|
@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
|
|||
use eureka_pipeline::Slug;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::control::{BurnRate, ControlWriter, FileSource, control_path, load_state, read_effective};
|
||||
use crate::control::{BurnRate, ControlWriter, FileSource, control_path, derive, load_state};
|
||||
|
||||
const USAGE: &str =
|
||||
"usage: mind-control [--root <dir>] --instance <slug> show | brake hold | brake release | dial set --heat H --base-cooldown-s S --base-run-usd U";
|
||||
|
|
@ -120,6 +120,6 @@ fn show(root: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
|
|||
}
|
||||
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
|
||||
}
|
||||
writeln!(out, "effective: {:?}", read_effective(root, instance))?;
|
||||
writeln!(out, "effective: {:?}", derive(&source, instance))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
//! `Released` carries a `Grant`, the only value that hands a run cap to the
|
||||
//! launch: it cannot be built outside this module, so a cap exists only as the
|
||||
//! reading of a released brake and an in-bounds dial. The Grant names the
|
||||
//! instance whose store was read, and that store's path is derived from the
|
||||
//! instance here, by the one reader: no caller supplies a path or a source, so
|
||||
//! a Grant is the proof that this instance's own root-owned store released.
|
||||
//! instance whose store was read. The store is at a fixed place, `CONTROL_ROOT`,
|
||||
//! and the one reader takes the instance only: no function, argument, variable
|
||||
//! or flag lets a caller choose a root, a path or a source, so a Grant is the
|
||||
//! proof that this instance's own root-owned store released. Only a
|
||||
//! `#[cfg(test)]` override inside this crate moves the root.
|
||||
//!
|
||||
//! State is a CultCache single-file store at
|
||||
//! `/etc/gamecult/minds/<instance>/control.cc`, root-owned and world-readable.
|
||||
|
|
@ -35,8 +37,8 @@ use eureka_pipeline::Slug;
|
|||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
/// Where every instance's control directory lives. Tests and the library never
|
||||
/// touch it: the root is a parameter.
|
||||
/// Where every instance's control directory lives. The reader reads only here;
|
||||
/// the operator's CLI takes its own `--root` for writing and display.
|
||||
pub const CONTROL_ROOT: &str = "/etc/gamecult/minds";
|
||||
|
||||
/// The one key each control document has in the store.
|
||||
|
|
@ -170,7 +172,7 @@ pub struct ControlState {
|
|||
impl ControlState {
|
||||
/// The one derivation. Order: brake first, then dial; each failing rung is
|
||||
/// its own reason.
|
||||
fn effective(&self, instance: &Slug) -> Effective {
|
||||
pub(crate) fn effective(&self, instance: &Slug) -> Effective {
|
||||
let held = |reason| Effective::Held { reason };
|
||||
let Some(brake) = &self.brake else {
|
||||
return held(HeldReason::BrakeAbsent);
|
||||
|
|
@ -233,45 +235,85 @@ pub fn load_state(source: &impl ControlSource) -> Result<ControlState> {
|
|||
}
|
||||
|
||||
/// The only reader every organ uses (the waker now; the permit issuer and the
|
||||
/// Persona organ later). The store is `instance`'s own, at the path
|
||||
/// `control_path` derives under `root`; the caller supplies neither a path nor
|
||||
/// a source. An instance that is not a slug reads as held.
|
||||
pub fn read_effective(root: &Path, instance: &Slug) -> Effective {
|
||||
match control_path(root, instance) {
|
||||
/// Persona organ later). The store is `instance`'s own at
|
||||
/// `CONTROL_ROOT/<instance>/control.cc`; the caller supplies neither a root, a
|
||||
/// path nor a source. An instance that is not a slug reads as held.
|
||||
///
|
||||
/// Nothing outside this crate can name another place:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use eureka_pipeline::Slug;
|
||||
/// // a root is not an argument
|
||||
/// let _ = mind_body::control::read_effective(std::path::Path::new("/tmp"), &Slug("eureka".into()));
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// // there is no reader over a source of the caller's
|
||||
/// let _ = mind_body::control::read_effective_from;
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use mind_body::control::Grant;
|
||||
/// // a Grant cannot be written out
|
||||
/// let _ = Grant { instance: eureka_pipeline::Slug("eureka".into()), cadence: std::time::Duration::ZERO, run_cap_usd: rust_decimal::Decimal::ONE };
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// // and the test constructor is not in the library build
|
||||
/// let _ = mind_body::control::Grant::for_test;
|
||||
/// ```
|
||||
///
|
||||
/// The same preamble with the real call compiles:
|
||||
///
|
||||
/// ```
|
||||
/// use eureka_pipeline::Slug;
|
||||
/// let _ = mind_body::control::read_effective(&Slug("eureka".into()));
|
||||
/// ```
|
||||
pub fn read_effective(instance: &Slug) -> Effective {
|
||||
match control_path(&control_root(), instance) {
|
||||
Ok(path) => derive(&FileSource::new(path), instance),
|
||||
Err(_) => Effective::Held { reason: HeldReason::Undecodable },
|
||||
}
|
||||
}
|
||||
|
||||
fn derive(source: &impl ControlSource, instance: &Slug) -> Effective {
|
||||
#[cfg(not(test))]
|
||||
fn control_root() -> PathBuf {
|
||||
PathBuf::from(CONTROL_ROOT)
|
||||
}
|
||||
|
||||
/// The test seam: this crate's own tests move the root for their thread. It
|
||||
/// does not exist in a library build, and it is not a cargo feature.
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static TEST_ROOT: std::cell::RefCell<Option<PathBuf>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn control_root() -> PathBuf {
|
||||
TEST_ROOT.with(|root| root.borrow().clone()).unwrap_or_else(|| PathBuf::from(CONTROL_ROOT))
|
||||
}
|
||||
|
||||
/// Runs `body` with the reader pointed at `root` on this thread.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_root<T>(root: &Path, body: impl FnOnce() -> T) -> T {
|
||||
let before = TEST_ROOT.with(|cell| cell.replace(Some(root.to_path_buf())));
|
||||
let result = body();
|
||||
TEST_ROOT.with(|cell| cell.replace(before));
|
||||
result
|
||||
}
|
||||
|
||||
/// The derivation over any source. The reader and the CLI's `show` (display
|
||||
/// only: its output is text) are its callers; nothing public returns its result
|
||||
/// for a source of the caller's.
|
||||
pub(crate) fn derive(source: &impl ControlSource, instance: &Slug) -> Effective {
|
||||
match load_state(source) {
|
||||
Ok(state) => state.effective(instance),
|
||||
Err(_) => Effective::Held { reason: HeldReason::Undecodable },
|
||||
}
|
||||
}
|
||||
|
||||
/// The same derivation over a fake store, for tests of the reader; the Grant it
|
||||
/// makes names the instance given, and the launch judges that like any other.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_effective_from(source: &impl ControlSource, instance: &Slug) -> Effective {
|
||||
derive(source, instance)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_source_that_fails_reads_as_held() {
|
||||
struct Failing;
|
||||
impl ControlSource for Failing {
|
||||
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>> {
|
||||
Err(anyhow::anyhow!("disk gone"))
|
||||
}
|
||||
}
|
||||
assert_eq!(read_effective_from(&Failing, &Slug("eureka".into())), Effective::Held { reason: HeldReason::Undecodable });
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
/// The operator's writer over one control file. Each setter replaces its
|
||||
/// document whole; the other document is untouched. The file is created on the
|
||||
|
|
|
|||
414
crates/mind-body/src/control/tests.rs
Normal file
414
crates/mind-body/src/control/tests.rs
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
//! The reader's tests, in the crate because they move the reader's root with
|
||||
//! the `#[cfg(test)]` seam: no caller outside the crate can. Every test works in
|
||||
//! a temporary directory through the same file door the units use; nothing
|
||||
//! touches `/etc`. They run as root in the verify container like the writer's.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
|
||||
use eureka_pipeline::Slug;
|
||||
use rust_decimal::Decimal;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn dec(text: &str) -> Decimal {
|
||||
Decimal::from_str(text).unwrap()
|
||||
}
|
||||
|
||||
fn at(seconds: i64) -> DateTime<Utc> {
|
||||
Utc.timestamp_opt(1_800_000_000 + seconds, 0).unwrap()
|
||||
}
|
||||
|
||||
fn dial(heat: &str, cooldown: u32, run_usd: &str) -> BurnRate {
|
||||
BurnRate { heat: dec(heat), base_cooldown_s: cooldown, base_run_usd: dec(run_usd), set_at: at(0), set_by: "op".into() }
|
||||
}
|
||||
|
||||
/// The store of the instance `eureka` under the root `dir`.
|
||||
fn store_path(dir: &Path) -> PathBuf {
|
||||
dir.join("eureka").join("control.cc")
|
||||
}
|
||||
|
||||
fn held(reason: HeldReason) -> Effective {
|
||||
Effective::Held { reason }
|
||||
}
|
||||
|
||||
/// A written store: brake released and the given dial, through the real writer.
|
||||
fn released_store(dir: &Path, dial: BurnRate) -> PathBuf {
|
||||
let path = store_path(dir);
|
||||
let mut writer = ControlWriter::open(&path).unwrap();
|
||||
writer.set_brake(true, at(0), "op").unwrap();
|
||||
writer.set_dial(dial).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
/// The real reader's answer for the store at `<root>/<instance>/control.cc`,
|
||||
/// with the reader's root moved to `<root>` on this thread.
|
||||
fn effective(path: &Path) -> Effective {
|
||||
let directory = path.parent().unwrap();
|
||||
let instance = Slug(directory.file_name().unwrap().to_str().unwrap().into());
|
||||
with_root(directory.parent().unwrap(), || read_effective(&instance))
|
||||
}
|
||||
|
||||
/// A dial written around the writer's bounds check, so the reader's own check
|
||||
/// is what is under test.
|
||||
fn write_unchecked_dial(path: &Path, dial: &BurnRate) {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<BurnRate>().unwrap();
|
||||
cache.register_entry_type::<Brake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache.put(DIAL_KEY, dial).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_source_that_fails_reads_as_held() {
|
||||
struct Failing;
|
||||
impl ControlSource for Failing {
|
||||
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>> {
|
||||
Err(anyhow::anyhow!("disk gone"))
|
||||
}
|
||||
}
|
||||
assert_eq!(derive(&Failing, &Slug("eureka".into())), Effective::Held { reason: HeldReason::Undecodable });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_store_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(effective(&store_path(dir.path())), held(HeldReason::BrakeAbsent));
|
||||
assert!(!store_path(dir.path()).exists(), "reading must not create the store");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_brake_reads_as_held_even_with_a_dial() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_dial(dial("1", 3600, "1")).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeAbsent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undecodable_store_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let good = std::fs::read(&path).unwrap();
|
||||
|
||||
// Garbage in place of the store.
|
||||
std::fs::write(&path, b"this is not a cultcache store").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
|
||||
// A store cut off mid-document.
|
||||
std::fs::write(&path, &good[..good.len() / 2]).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
|
||||
// The untouched store is still released, so the two reads above were the damage.
|
||||
std::fs::write(&path, &good).unwrap();
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_that_cannot_be_read_as_a_file_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// A directory sits where the store should be: it exists and cannot be read as a file.
|
||||
let path = store_path(dir.path());
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_document_type_makes_the_store_undecodable() {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.something_else.v1", schema = "Other")]
|
||||
struct Other {
|
||||
#[cultcache(key = 0)]
|
||||
value: u32,
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Other>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
|
||||
cache.put("x", &Other { value: 1 }).unwrap();
|
||||
// The store now holds a released brake, a good dial and a foreign document.
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_brake_payload_that_does_not_decode_makes_the_store_undecodable() {
|
||||
// Same type id as the brake, a different shape: the payload cannot decode as a Brake.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.brake.v1", schema = "ControlBrake")]
|
||||
struct WrongBrake {
|
||||
#[cultcache(key = 0)]
|
||||
released: String,
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<WrongBrake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
|
||||
cache.put(BRAKE_KEY, &WrongBrake { released: "yes".into() }).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_decode_failure_outranks_a_held_brake() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
ControlWriter::open(&path).unwrap().set_brake(false, at(5), "op").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
|
||||
std::fs::write(&path, b"junk").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_brake_with_no_dial_reads_held_dial_absent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::DialAbsent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dial_bounds_are_inclusive_at_both_ends() {
|
||||
for (heat, expected_in) in [("0.05", true), ("2.0", true), ("0.0499", false), ("2.0001", false), ("0", false), ("-1", false), ("3", false)] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store_unchecked(dir.path(), dial(heat, 3600, "1"));
|
||||
let got = effective(&path);
|
||||
if expected_in {
|
||||
assert!(matches!(got, Effective::Released(_)), "heat {heat}: {got:?}");
|
||||
} else {
|
||||
assert_eq!(got, held(HeldReason::DialOutOfBounds), "heat {heat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_base_cooldown_or_a_non_positive_run_cost_is_out_of_bounds() {
|
||||
for bad in [dial("1", 0, "1"), dial("1", 3600, "0"), dial("1", 3600, "-0.5")] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store_unchecked(dir.path(), bad.clone());
|
||||
assert_eq!(effective(&path), held(HeldReason::DialOutOfBounds), "{bad:?}");
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let smallest = released_store_unchecked(dir.path(), dial("1", 1, "0.000001"));
|
||||
assert!(matches!(effective(&smallest), Effective::Released(_)));
|
||||
}
|
||||
|
||||
fn released_store_unchecked(dir: &Path, bad: BurnRate) -> PathBuf {
|
||||
let path = store_path(dir);
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
|
||||
write_unchecked_dial(&path, &bad);
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_and_run_cap_are_the_dial_divided_and_multiplied_by_heat() {
|
||||
for (heat, cooldown, run_usd, cadence, cap) in [
|
||||
("0.5", 3600u32, "5", Duration::from_secs(7200), dec("2.5")),
|
||||
("2.0", 3600, "5", Duration::from_secs(1800), dec("10.0")),
|
||||
("1", 600, "3", Duration::from_secs(600), dec("3")),
|
||||
("0.05", 90, "4", Duration::from_secs(1800), dec("0.20")),
|
||||
("1.5", 10, "2", Duration::new(6, 666_666_667), dec("3.0")),
|
||||
("2.0", 1, "0.25", Duration::from_millis(500), dec("0.500")),
|
||||
] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial(heat, cooldown, run_usd));
|
||||
let Effective::Released(grant) = effective(&path) else { panic!("heat {heat} cooldown {cooldown} run {run_usd}: not released") };
|
||||
assert_eq!(grant.cadence(), cadence, "heat {heat} cooldown {cooldown} run {run_usd}");
|
||||
assert_eq!(grant.run_cap_usd(), cap, "heat {heat} cooldown {cooldown} run {run_usd}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_operators_last_write_is_what_is_in_force_and_provenance_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
|
||||
ControlWriter::open(&path).unwrap().set_brake(false, at(60), "alice").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
|
||||
let state = load_state(&FileSource::new(&path)).unwrap();
|
||||
let brake = state.brake.unwrap();
|
||||
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (false, at(60), "alice"));
|
||||
// The dial survives the brake write untouched.
|
||||
assert_eq!(state.dial.unwrap().set_by, "op");
|
||||
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(120), "bob").unwrap();
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
let brake = load_state(&FileSource::new(&path)).unwrap().brake.unwrap();
|
||||
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (true, at(120), "bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_writes_nothing_and_takes_no_lock_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let store_dir = path.parent().unwrap();
|
||||
let listing = |dir: &Path| {
|
||||
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
// The writer left its sibling lock file; remove it, so a reader that wants one has to create it.
|
||||
for entry in std::fs::read_dir(store_dir).unwrap() {
|
||||
let entry = entry.unwrap().path();
|
||||
if entry != path {
|
||||
std::fs::remove_file(entry).unwrap();
|
||||
}
|
||||
}
|
||||
let before = (listing(store_dir), std::fs::read(&path).unwrap());
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
assert_eq!((listing(store_dir), std::fs::read(&path).unwrap()), before);
|
||||
}
|
||||
|
||||
|
||||
/// A dial document under the dial's type id whose payload is not a dial.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.burn_rate.v1", schema = "ControlBurnRate")]
|
||||
struct WrongDial {
|
||||
#[cultcache(key = 0)]
|
||||
heat: String,
|
||||
}
|
||||
|
||||
fn write_wrong_dial(path: &Path, heat: &str) {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<WrongDial>().unwrap();
|
||||
cache.register_entry_type::<mind_body::control::Brake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache.put(mind_body::control::DIAL_KEY, &WrongDial { heat: heat.into() }).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dial_that_does_not_decode_reads_held_whatever_the_brake_says() {
|
||||
for brake_released in [true, false] {
|
||||
for heat in ["not a number", "NaN", ""] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_brake(brake_released, at(0), "op").unwrap();
|
||||
write_wrong_dial(&path, heat);
|
||||
assert_eq!(
|
||||
effective(&path),
|
||||
held(HeldReason::Undecodable),
|
||||
"brake released={brake_released}, dial heat {heat:?}: a dial that cannot be read is never replaced by a default"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mod grant {
|
||||
//! A Grant is the proof that this instance's own control store released: the
|
||||
//! reader derives the path from the instance, and a launch refuses a Grant
|
||||
//! read for another instance before it asks the mind anything.
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultnet_rs::Selection;
|
||||
use eureka_pipeline::{PipelineDocument, RunTurn};
|
||||
use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage};
|
||||
|
||||
use super::*;
|
||||
use crate::launch::{Clock, Declined, LaunchOutcome, LaunchRequest, Launcher, Ports, open_and_launch};
|
||||
use crate::queue::MindPort;
|
||||
|
||||
fn slug(name: &str) -> Slug {
|
||||
Slug(name.into())
|
||||
}
|
||||
|
||||
fn release(root: &Path, instance: &str) {
|
||||
let mut writer = ControlWriter::open(control_path(root, &slug(instance)).unwrap()).unwrap();
|
||||
writer.set_brake(true, at(0), "op").unwrap();
|
||||
writer.set_dial(dial("1", 3600, "5")).unwrap();
|
||||
}
|
||||
|
||||
fn read(root: &Path, instance: &str) -> Effective {
|
||||
with_root(root, || read_effective(&slug(instance)))
|
||||
}
|
||||
|
||||
/// A mind that answers nothing: a launch that reaches it has gone past the
|
||||
/// instance check.
|
||||
struct SilentMind(Slug);
|
||||
|
||||
impl MindPort for SilentMind {
|
||||
fn instance(&self) -> &Slug {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn query(&self, _selection: &Selection) -> Result<PipelineSelectionPage> {
|
||||
Err(anyhow!("the launch asked the mind"))
|
||||
}
|
||||
|
||||
fn admit(&self, _agent: &str, _session: &str, _documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
|
||||
Err(anyhow!("the launch admitted"))
|
||||
}
|
||||
}
|
||||
|
||||
struct NoUnits;
|
||||
|
||||
impl Launcher for NoUnits {
|
||||
fn start(&self, _instance: &Slug, _turn: RunTurn) -> Result<()> {
|
||||
Err(anyhow!("the launch started a unit"))
|
||||
}
|
||||
|
||||
fn alive(&self, _instance: &Slug, _turn: RunTurn) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
struct Now;
|
||||
|
||||
impl Clock for Now {
|
||||
fn now(&self) -> DateTime<Utc> {
|
||||
at(100)
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_as(mind_instance: &str, grant: Grant, turn: RunTurn) -> Result<LaunchOutcome> {
|
||||
let mind = SilentMind(slug(mind_instance));
|
||||
let ports = Ports { mind: &mind, launcher: &NoUnits, clock: &Now, host: "host" };
|
||||
open_and_launch(&ports, LaunchRequest { turn, claims: vec![], agent: "agent".into(), grant })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reader_makes_a_grant_for_the_instance_whose_store_it_read() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), "void");
|
||||
let Effective::Released(grant) = read(root.path(), "void") else { panic!("void's store is released") };
|
||||
assert_eq!(grant.instance(), &slug("void"));
|
||||
assert_eq!(grant.run_cap_usd(), dec("5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_instances_released_store_yields_no_grant_for_this_one() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), "void");
|
||||
assert_eq!(read(root.path(), "eureka"), held(HeldReason::BrakeAbsent));
|
||||
for walk in ["../void", "void/..", "eureka/../void", "/void"] {
|
||||
assert_eq!(read(root.path(), walk), held(HeldReason::Undecodable), "{walk}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_read_for_one_instance_cannot_launch_another() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), "void");
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
let Effective::Released(grant) = read(root.path(), "void") else { panic!("void's store is released") };
|
||||
// The mind is eureka's; the silent mind and the unit-less launcher fail loudly if the launch goes on.
|
||||
let outcome = launch_as("eureka", grant, turn).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "{turn:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_matching_grant_is_not_stopped_by_the_instance_check() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), "void");
|
||||
let Effective::Released(grant) = read(root.path(), "void") else { panic!("void's store is released") };
|
||||
let error = launch_as("void", grant, RunTurn::SelfRun).unwrap_err();
|
||||
assert!(format!("{error:#}").contains("asked the mind"), "{error:#}");
|
||||
}
|
||||
}
|
||||
|
|
@ -850,19 +850,25 @@ mod tests {
|
|||
fn a_grant_read_for_another_instance_opens_nothing_and_starts_nothing() {
|
||||
let rig = Rig::new();
|
||||
rig.mind.committed(vec![spec("x")]);
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
let claims = if turn == RunTurn::SelfRun { vec![spec_ref("x")] } else { vec![] };
|
||||
let ports = Ports { mind: &rig.mind, launcher: &rig.launcher, clock: &rig.clock, host: "yggdrasil-host" };
|
||||
let grant = Grant::for_test(Slug("another-instance".into()), StdDuration::from_secs(60), five());
|
||||
let outcome = open_and_launch(&ports, LaunchRequest { turn, claims, agent: "agent-x".into(), grant }).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "{turn:?}");
|
||||
// Forgeries shaped like the real name: the same length and prefix, differing only at the end;
|
||||
// a prefix of it; and a name it is a prefix of. Each is checked whole.
|
||||
let forged = [format!("{}k", &INSTANCE[..INSTANCE.len() - 1]), INSTANCE[..INSTANCE.len() - 1].to_string(), format!("{INSTANCE}-test"), "another-instance".to_string()];
|
||||
assert_eq!(forged[0].len(), INSTANCE.len());
|
||||
for name in forged {
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
let claims = if turn == RunTurn::SelfRun { vec![spec_ref("x")] } else { vec![] };
|
||||
let ports = Ports { mind: &rig.mind, launcher: &rig.launcher, clock: &rig.clock, host: "yggdrasil-host" };
|
||||
let grant = Grant::for_test(Slug(name.clone()), StdDuration::from_secs(60), five());
|
||||
let outcome = open_and_launch(&ports, LaunchRequest { turn, claims, agent: "agent-x".into(), grant }).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "{name} {turn:?}");
|
||||
}
|
||||
}
|
||||
assert_eq!((rig.runs(), rig.started()), (0, 0));
|
||||
assert!(matches!(rig.launch(RunTurn::SelfRun, &[spec_ref("x")], five()), LaunchOutcome::Launched { .. }), "the mind's own Grant launches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_persona_turn_with_claims_is_refused_and_opens_nothing() {
|
||||
fn a_persona_turn_with_a_claim_of_any_kind_is_refused_and_opens_nothing() {
|
||||
let rig = Rig::new();
|
||||
rig.mind.committed(vec![spec("x"), spec("blocked")]);
|
||||
rig.mind.committed(vec![question_in("fork", &spec_ref("blocked"))]);
|
||||
|
|
@ -871,6 +877,11 @@ mod tests {
|
|||
let outcome = rig.launch(RunTurn::PersonaTurn, &[spec_ref(item)], five());
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::PersonaRunTakesNoClaims), "{item}");
|
||||
}
|
||||
// And a claim of every kind the leaf has, whether or not the document exists.
|
||||
for kind in PipelineKind::ALL {
|
||||
let outcome = rig.launch(RunTurn::PersonaTurn, &[reference(*kind, "no-such-document")], five());
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::PersonaRunTakesNoClaims), "{kind:?}");
|
||||
}
|
||||
assert_eq!((rig.runs(), rig.started()), (0, 0));
|
||||
assert!(matches!(rig.launch(RunTurn::PersonaTurn, &[], five()), LaunchOutcome::Launched { .. }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ use std::time::Duration;
|
|||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
|
||||
use mind_body::control::{
|
||||
BRAKE_KEY, BurnRate, ControlWriter, Effective, FileSource, HeldReason, control_path, load_state, read_effective,
|
||||
};
|
||||
use mind_body::control::{BurnRate, ControlWriter, FileSource, control_path, load_state};
|
||||
use eureka_pipeline::Slug;
|
||||
use rust_decimal::Decimal;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -38,10 +36,6 @@ fn store_path(dir: &Path) -> PathBuf {
|
|||
dir.join("eureka").join("control.cc")
|
||||
}
|
||||
|
||||
fn held(reason: HeldReason) -> Effective {
|
||||
Effective::Held { reason }
|
||||
}
|
||||
|
||||
/// A written store: brake released and the given dial, through the real writer.
|
||||
fn released_store(dir: &Path, dial: BurnRate) -> PathBuf {
|
||||
let path = store_path(dir);
|
||||
|
|
@ -51,193 +45,6 @@ fn released_store(dir: &Path, dial: BurnRate) -> PathBuf {
|
|||
path
|
||||
}
|
||||
|
||||
/// The reader's answer for the store at `<root>/<instance>/control.cc`: the
|
||||
/// reader derives that path itself from the root and the instance.
|
||||
fn effective(path: &Path) -> Effective {
|
||||
let directory = path.parent().unwrap();
|
||||
let instance = Slug(directory.file_name().unwrap().to_str().unwrap().into());
|
||||
read_effective(directory.parent().unwrap(), &instance)
|
||||
}
|
||||
|
||||
/// A dial written around the writer's bounds check, so the reader's own check
|
||||
/// is what is under test.
|
||||
fn write_unchecked_dial(path: &Path, dial: &BurnRate) {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<BurnRate>().unwrap();
|
||||
cache.register_entry_type::<mind_body::control::Brake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache.put(mind_body::control::DIAL_KEY, dial).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_store_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(effective(&store_path(dir.path())), held(HeldReason::BrakeAbsent));
|
||||
assert!(!store_path(dir.path()).exists(), "reading must not create the store");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_brake_reads_as_held_even_with_a_dial() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_dial(dial("1", 3600, "1")).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeAbsent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undecodable_store_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let good = std::fs::read(&path).unwrap();
|
||||
|
||||
// Garbage in place of the store.
|
||||
std::fs::write(&path, b"this is not a cultcache store").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
|
||||
// A store cut off mid-document.
|
||||
std::fs::write(&path, &good[..good.len() / 2]).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
|
||||
// The untouched store is still released, so the two reads above were the damage.
|
||||
std::fs::write(&path, &good).unwrap();
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_that_cannot_be_read_as_a_file_reads_as_held() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// A directory sits where the store should be: it exists and cannot be read as a file.
|
||||
let path = store_path(dir.path());
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_document_type_makes_the_store_undecodable() {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.something_else.v1", schema = "Other")]
|
||||
struct Other {
|
||||
#[cultcache(key = 0)]
|
||||
value: u32,
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Other>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
|
||||
cache.put("x", &Other { value: 1 }).unwrap();
|
||||
// The store now holds a released brake, a good dial and a foreign document.
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_brake_payload_that_does_not_decode_makes_the_store_undecodable() {
|
||||
// Same type id as the brake, a different shape: the payload cannot decode as a Brake.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.brake.v1", schema = "ControlBrake")]
|
||||
struct WrongBrake {
|
||||
#[cultcache(key = 0)]
|
||||
released: String,
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<WrongBrake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
|
||||
cache.put(BRAKE_KEY, &WrongBrake { released: "yes".into() }).unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_decode_failure_outranks_a_held_brake() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
ControlWriter::open(&path).unwrap().set_brake(false, at(5), "op").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
|
||||
std::fs::write(&path, b"junk").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::Undecodable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_brake_with_no_dial_reads_held_dial_absent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::DialAbsent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dial_bounds_are_inclusive_at_both_ends() {
|
||||
for (heat, expected_in) in [("0.05", true), ("2.0", true), ("0.0499", false), ("2.0001", false), ("0", false), ("-1", false), ("3", false)] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store_unchecked(dir.path(), dial(heat, 3600, "1"));
|
||||
let got = effective(&path);
|
||||
if expected_in {
|
||||
assert!(matches!(got, Effective::Released(_)), "heat {heat}: {got:?}");
|
||||
} else {
|
||||
assert_eq!(got, held(HeldReason::DialOutOfBounds), "heat {heat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_base_cooldown_or_a_non_positive_run_cost_is_out_of_bounds() {
|
||||
for bad in [dial("1", 0, "1"), dial("1", 3600, "0"), dial("1", 3600, "-0.5")] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store_unchecked(dir.path(), bad.clone());
|
||||
assert_eq!(effective(&path), held(HeldReason::DialOutOfBounds), "{bad:?}");
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let smallest = released_store_unchecked(dir.path(), dial("1", 1, "0.000001"));
|
||||
assert!(matches!(effective(&smallest), Effective::Released(_)));
|
||||
}
|
||||
|
||||
fn released_store_unchecked(dir: &Path, bad: BurnRate) -> PathBuf {
|
||||
let path = store_path(dir);
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(0), "op").unwrap();
|
||||
write_unchecked_dial(&path, &bad);
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_and_run_cap_are_the_dial_divided_and_multiplied_by_heat() {
|
||||
for (heat, cooldown, run_usd, cadence, cap) in [
|
||||
("0.5", 3600u32, "5", Duration::from_secs(7200), dec("2.5")),
|
||||
("2.0", 3600, "5", Duration::from_secs(1800), dec("10.0")),
|
||||
("1", 600, "3", Duration::from_secs(600), dec("3")),
|
||||
("0.05", 90, "4", Duration::from_secs(1800), dec("0.20")),
|
||||
("1.5", 10, "2", Duration::new(6, 666_666_667), dec("3.0")),
|
||||
("2.0", 1, "0.25", Duration::from_millis(500), dec("0.500")),
|
||||
] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial(heat, cooldown, run_usd));
|
||||
let Effective::Released(grant) = effective(&path) else { panic!("heat {heat} cooldown {cooldown} run {run_usd}: not released") };
|
||||
assert_eq!(grant.cadence(), cadence, "heat {heat} cooldown {cooldown} run {run_usd}");
|
||||
assert_eq!(grant.run_cap_usd(), cap, "heat {heat} cooldown {cooldown} run {run_usd}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_operators_last_write_is_what_is_in_force_and_provenance_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
|
||||
ControlWriter::open(&path).unwrap().set_brake(false, at(60), "alice").unwrap();
|
||||
assert_eq!(effective(&path), held(HeldReason::BrakeHeld));
|
||||
let state = load_state(&FileSource::new(&path)).unwrap();
|
||||
let brake = state.brake.unwrap();
|
||||
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (false, at(60), "alice"));
|
||||
// The dial survives the brake write untouched.
|
||||
assert_eq!(state.dial.unwrap().set_by, "op");
|
||||
|
||||
ControlWriter::open(&path).unwrap().set_brake(true, at(120), "bob").unwrap();
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
let brake = load_state(&FileSource::new(&path)).unwrap().brake.unwrap();
|
||||
assert_eq!((brake.released, brake.set_at, brake.set_by.as_str()), (true, at(120), "bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_the_dial_replaces_it_whole_and_keeps_the_brake() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -262,28 +69,6 @@ fn the_writer_refuses_an_out_of_bounds_dial_and_leaves_the_store_unchanged() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_writes_nothing_and_takes_no_lock_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = released_store(dir.path(), dial("1", 3600, "1"));
|
||||
let store_dir = path.parent().unwrap();
|
||||
let listing = |dir: &Path| {
|
||||
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
// The writer left its sibling lock file; remove it, so a reader that wants one has to create it.
|
||||
for entry in std::fs::read_dir(store_dir).unwrap() {
|
||||
let entry = entry.unwrap().path();
|
||||
if entry != path {
|
||||
std::fs::remove_file(entry).unwrap();
|
||||
}
|
||||
}
|
||||
let before = (listing(store_dir), std::fs::read(&path).unwrap());
|
||||
assert!(matches!(effective(&path), Effective::Released(_)));
|
||||
assert_eq!((listing(store_dir), std::fs::read(&path).unwrap()), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_control_path_is_derived_from_a_valid_instance_only() {
|
||||
let root = Path::new(mind_body::control::CONTROL_ROOT);
|
||||
|
|
@ -292,41 +77,6 @@ fn the_control_path_is_derived_from_a_valid_instance_only() {
|
|||
assert!(control_path(root, &Slug(bad.into())).is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A dial document under the dial's type id whose payload is not a dial.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "eureka.control.burn_rate.v1", schema = "ControlBurnRate")]
|
||||
struct WrongDial {
|
||||
#[cultcache(key = 0)]
|
||||
heat: String,
|
||||
}
|
||||
|
||||
fn write_wrong_dial(path: &Path, heat: &str) {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<WrongDial>().unwrap();
|
||||
cache.register_entry_type::<mind_body::control::Brake>().unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache.put(mind_body::control::DIAL_KEY, &WrongDial { heat: heat.into() }).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dial_that_does_not_decode_reads_held_whatever_the_brake_says() {
|
||||
for brake_released in [true, false] {
|
||||
for heat in ["not a number", "NaN", ""] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = store_path(dir.path());
|
||||
ControlWriter::open(&path).unwrap().set_brake(brake_released, at(0), "op").unwrap();
|
||||
write_wrong_dial(&path, heat);
|
||||
assert_eq!(
|
||||
effective(&path),
|
||||
held(HeldReason::Undecodable),
|
||||
"brake released={brake_released}, dial heat {heat:?}: a dial that cannot be read is never replaced by a default"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod modes {
|
||||
use super::*;
|
||||
|
|
@ -391,7 +141,7 @@ mod modes {
|
|||
);
|
||||
assert!(std::fs::OpenOptions::new().write(true).open(&path).is_err());
|
||||
assert!(std::fs::OpenOptions::new().write(true).open(path.with_file_name("control.cc.lock")).is_err());
|
||||
assert!(matches!(effective(&path), Effective::Released(_)), "a non-root user can still read the store");
|
||||
assert!(load_state(&FileSource::new(&path)).unwrap().brake.unwrap().released, "a non-root user can still read the store");
|
||||
}
|
||||
other => panic!("unknown role {other}"),
|
||||
}
|
||||
|
|
|
|||
66
crates/mind-body/tests/forge.rs
Normal file
66
crates/mind-body/tests/forge.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
//! The brake-grant rule seen from outside the crate: the only reader takes the
|
||||
//! instance, so whatever a caller builds elsewhere (a store in a temporary
|
||||
//! directory, a symlink to another instance's directory, a copy of another
|
||||
//! instance's store, the relative root `.`) is never what a Grant is made from.
|
||||
//! The signature half of the rule (no root argument, no source reader, no
|
||||
//! struct literal, no test constructor) is the `compile_fail` examples on
|
||||
//! `read_effective`; this is the behavioural half, committed because it is the
|
||||
//! only defence of the rule against a forged store.
|
||||
|
||||
#![cfg(unix)]
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use eureka_pipeline::Slug;
|
||||
use mind_body::control::{BurnRate, ControlWriter, Effective, HeldReason, control_path, read_effective};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
/// A name no real install has a store for under the real root.
|
||||
const FORGED: &str = "forge-probe-instance";
|
||||
const OTHER: &str = "forge-probe-other";
|
||||
|
||||
fn release(root: &Path, instance: &str) {
|
||||
let mut writer = ControlWriter::open(control_path(root, &Slug(instance.into())).unwrap()).unwrap();
|
||||
let at = Utc.timestamp_opt(1_800_000_000, 0).unwrap();
|
||||
writer.set_brake(true, at, "forger").unwrap();
|
||||
writer
|
||||
.set_dial(BurnRate { heat: Decimal::ONE, base_cooldown_s: 3600, base_run_usd: Decimal::new(5, 0), set_at: at, set_by: "forger".into() })
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn reads(instance: &str) -> Effective {
|
||||
read_effective(&Slug(instance.into()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_forged_store_yields_a_grant() {
|
||||
assert_eq!(reads(FORGED), Effective::Held { reason: HeldReason::BrakeAbsent }, "the real root has no store for the probe's name");
|
||||
|
||||
// A released store for the name in a root of the forger's choosing.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
release(tmp.path(), FORGED);
|
||||
assert!(matches!(reads(FORGED), Effective::Held { .. }), "store in a temporary root");
|
||||
|
||||
// A symlink from the name to another instance's released directory.
|
||||
let linked = tempfile::tempdir().unwrap();
|
||||
release(linked.path(), OTHER);
|
||||
std::os::unix::fs::symlink(linked.path().join(OTHER), linked.path().join(FORGED)).unwrap();
|
||||
assert!(matches!(reads(FORGED), Effective::Held { .. }), "symlink to another instance");
|
||||
|
||||
// A copy of another instance's store under the name.
|
||||
let copied = tempfile::tempdir().unwrap();
|
||||
release(copied.path(), OTHER);
|
||||
std::fs::create_dir_all(copied.path().join(FORGED)).unwrap();
|
||||
std::fs::copy(copied.path().join(OTHER).join("control.cc"), copied.path().join(FORGED).join("control.cc")).unwrap();
|
||||
assert!(matches!(reads(FORGED), Effective::Held { .. }), "copy of another instance's store");
|
||||
|
||||
// The relative root '.': the working directory holds <name>/control.cc.
|
||||
let here = tempfile::tempdir().unwrap();
|
||||
release(here.path(), FORGED);
|
||||
let before = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(here.path()).unwrap();
|
||||
let relative = reads(FORGED);
|
||||
std::env::set_current_dir(before).unwrap();
|
||||
assert!(matches!(relative, Effective::Held { .. }), "relative root");
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
//! A Grant is the proof that this instance's own control store released. The
|
||||
//! tests are an outside crate's view: the only reader takes a root and an
|
||||
//! instance and derives the store path itself, and a launch refuses a Grant
|
||||
//! read for another instance before it asks the mind anything.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use cultnet_rs::Selection;
|
||||
use eureka_pipeline::{PipelineDocument, RunTurn, Slug};
|
||||
use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage};
|
||||
use mind_body::control::{BurnRate, ControlWriter, Effective, HeldReason, control_path, read_effective};
|
||||
use mind_body::launch::{Clock, Declined, LaunchOutcome, LaunchRequest, Launcher, Ports, open_and_launch};
|
||||
use mind_body::queue::MindPort;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
fn slug(name: &str) -> Slug {
|
||||
Slug(name.into())
|
||||
}
|
||||
|
||||
fn release(root: &Path, instance: &Slug) {
|
||||
let mut writer = ControlWriter::open(control_path(root, instance).unwrap()).unwrap();
|
||||
let at = Utc.timestamp_opt(1_800_000_000, 0).unwrap();
|
||||
writer.set_brake(true, at, "op").unwrap();
|
||||
writer.set_dial(BurnRate { heat: Decimal::ONE, base_cooldown_s: 3600, base_run_usd: Decimal::new(5, 0), set_at: at, set_by: "op".into() }).unwrap();
|
||||
}
|
||||
|
||||
/// A mind that answers nothing: a launch that reaches it has gone past the
|
||||
/// instance check.
|
||||
struct SilentMind(Slug);
|
||||
|
||||
impl MindPort for SilentMind {
|
||||
fn instance(&self) -> &Slug {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn query(&self, _selection: &Selection) -> Result<PipelineSelectionPage> {
|
||||
Err(anyhow!("the launch asked the mind"))
|
||||
}
|
||||
|
||||
fn admit(&self, _agent: &str, _session: &str, _documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
|
||||
Err(anyhow!("the launch admitted"))
|
||||
}
|
||||
}
|
||||
|
||||
struct NoUnits;
|
||||
|
||||
impl Launcher for NoUnits {
|
||||
fn start(&self, _instance: &Slug, _turn: RunTurn) -> Result<()> {
|
||||
Err(anyhow!("the launch started a unit"))
|
||||
}
|
||||
|
||||
fn alive(&self, _instance: &Slug, _turn: RunTurn) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
struct Now;
|
||||
|
||||
impl Clock for Now {
|
||||
fn now(&self) -> DateTime<Utc> {
|
||||
Utc.timestamp_opt(1_800_000_100, 0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_as(mind_instance: &str, grant: mind_body::control::Grant, turn: RunTurn) -> Result<LaunchOutcome> {
|
||||
let mind = SilentMind(slug(mind_instance));
|
||||
let ports = Ports { mind: &mind, launcher: &NoUnits, clock: &Now, host: "host" };
|
||||
open_and_launch(&ports, LaunchRequest { turn, claims: vec![], agent: "agent".into(), grant })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reader_makes_a_grant_for_the_instance_whose_store_it_read() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), &slug("void"));
|
||||
let Effective::Released(grant) = read_effective(root.path(), &slug("void")) else { panic!("void's store is released") };
|
||||
assert_eq!(grant.instance(), &slug("void"));
|
||||
assert_eq!(grant.run_cap_usd(), Decimal::new(5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_instances_released_store_yields_no_grant_for_this_one() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), &slug("void"));
|
||||
// The operator holds eureka's brake: eureka has no released store under this root.
|
||||
assert_eq!(read_effective(root.path(), &slug("eureka")), Effective::Held { reason: HeldReason::BrakeAbsent });
|
||||
// A path-shaped instance cannot reach void's store by walking.
|
||||
for walk in ["../void", "void/..", "eureka/../void", "/void"] {
|
||||
assert_eq!(read_effective(root.path(), &slug(walk)), Effective::Held { reason: HeldReason::Undecodable }, "{walk}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_read_for_one_instance_cannot_launch_another() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), &slug("void"));
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
let Effective::Released(grant) = read_effective(root.path(), &slug("void")) else { panic!("void's store is released") };
|
||||
// The mind is eureka's; the silent mind and the unit-less launcher fail loudly if the launch goes on.
|
||||
let outcome = launch_as("eureka", grant, turn).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "{turn:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_matching_grant_is_not_stopped_by_the_instance_check() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
release(root.path(), &slug("void"));
|
||||
let Effective::Released(grant) = read_effective(root.path(), &slug("void")) else { panic!("void's store is released") };
|
||||
// Past the check the silent mind is asked, so the launch errors there.
|
||||
let error = launch_as("void", grant, RunTurn::SelfRun).unwrap_err();
|
||||
assert!(format!("{error:#}").contains("asked the mind"), "{error:#}");
|
||||
}
|
||||
Loading…
Reference in a new issue