#![cfg(all(unix, feature = "workflow"))]
mod common;
use std::process::{Command, Stdio};
fn run(cfg_text: &str) -> (Option<i32>, String) {
let dir = common::unique_path("tiers", "d");
std::fs::create_dir_all(&dir).unwrap();
let cfg = format!("{dir}/c.yaml");
std::fs::write(&cfg, cfg_text.replace("__STATE__", &format!("{dir}/state"))).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(["--config", &cfg])
.stdin(Stdio::null())
.stdout(Stdio::null())
.output()
.expect("run");
let log = String::from_utf8_lossy(&out.stderr).to_string();
let _ = std::fs::remove_dir_all(&dir);
(out.status.code(), log)
}
const BASE: &str = "config_version: \"1\"\nagent: { name: t }\n\
store: { kind: file, file: { path: __STATE__ } }\n\
observability: { log_level: info, log_content: true }\n\
lifecycle: { run_until: idle, idle_grace: 2s }\n";
const TIERS: &str = "intelligence:\n endpoints: \"mock:json\"\n\
\x20 models:\n\
\x20 big: { model: big-model-1, window: 200000, fallback: small }\n\
\x20 small: { model: small-model-3, window: 128000 }\n\
\x20 default: big\n";
#[test]
fn a_step_can_name_a_cheaper_tier_than_the_instance_default() {
let (code, log) = run(&format!(
"{BASE}{TIERS}workflows:\n\
\x20 - name: w\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 t: {{ kind: think, prompt: \"classify this\", model: small, depends_on: [s] }}\n\
\x20 f: {{ kind: finish, depends_on: [t], status: completed }}\n"
));
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("\"model\":\"small-model-3\""),
"the step should have run on the tier's wire model\n{log}"
);
}
#[test]
fn every_shaping_preset_can_name_a_tier() {
let play = common::unique_path("tiers-play", "json");
std::fs::write(
&play,
r#"{"turns": [{"content": "{}"}], "match": [
{"when_contains": "Classify the input", "content": "{\"class\": \"a\", \"confidence\": 0.9}"},
{"when_contains": "Extract the structured", "content": "{\"ok\": true}"},
{"when_contains": "Summarize the input", "content": "{\"summary\": \"hi\"}"},
{"when_contains": "Judge the input", "content": "{\"verdict\": \"pass\", \"score\": 8}"},
{"when_contains": "Route the input", "content": "{\"choice\": \"a\"}"}]}"#,
)
.unwrap();
let tiers = format!(
"intelligence:\n endpoints: \"mock:file:{play}\"\n\
\x20 models:\n\
\x20 big: {{ model: big-model-1, window: 200000 }}\n\
\x20 small: {{ model: small-model-3, window: 128000 }}\n\
\x20 default: big\n"
);
let cases = [
("classify", "input: hello, classes: [a, b]"),
("extract", "input: hello, output_schema: { type: object }"),
("summarize", "input: hello"),
("judge", "input: hello, rubric: \"is it a greeting\""),
("route", "input: hello, choices: [a, b]"),
];
for (kind, fields) in cases {
let (code, log) = run(&format!(
"{BASE}{tiers}workflows:\n\
\x20 - name: w\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 t: {{ kind: {kind}, {fields}, model: small, depends_on: [s] }}\n\
\x20 f: {{ kind: finish, depends_on: [t], status: completed }}\n"
));
assert_eq!(code, Some(0), "{kind} should complete\n{log}");
assert!(
log.contains("\"model\":\"small-model-3\""),
"{kind} should have run on the tier's wire model, not the default\n{log}"
);
assert!(
!log.contains("big-model-1"),
"{kind} named a tier, so the instance default must not have been used\n{log}"
);
}
let _ = std::fs::remove_file(&play);
}
#[test]
fn an_unknown_tier_on_a_shaping_kind_is_refused_at_startup() {
let (code, log) = run(&format!(
"{BASE}{TIERS}workflows:\n\
\x20 - name: w\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 t: {{ kind: classify, input: hi, classes: [a], model: smal, depends_on: [s] }}\n\
\x20 f: {{ kind: finish, depends_on: [t], status: completed }}\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("is not a declared tier"),
"the refusal should name the typo\n{log}"
);
}
#[test]
fn the_default_tier_resolves_to_its_wire_model() {
let (code, log) = run(&format!(
"{BASE}{TIERS}workflows:\n\
\x20 - name: w\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 t: {{ kind: think, prompt: \"decide\", depends_on: [s] }}\n\
\x20 f: {{ kind: finish, depends_on: [t], status: completed }}\n"
));
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("\"model\":\"big-model-1\"") && !log.contains("\"model\":\"big\""),
"the tier NAME must not reach the provider\n{log}"
);
}
#[test]
fn an_unknown_tier_on_a_step_is_refused_at_startup() {
let (code, log) = run(&format!(
"{BASE}{TIERS}workflows:\n\
\x20 - name: w\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 t: {{ kind: think, prompt: \"x\", model: smal, depends_on: [s] }}\n\
\x20 f: {{ kind: finish, depends_on: [t], status: completed }}\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(log.contains("is not a declared tier"), "{log}");
}
#[test]
fn a_fallback_cycle_is_refused_at_startup() {
let (code, log) = run(&format!(
"{BASE}intelligence:\n endpoints: \"mock:json\"\n\
\x20 models:\n\
\x20 a: {{ model: m-a, fallback: b }}\n\
\x20 b: {{ model: m-b, fallback: a }}\n\
\x20 default: a\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(log.contains("fallback cycle"), "{log}");
}
#[test]
fn a_tier_pointing_at_a_non_intelligence_service_is_refused() {
let (code, log) = run(&format!(
"{BASE}services:\n billing: {{ kind: mcp, endpoint: \"https://b.example/mcp\" }}\n\
intelligence:\n endpoints: \"mock:json\"\n\
\x20 models:\n x: {{ model: m, service: billing }}\n default: x\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("needs `kind: intelligence`"),
"the refusal should name the mismatch\n{log}"
);
}
#[test]
fn a_declared_window_replaces_the_guess_from_the_model_name() {
let (code, log) = run(&format!(
"{BASE}intelligence:\n endpoints: \"mock:json\"\n\
\x20 models:\n only: {{ model: entirely-unrecognisable-name, window: 7777 }}\n\
\x20 default: only\n"
));
assert_eq!(code, Some(0), "{log}");
assert!(
!log.contains("config.invalid"),
"a declared window should be accepted\n{log}"
);
}
#[test]
fn an_unknown_default_tier_is_refused() {
let (code, log) = run(&format!(
"{BASE}intelligence:\n endpoints: \"mock:json\"\n\
\x20 models:\n small: {{ model: m-1 }}\n default: enormous\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(log.contains("not a declared model tier"), "{log}");
}