//! 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::>() .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 = 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" ); } #[test] fn the_broker_table_is_optional_and_its_socket_has_no_default() { let c = Config::load(&fixture("minimal.toml")).unwrap(); assert_eq!( c.broker.socket, None, "no socket means no broker, never a guessed path" ); assert_eq!(c.broker.timeout_ms, 120_000); } #[test] fn the_broker_table_can_be_set_and_rejects_unknown_keys() { let base = std::fs::read_to_string(fixture("m2b.toml")).unwrap(); let text = format!( "{base}\n[broker]\nsocket = \"/run/boxmaker/loop-broker/broker.sock\"\ntimeout_ms = 5000\n" ); let c = Config::parse(&text).unwrap(); assert_eq!( c.broker.socket, Some(PathBuf::from("/run/boxmaker/loop-broker/broker.sock")) ); assert_eq!(c.broker.timeout_ms, 5000); let only_timeout = format!("{base}\n[broker]\ntimeout_ms = 5000\n"); let c = Config::parse(&only_timeout).unwrap(); assert_eq!((c.broker.socket, c.broker.timeout_ms), (None, 5000)); let only_socket = format!("{base}\n[broker]\nsocket = \"/b.sock\"\n"); let c = Config::parse(&only_socket).unwrap(); assert_eq!( c.broker.timeout_ms, 120_000, "a partial table keeps the default" ); for bad in [ "zz_unknown = 1", "timeout = 5000", "timeout_ms = -1", "timeout_ms = \"5s\"", "socket = 7", ] { let text = format!("{base}\n[broker]\n{bad}\n"); assert!(Config::parse(&text).is_err(), "[broker] accepted `{bad}`"); } }