#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Site {
Sandbox,
ToolRouting,
Region,
}
impl Site {
fn shapes(self) -> &'static [&'static [&'static str]] {
match self {
Site::Sandbox => &[&["sandbox"], &["stages", "*", "sandbox"]],
Site::ToolRouting => &[&["stages", "*", "tool_routing"]],
Site::Region => &[
&["context", "regions", "*"],
&["stages", "*", "context", "regions", "*"],
],
}
}
pub fn holds(self, path: &[&str]) -> bool {
self.shapes().iter().any(|shape| {
shape.len() == path.len()
&& shape
.iter()
.zip(path)
.all(|(want, got)| *want == "*" || want == got)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenamedKey {
pub section: &'static str,
pub site: Site,
pub old: &'static str,
pub new: &'static str,
pub note: &'static str,
}
pub const RENAMED_KEYS: &[RenamedKey] = &[KEEP_RESULTS, KEEP_WARM, PINNED];
pub const KEEP_RESULTS: RenamedKey = RenamedKey {
section: "[stages.<stage>.tool_routing]",
site: Site::ToolRouting,
old: "persist",
new: "keep_results",
note: "It decides whether a tool's result stays in the region it was routed to, or goes to \
`scratch` instead. It never had anything to do with surviving a stage change, which is \
what `persist` reads as.",
};
pub const KEEP_WARM: RenamedKey = RenamedKey {
section: "[sandbox]",
site: Site::Sandbox,
old: "persist",
new: "keep_warm",
note: "It keeps one container warm across the run's stages rather than building one per call. \
The container is still torn down when the run ends, so `persist` promised a lifetime it \
never gave.",
};
pub const PINNED: RenamedKey = RenamedKey {
section: "a region with `kind = \"custom\"`",
site: Site::Region,
old: "persistent",
new: "pinned",
note: "It makes a custom region behave like a pinned one: never evicted, immune to a `clear` \
transform, counted as fixed budget. It says nothing about later runs, which is what \
`persistent` suggests.",
};
pub fn current_names<'a>(allowed: &[&'a str]) -> Vec<&'a str> {
allowed
.iter()
.copied()
.filter(|name| {
!RENAMED_KEYS
.iter()
.any(|key| key.old == *name && allowed.contains(&key.new))
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Found {
pub key: &'static RenamedKey,
pub path: String,
pub value: String,
pub superseded: bool,
}
pub fn legacy_keys_in(document: &toml::value::Table) -> Vec<Found> {
let mut found = Vec::new();
walk(document, &mut Vec::new(), &mut found);
found
}
fn walk<'a>(table: &'a toml::value::Table, path: &mut Vec<&'a str>, found: &mut Vec<Found>) {
for key in RENAMED_KEYS {
if !key.site.holds(path) {
continue;
}
let Some(value) = table.get(key.old) else {
continue;
};
found.push(Found {
key,
path: path.join("."),
value: value.to_string(),
superseded: table.contains_key(key.new),
});
}
for (name, value) in table {
let Some(child) = value.as_table() else {
continue;
};
path.push(name);
walk(child, path, found);
path.pop();
}
}
#[cfg(test)]
mod tests {
use super::{
KEEP_RESULTS, KEEP_WARM, PINNED, RENAMED_KEYS, Site, current_names, legacy_keys_in,
};
const EVERYWHERE: &str = r#"
[agent]
name = "a"
persist = "not a setting of ours"
[sandbox]
persist = true
[context.regions]
brain = { kind = "custom", script = "c.rhai", persistent = true }
[stages.plan]
model = "m"
[stages.plan.sandbox]
persist = false
[stages.plan.tool_routing]
persist = false
[stages.plan.context.regions]
notes = { kind = "custom", script = "n.rhai", persistent = false }
"#;
fn parse(text: &str) -> toml::value::Table {
toml::from_str(text).expect("the fixture is TOML")
}
#[test]
fn the_table_holds_no_chains() {
for key in RENAMED_KEYS {
assert!(
!RENAMED_KEYS
.iter()
.any(|other| other.section == key.section && other.old == key.new),
"{} is both a new name and an old one in {}",
key.new,
key.section
);
assert_ne!(key.old, key.new, "{} renames to itself", key.old);
assert!(!key.note.is_empty(), "{} has no note", key.old);
}
}
#[test]
fn every_site_is_found_at_both_levels_and_nothing_else_is() {
let found = legacy_keys_in(&parse(EVERYWHERE));
let seen: Vec<(&str, &str, &str)> = found
.iter()
.map(|f| (f.path.as_str(), f.key.old, f.key.new))
.collect();
assert_eq!(
seen,
vec![
("sandbox", "persist", "keep_warm"),
("context.regions.brain", "persistent", "pinned"),
("stages.plan.sandbox", "persist", "keep_warm"),
("stages.plan.tool_routing", "persist", "keep_results"),
("stages.plan.context.regions.notes", "persistent", "pinned"),
]
);
assert_eq!(found[0].value, "true");
assert!(!found[0].superseded);
}
#[test]
fn a_key_written_twice_is_reported_as_superseded() {
let found = legacy_keys_in(&parse("[sandbox]\npersist = true\nkeep_warm = false\n"));
assert_eq!(found.len(), 1);
assert!(found[0].superseded);
}
#[test]
fn a_blueprint_that_does_not_use_them_reports_nothing() {
assert!(legacy_keys_in(&parse("[sandbox]\nkeep_warm = true\n")).is_empty());
assert!(legacy_keys_in(&parse("[context]\nregions = \"x\"\n")).is_empty());
assert!(legacy_keys_in(&parse("[regions.brain]\npersistent = true\n")).is_empty());
}
#[test]
fn a_site_matches_the_shape_of_the_path() {
assert!(Site::Sandbox.holds(&["sandbox"]));
assert!(Site::Sandbox.holds(&["stages", "plan", "sandbox"]));
assert!(!Site::Sandbox.holds(&["agent", "sandbox"]));
assert!(!Site::Sandbox.holds(&["stages", "plan", "sandbox", "limits"]));
assert!(Site::ToolRouting.holds(&["stages", "p", "tool_routing"]));
assert!(!Site::ToolRouting.holds(&["tool_routing"]));
assert!(Site::Region.holds(&["context", "regions", "brain"]));
assert!(!Site::Region.holds(&["context", "regions"]));
}
#[test]
fn an_old_spelling_is_accepted_but_never_offered() {
assert_eq!(
current_names(&["default_region", "keep_results", "persist"]),
vec!["default_region", "keep_results"]
);
assert_eq!(current_names(&["kind", "image"]), vec!["kind", "image"]);
assert_eq!(current_names(&["persist"]), vec!["persist"]);
}
#[test]
fn each_entry_is_the_one_its_site_names() {
assert_eq!(KEEP_RESULTS.site, Site::ToolRouting);
assert_eq!(KEEP_WARM.site, Site::Sandbox);
assert_eq!(PINNED.site, Site::Region);
}
}