use std::path::Path;
use std::collections::HashSet;
use super::common::FenceExtractor;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_toml_examples_parse() {
let config_doc_path = Path::new("docs/CONFIGURATION.md");
assert!(
config_doc_path.exists(),
"CONFIGURATION.md not found at docs/CONFIGURATION.md"
);
let extractor = FenceExtractor::new(config_doc_path)
.expect("Failed to read CONFIGURATION.md");
let toml_blocks = extractor.extract_by_language("toml");
assert!(
!toml_blocks.is_empty(),
"No TOML code blocks found in CONFIGURATION.md"
);
for (idx, block) in toml_blocks.iter().enumerate() {
let parse_result: Result<toml::Value, _> = toml::from_str(&block.content);
assert!(
parse_result.is_ok(),
"TOML block {} failed to parse: {:?}\nContent:\n{}",
idx,
parse_result.err(),
block.content
);
}
}
#[test]
fn test_config_fields_exist_in_struct() {
let config_doc_path = Path::new("docs/CONFIGURATION.md");
let extractor = FenceExtractor::new(config_doc_path)
.expect("Failed to read CONFIGURATION.md");
let toml_blocks = extractor.extract_by_language("toml");
let valid_defaults_fields: HashSet<&str> = [
"model",
"max_turns",
"packet_max_bytes",
"packet_max_lines",
"output_format",
"verbose",
"phase_timeout",
"runner_mode",
"runner_distro",
"claude_path",
].iter().copied().collect();
let valid_selectors_fields: HashSet<&str> = [
"include",
"exclude",
].iter().copied().collect();
let valid_runner_fields: HashSet<&str> = [
"mode",
"distro",
"claude_path",
].iter().copied().collect();
for block in toml_blocks.iter() {
let parsed: toml::Value = toml::from_str(&block.content)
.expect("TOML should parse (verified in previous test)");
if let Some(table) = parsed.as_table() {
if let Some(defaults) = table.get("defaults") {
if let Some(defaults_table) = defaults.as_table() {
for field_name in defaults_table.keys() {
assert!(
valid_defaults_fields.contains(field_name.as_str()),
"Unknown field '{}' in [defaults] section. Valid fields: {:?}",
field_name,
valid_defaults_fields
);
}
}
}
if let Some(selectors) = table.get("selectors") {
if let Some(selectors_table) = selectors.as_table() {
for field_name in selectors_table.keys() {
assert!(
valid_selectors_fields.contains(field_name.as_str()),
"Unknown field '{}' in [selectors] section. Valid fields: {:?}",
field_name,
valid_selectors_fields
);
}
}
}
if let Some(runner) = table.get("runner") {
if let Some(runner_table) = runner.as_table() {
for field_name in runner_table.keys() {
assert!(
valid_runner_fields.contains(field_name.as_str()),
"Unknown field '{}' in [runner] section. Valid fields: {:?}",
field_name,
valid_runner_fields
);
}
}
}
}
}
}
struct CwdGuard(std::path::PathBuf);
impl CwdGuard {
fn new(path: &std::path::Path) -> Self {
let prev = std::env::current_dir().expect("Failed to get current dir");
std::env::set_current_dir(path).expect("Failed to change dir");
Self(prev)
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
#[test]
fn test_config_precedence() {
use std::process::Command;
use std::fs;
use tempfile::TempDir;
use serde_json::Value;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let xchecker_home = temp_dir.path().join(".xchecker");
let spec_id = "test-config-precedence";
let spec_root = xchecker_home.join("specs").join(spec_id);
fs::create_dir_all(&spec_root).expect("Failed to create spec root");
let receipts_dir = spec_root.join("receipts");
fs::create_dir_all(&receipts_dir).expect("Failed to create receipts dir");
let receipt_json = serde_json::json!({
"schema_version": "1",
"emitted_at": "2025-01-01T00:00:00Z",
"spec_id": spec_id,
"phase": "requirements",
"xchecker_version": "0.1.0",
"claude_cli_version": "0.8.1",
"model_full_name": "haiku",
"canonicalization_version": "yaml-v1,md-v1",
"canonicalization_backend": "jcs-rfc8785",
"flags": {},
"runner": "native",
"packet": {
"files": [],
"max_bytes": 65536,
"max_lines": 1200
},
"outputs": [],
"exit_code": 0,
"warnings": []
});
let receipt_path = receipts_dir.join("requirements-20250101_000000.json");
fs::write(&receipt_path, serde_json::to_string_pretty(&receipt_json).unwrap())
.expect("Failed to write receipt");
let config_dir = spec_root.join(".xchecker");
fs::create_dir_all(&config_dir).expect("Failed to create config dir");
let config_content = r#"
[defaults]
model = "opus"
max_turns = 10
verbose = false
phase_timeout = 900
[runner]
mode = "native"
"#;
let config_path = config_dir.join("config.toml");
fs::write(&config_path, config_content).expect("Failed to write config file");
let _cwd_guard = CwdGuard::new(&spec_root);
let output = Command::new(env!("CARGO_BIN_EXE_xchecker"))
.env("XCHECKER_HOME", &xchecker_home)
.current_dir(&spec_root)
.args(&["status", spec_id, "--json", "--verbose"])
.output()
.expect("Failed to execute xchecker status");
if !output.status.success() {
eprintln!("Command failed with status: {}", output.status);
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
panic!("xchecker status command failed");
}
let stdout = String::from_utf8_lossy(&output.stdout);
let status_output: Value = serde_json::from_str(&stdout)
.expect("Failed to parse status JSON output");
let effective_config = status_output.get("effective_config")
.expect("effective_config field missing from status output");
if let Some(verbose_config) = effective_config.get("verbose") {
let source = verbose_config.get("source")
.and_then(|s| s.as_str())
.expect("verbose.source missing");
let value = verbose_config.get("value")
.and_then(|v| v.as_bool())
.expect("verbose.value missing or not boolean");
assert_eq!(source, "cli",
"verbose source should be 'cli' (case-exact), got: {}", source);
assert_eq!(value, true,
"verbose value should be true from CLI override");
}
if let Some(model_config) = effective_config.get("model") {
let source = model_config.get("source")
.and_then(|s| s.as_str())
.expect("model.source missing");
let value = model_config.get("value")
.and_then(|v| v.as_str())
.expect("model.value missing or not string");
assert_eq!(source, "config",
"model source should be 'config' (case-exact), got: {}", source);
assert_eq!(value, "opus",
"model value should be 'opus' from config file");
}
if let Some(max_turns_config) = effective_config.get("max_turns") {
let source = max_turns_config.get("source")
.and_then(|s| s.as_str())
.expect("max_turns.source missing");
let value = max_turns_config.get("value")
.and_then(|v| v.as_i64())
.expect("max_turns.value missing or not number");
assert_eq!(source, "config",
"max_turns source should be 'config' (case-exact), got: {}", source);
assert_eq!(value, 10,
"max_turns value should be 10 from config file");
}
if let Some(phase_timeout_config) = effective_config.get("phase_timeout") {
let source = phase_timeout_config.get("source")
.and_then(|s| s.as_str())
.expect("phase_timeout.source missing");
let value = phase_timeout_config.get("value")
.and_then(|v| v.as_i64())
.expect("phase_timeout.value missing or not number");
assert_eq!(source, "config",
"phase_timeout source should be 'config' (case-exact), got: {}", source);
assert_eq!(value, 900,
"phase_timeout value should be 900 from config file");
}
if let Some(packet_max_bytes_config) = effective_config.get("packet_max_bytes") {
let source = packet_max_bytes_config.get("source")
.and_then(|s| s.as_str())
.expect("packet_max_bytes.source missing");
let value = packet_max_bytes_config.get("value")
.and_then(|v| v.as_i64())
.expect("packet_max_bytes.value missing or not number");
assert_eq!(source, "default",
"packet_max_bytes source should be 'default' (case-exact), got: {}", source);
assert_eq!(value, 65536,
"packet_max_bytes value should be 65536 from defaults");
}
if let Some(runner_mode_config) = effective_config.get("runner_mode") {
let source = runner_mode_config.get("source")
.and_then(|s| s.as_str())
.expect("runner_mode.source missing");
let value = runner_mode_config.get("value")
.and_then(|v| v.as_str())
.expect("runner_mode.value missing or not string");
assert_eq!(source, "config",
"runner_mode source should be 'config' (case-exact), got: {}", source);
assert_eq!(value, "native",
"runner_mode value should be 'native' from config file");
}
println!("✓ Config precedence test passed:");
println!(" - CLI overrides (verbose) correctly marked as 'cli'");
println!(" - Config file overrides (model, max_turns, phase_timeout, runner_mode) correctly marked as 'config'");
println!(" - Default values (packet_max_bytes) correctly marked as 'default'");
}
#[test]
fn test_config_defaults() {
use xchecker::config::{Defaults, RunnerConfig};
let config_doc_path = Path::new("docs/CONFIGURATION.md");
assert!(
config_doc_path.exists(),
"CONFIGURATION.md not found at docs/CONFIGURATION.md"
);
let content = std::fs::read_to_string(config_doc_path)
.expect("Failed to read CONFIGURATION.md");
let mut documented_defaults: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let lines: Vec<&str> = content.lines().collect();
let mut in_defaults_table = false;
for line in lines.iter() {
if line.contains("### [defaults]") {
in_defaults_table = true;
continue;
}
if in_defaults_table && line.starts_with("###") && !line.contains("[defaults]") {
break;
}
if in_defaults_table && line.starts_with("|") && !line.contains("Key | Type") && !line.contains("---|") {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 4 {
let key = parts[1].trim().trim_matches('`');
let default_value = parts[3].trim().trim_matches('`');
if !key.is_empty() && !default_value.is_empty() {
documented_defaults.insert(key.to_string(), default_value.to_string());
}
}
}
}
let defaults = Defaults::default();
let runner = RunnerConfig::default();
if let Some(doc_model) = documented_defaults.get("model") {
assert_eq!(defaults.model, None,
"Code default for model is None, but documentation shows: {}", doc_model);
}
if let Some(doc_max_turns) = documented_defaults.get("max_turns") {
let expected: u32 = doc_max_turns.parse().expect("max_turns should be a number");
assert_eq!(defaults.max_turns, Some(expected),
"max_turns default mismatch: code has {:?}, docs say {}",
defaults.max_turns, doc_max_turns);
}
if let Some(doc_output_format) = documented_defaults.get("output_format") {
let expected = doc_output_format.trim_matches('"');
assert_eq!(defaults.output_format.as_deref(), Some(expected),
"output_format default mismatch: code has {:?}, docs say {}",
defaults.output_format, doc_output_format);
}
if let Some(doc_packet_max_bytes) = documented_defaults.get("packet_max_bytes") {
let expected: usize = doc_packet_max_bytes.parse().expect("packet_max_bytes should be a number");
assert_eq!(defaults.packet_max_bytes, Some(expected),
"packet_max_bytes default mismatch: code has {:?}, docs say {}",
defaults.packet_max_bytes, doc_packet_max_bytes);
}
if let Some(doc_packet_max_lines) = documented_defaults.get("packet_max_lines") {
let expected: usize = doc_packet_max_lines.parse().expect("packet_max_lines should be a number");
assert_eq!(defaults.packet_max_lines, Some(expected),
"packet_max_lines default mismatch: code has {:?}, docs say {}",
defaults.packet_max_lines, doc_packet_max_lines);
}
if let Some(doc_runner_mode) = documented_defaults.get("runner_mode") {
let expected = doc_runner_mode.trim_matches('"');
assert_eq!(runner.mode.as_deref(), Some(expected),
"runner_mode default mismatch: code has {:?}, docs say {}",
runner.mode, doc_runner_mode);
}
if let Some(doc_runner_distro) = documented_defaults.get("runner_distro") {
if doc_runner_distro == "null" {
assert_eq!(runner.distro, None,
"runner_distro default mismatch: code has {:?}, docs say null",
runner.distro);
}
}
if let Some(doc_claude_path) = documented_defaults.get("claude_path") {
if doc_claude_path == "null" {
assert_eq!(runner.claude_path, None,
"claude_path default mismatch: code has {:?}, docs say null",
runner.claude_path);
}
}
let mut undocumented_fields = Vec::new();
if defaults.phase_timeout.is_some() && !documented_defaults.contains_key("phase_timeout") {
undocumented_fields.push(format!("phase_timeout (default: {:?})", defaults.phase_timeout));
}
if defaults.verbose.is_some() && !documented_defaults.contains_key("verbose") {
undocumented_fields.push(format!("verbose (default: {:?})", defaults.verbose));
}
if !undocumented_fields.is_empty() {
println!("⚠ Warning: The following fields have defaults in code but are not documented:");
for field in &undocumented_fields {
println!(" - {}", field);
}
println!(" Consider adding these to the [defaults] table in CONFIGURATION.md");
}
println!("✓ Config defaults verification passed:");
println!(" - max_turns: {:?} matches docs", defaults.max_turns);
println!(" - packet_max_bytes: {:?} matches docs", defaults.packet_max_bytes);
println!(" - packet_max_lines: {:?} matches docs", defaults.packet_max_lines);
println!(" - output_format: {:?} matches docs", defaults.output_format);
println!(" - runner_mode: {:?} matches docs", runner.mode);
}
}