use std::path::{Path, PathBuf};
use super::Limits;
pub const GLOBAL_LIMITS_DIR: &str = "limits";
pub const GLOBAL_LIMITS_FILE: &str = "global.yaml";
pub const LEGACY_CONFIG_FILE: &str = "config.yaml";
pub fn global_limits_dir(mur_home: &Path) -> PathBuf {
mur_home.join(GLOBAL_LIMITS_DIR)
}
pub fn global_limits_path(mur_home: &Path) -> PathBuf {
global_limits_dir(mur_home).join(GLOBAL_LIMITS_FILE)
}
pub fn legacy_config_path(mur_home: &Path) -> PathBuf {
mur_home.join(LEGACY_CONFIG_FILE)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlobalSource {
File,
Legacy,
}
impl GlobalSource {
pub fn label(self) -> &'static str {
match self {
GlobalSource::File => "limits/global.yaml",
GlobalSource::Legacy => "config.yaml limits (legacy)",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GlobalLimits {
pub limits: Limits,
pub source: GlobalSource,
}
pub fn read_global_file(path: &Path) -> Result<Option<Limits>, String> {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("{}: {e}", path.display())),
};
if text.trim().is_empty() {
return Ok(Some(Limits::default()));
}
let parsed: Option<Limits> =
serde_yaml_ng::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(Some(parsed.unwrap_or_default()))
}
pub fn load_global(mur_home: &Path) -> Result<GlobalLimits, String> {
load_with(mur_home, |p| {
Ok(crate::config::Config::load_or_default(p).limits)
})
}
pub fn load_global_strict(mur_home: &Path) -> Result<GlobalLimits, String> {
load_with(mur_home, |p| {
crate::config::Config::try_load(p).map(|c| c.limits)
})
}
fn load_with(
mur_home: &Path,
legacy: impl FnOnce(&Path) -> Result<Limits, String>,
) -> Result<GlobalLimits, String> {
if let Some(limits) = read_global_file(&global_limits_path(mur_home))? {
return Ok(GlobalLimits {
limits,
source: GlobalSource::File,
});
}
Ok(GlobalLimits {
limits: legacy(&legacy_config_path(mur_home))?,
source: GlobalSource::Legacy,
})
}
pub fn legacy_block_present(mur_home: &Path) -> bool {
!crate::config::Config::load_or_default(&legacy_config_path(mur_home))
.limits
.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
fn home() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn write_new(h: &Path, text: &str) {
std::fs::create_dir_all(global_limits_dir(h)).unwrap();
std::fs::write(global_limits_path(h), text).unwrap();
}
fn write_legacy(h: &Path, text: &str) {
std::fs::write(legacy_config_path(h), text).unwrap();
}
#[test]
fn nothing_on_disk_is_an_empty_legacy_scope() {
let h = home();
let g = load_global(h.path()).unwrap();
assert_eq!(g.source, GlobalSource::Legacy);
assert!(g.limits.is_empty());
}
#[test]
fn new_file_only() {
let h = home();
write_new(h.path(), "deadline: 2h\nstuck: off\n");
let g = load_global_strict(h.path()).unwrap();
assert_eq!(g.source, GlobalSource::File);
assert_eq!(g.limits.deadline.as_deref(), Some("2h"));
assert_eq!(g.limits.stuck.as_deref(), Some("off"));
}
#[test]
fn legacy_only_falls_back_to_config_yaml() {
let h = home();
write_legacy(h.path(), "limits:\n cost_usd: 3\n");
let g = load_global(h.path()).unwrap();
assert_eq!(g.source, GlobalSource::Legacy);
assert_eq!(g.limits.cost_usd, Some(3.0));
assert!(legacy_block_present(h.path()));
}
#[test]
fn both_present_the_new_file_wins_outright() {
let h = home();
write_legacy(h.path(), "limits:\n deadline: 9h\n cost_usd: 3\n");
write_new(h.path(), "deadline: 1h\n");
let g = load_global(h.path()).unwrap();
assert_eq!(g.source, GlobalSource::File);
assert_eq!(g.limits.deadline.as_deref(), Some("1h"));
assert_eq!(g.limits.cost_usd, None);
assert!(legacy_block_present(h.path()));
}
#[test]
fn an_empty_new_file_still_shadows_the_legacy_block() {
let h = home();
write_legacy(h.path(), "limits:\n deadline: 9h\n");
write_new(h.path(), "");
let g = load_global(h.path()).unwrap();
assert_eq!(g.source, GlobalSource::File);
assert!(g.limits.is_empty());
}
#[test]
fn a_broken_new_file_is_an_error_not_the_defaults() {
let h = home();
write_legacy(h.path(), "limits:\n deadline: 9h\n");
write_new(h.path(), "deadline: [unclosed\n");
assert!(load_global(h.path()).is_err());
write_new(h.path(), "max_iterations: 3\n");
let e = load_global_strict(h.path()).unwrap_err();
assert!(e.contains("max_iterations"), "{e}");
}
#[test]
fn legacy_strictness_differs_between_the_two_loaders() {
let h = home();
write_legacy(h.path(), "limits: [unclosed\n");
assert!(load_global(h.path()).unwrap().limits.is_empty());
assert!(load_global_strict(h.path()).is_err());
}
}