Files
boxmaker/crates/loopd/tests/config.rs
T
kyle ec5d887daa Add the paths, channel, loop and baseline config tables
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-18 17:36:20 -07:00

227 lines
7.1 KiB
Rust

//! Tests for `loopd`'s configuration. Do not edit: these define the required behaviour.
use loopd::config::{Config, ConfigError, Limits, Sampling};
use std::path::{Path, PathBuf};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/config")
.join(name)
}
#[test]
fn minimal_file_gets_the_documented_defaults() {
let c = Config::load(&fixture("minimal.toml")).unwrap();
assert_eq!(
c.infer.socket,
PathBuf::from("/run/boxmaker/infer/infer.sock")
);
assert_eq!(c.infer.model, "ornith-1.5-35b-a3b");
assert_eq!((c.slots.main, c.slots.background), (0, 1));
assert_eq!(c.expect.n_ctx, 131_072);
assert_eq!(c.expect.slots, 2);
assert_eq!(
c.expect.template_sha256.to_hex(),
"f55f52930aa8bf44ab5cb85f99370fcc3c56e9a85640b812086d5330bce5d86b"
);
assert_eq!(
c.sampling,
Sampling {
temperature: 0.6,
top_p: 0.95,
top_k: 20
}
);
let want = Limits {
poll_ms: 5_000,
busy_wait_ms: 600_000,
load_wait_ms: 180_000,
idle_grace_ms: 30_000,
liveness_ms: 30_000,
thinking_cap: 4_096,
thinking_overrun: 256,
max_tokens: 8_192,
queue_len: 8,
retry_attempts: 4,
retry_backoff_ms: vec![2_000, 8_000, 30_000],
retry_window_ms: 300_000,
};
assert_eq!(c.limits, want);
assert_eq!(Limits::default(), want);
}
#[test]
fn full_file_overrides_every_default() {
let c = Config::load(&fixture("full.toml")).unwrap();
assert_eq!(
c.sampling,
Sampling {
temperature: 0.2,
top_p: 0.9,
top_k: 40
}
);
let want = Limits {
poll_ms: 50,
busy_wait_ms: 200,
load_wait_ms: 300,
idle_grace_ms: 150,
liveness_ms: 100,
thinking_cap: 20,
thinking_overrun: 10,
max_tokens: 512,
queue_len: 1,
retry_attempts: 2,
retry_backoff_ms: vec![10],
retry_window_ms: 1_000,
};
assert_eq!(c.limits, want);
}
#[test]
fn a_partial_limits_table_keeps_the_other_defaults() {
let text = std::fs::read_to_string(fixture("minimal.toml")).unwrap();
let c = Config::parse(&format!("{text}\n[limits]\nliveness_ms = 1234\n")).unwrap();
assert_eq!(c.limits.liveness_ms, 1234);
assert_eq!(c.limits.poll_ms, 5_000);
}
/// Every table in the file must reject a key it does not know: a misspelt limit that silently
/// fell back to its default would be a limit the owner believes is set and is not.
#[test]
fn unknown_keys_are_errors_in_every_table() {
let text = std::fs::read_to_string(fixture("full.toml")).unwrap();
assert!(Config::parse(&text).is_ok());
for table in ["infer", "slots", "expect", "sampling", "limits"] {
let header = format!("[{table}]\n");
assert!(text.contains(&header), "fixture has no [{table}] table");
let bad = text.replacen(&header, &format!("{header}zz_unknown = 1\n"), 1);
assert!(
Config::parse(&bad).is_err(),
"[{table}] accepted an unknown key"
);
}
assert!(
Config::parse(&format!("zz_unknown = 1\n{text}")).is_err(),
"top level"
);
assert!(
Config::parse(&format!("{text}\n[zz_unknown]\nx = 1\n")).is_err(),
"unknown table"
);
}
#[test]
fn required_tables_and_values_are_checked() {
let text = std::fs::read_to_string(fixture("minimal.toml")).unwrap();
for table in ["infer", "slots", "expect"] {
let without: String = text
.split("\n\n")
.filter(|block| !block.trim_start().starts_with(&format!("[{table}]")))
.collect::<Vec<_>>()
.join("\n\n");
assert!(Config::parse(&without).is_err(), "[{table}] is required");
}
let bad_hash = text.replace("f55f5293", "F55F5293");
assert!(
Config::parse(&bad_hash).is_err(),
"the hash must be lowercase hex"
);
let negative = text.replace("main = 0", "main = -1");
assert!(Config::parse(&negative).is_err());
}
#[test]
fn load_reports_which_file_failed() {
let missing = fixture("does-not-exist.toml");
match Config::load(&missing) {
Err(ConfigError::Read(path, _)) => assert_eq!(path, missing),
other => panic!("expected a read error, got {other:?}"),
}
let e: Box<dyn std::error::Error> = Box::new(Config::load(&missing).unwrap_err());
assert!(e.to_string().contains("does-not-exist.toml"));
}
// ---- M2b: paths, channel, loop and baseline ----
#[test]
fn the_m2b_tables_have_defaults() {
let c = Config::load(&fixture("minimal.toml")).unwrap();
assert_eq!(
c.channel.socket,
PathBuf::new(),
"empty means: under the home"
);
assert_eq!(c.channel_socket(), c.paths.home.join("run/loop/loop.sock"));
assert_eq!(
(
c.r#loop.tool_iterations,
c.r#loop.repeat_detection,
c.r#loop.tool_result_cap
),
(8, true, 16 * 1024)
);
// A relative system prompt is taken from the config file's directory.
assert_eq!(c.baseline.system, fixture("system.md"));
// The home comes from BOXMAKER_HOME when set, else /var/lib/boxmaker. Either way it is absolute.
assert!(c.paths.home.is_absolute(), "{:?}", c.paths.home);
}
#[test]
fn the_m2b_tables_can_be_set() {
let c = Config::load(&fixture("m2b.toml")).unwrap();
assert_eq!(c.paths.home, PathBuf::from("/srv/boxmaker"));
assert_eq!(
c.channel.socket,
PathBuf::from("/run/boxmaker/loop/loop.sock")
);
assert_eq!(
c.channel_socket(),
PathBuf::from("/run/boxmaker/loop/loop.sock")
);
assert_eq!(
(
c.r#loop.tool_iterations,
c.r#loop.repeat_detection,
c.r#loop.tool_result_cap
),
(3, false, 1024)
);
assert_eq!(
c.baseline.system,
fixture("prompts/agent.md"),
"relative to the config file"
);
let text = std::fs::read_to_string(fixture("m2b.toml")).unwrap();
let absolute = text.replace("prompts/agent.md", "/etc/boxmaker/system.md");
assert_eq!(
Config::parse(&absolute).unwrap().baseline.system,
PathBuf::from("/etc/boxmaker/system.md")
);
}
#[test]
fn unknown_keys_are_errors_in_the_m2b_tables_too() {
let text = std::fs::read_to_string(fixture("m2b.toml")).unwrap();
assert!(Config::parse(&text).is_ok());
for table in ["paths", "channel", "loop", "baseline"] {
let header = format!("[{table}]\n");
let bad = text.replacen(&header, &format!("{header}zz_unknown = 1\n"), 1);
assert!(
Config::parse(&bad).is_err(),
"[{table}] accepted an unknown key"
);
}
let partial = text.replace("tool_iterations = 3\nrepeat_detection = false\n", "");
let c = Config::parse(&partial).unwrap();
assert_eq!(
(
c.r#loop.tool_iterations,
c.r#loop.repeat_detection,
c.r#loop.tool_result_cap
),
(8, true, 1024),
"a partial table keeps the other defaults"
);
}