#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::{Value, json};
use speccoder_runtime::tools::WorkspaceTools;
use speccoder_runtime::tools::policy::Policy;
fn git(dir: &Path, args: &[&str]) {
let ok = Command::new("git")
.args(["-c", "maintenance.auto=false", "-c", "gc.auto=0"])
.args([
"-c",
"user.name=t",
"-c",
"user.email=t@example.com",
"-c",
"commit.gpgsign=false",
])
.args([
"-c",
"init.defaultBranch=main",
"-c",
"core.hooksPath=/dev/null",
])
.args(args)
.current_dir(dir)
.status()
.unwrap()
.success();
assert!(ok, "git {args:?}");
}
fn write(root: &Path, rel: &str, text: &str) {
let p = root.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, text).unwrap();
}
fn worktree() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let r = dir.path();
git(r, &["init", "-q"]);
write(r, ".gitignore", "/target/\n");
write(
r,
"src/lib.rs",
"pub fn a() -> u8 { 1 }\npub fn b() -> u8 { 2 }\n// TODO: c\n",
);
write(r, "src/util.rs", "pub fn helper() {}\n");
write(r, "README.md", "# demo\n");
write(r, "specs/001-x/tasks.md", "## Phase 1\n- [ ] T001 a\n");
write(r, "speccoder.toml", "[checks.t]\nargv = [\"true\"]\n");
write(r, ".env", "OPENAI_API_KEY=sk-not-real\n");
write(r, "target/debug/out.txt", "TODO: ignored build output\n");
git(r, &["add", "-A"]);
git(r, &["commit", "-q", "-m", "init"]);
dir
}
fn tools(root: &Path) -> WorkspaceTools {
let argv = |s: &str| s.split(' ').map(str::to_owned).collect::<Vec<_>>();
WorkspaceTools::new(
root,
Policy::new(
&[argv("cargo test")],
&[argv("echo"), argv("cat"), argv("sleep")],
),
)
.unwrap()
}
fn call(t: &WorkspaceTools, name: &str, args: Value) -> Value {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(t.run(name, &args))
}
fn err(v: &Value) -> &str {
assert_eq!(v["ok"], false, "expected a failure: {v}");
v["error"].as_str().unwrap()
}
#[test]
fn read_file_returns_line_ranges() {
let w = worktree();
let t = tools(w.path());
let v = call(
&t,
"read_file",
json!({"path": "src/lib.rs", "start_line": 2, "end_line": 2}),
);
assert_eq!(v["content"], "pub fn b() -> u8 { 2 }");
assert_eq!(v["total_lines"], 3);
}
#[test]
fn list_and_search_respect_gitignore_and_skip_secrets() {
let w = worktree();
let t = tools(w.path());
let files = call(&t, "list_files", json!({}));
let names: Vec<&str> = files["files"]
.as_array()
.unwrap()
.iter()
.map(|f| f.as_str().unwrap())
.collect();
assert!(names.contains(&"src/lib.rs") && names.contains(&"README.md"));
assert!(!names.iter().any(|n| n.starts_with("target/")), "{names:?}");
assert!(!names.contains(&".env"), "secrets are not listed");
assert!(!names.iter().any(|n| n.starts_with(".git/")));
let rs = call(&t, "list_files", json!({"path": "src", "glob": "**/*.rs"}));
assert_eq!(rs["files"], json!(["src/lib.rs", "src/util.rs"]));
for root in [".", "./", ""] {
let all = call(&t, "list_files", json!({"path": root, "glob": "src/*.rs"}));
assert_eq!(
all["files"],
json!(["src/lib.rs", "src/util.rs"]),
"{root:?}"
);
let found = call(&t, "search_code", json!({"pattern": "TODO", "path": root}));
assert_eq!(found["total"], 1, "{root:?}");
}
let found = call(&t, "search_code", json!({"pattern": "TODO"}));
assert_eq!(found["matches"], json!(["src/lib.rs:3: // TODO: c"]));
assert!(err(&call(&t, "search_code", json!({"pattern": "("}))).contains("invalid pattern"));
}
#[test]
fn escapes_and_secrets_are_refused_for_reading() {
let w = worktree();
let outside = tempfile::tempdir().unwrap();
write(outside.path(), "secret.txt", "outside");
#[cfg(unix)]
std::os::unix::fs::symlink(outside.path().join("secret.txt"), w.path().join("link.txt"))
.unwrap();
let t = tools(w.path());
assert!(err(&call(&t, "read_file", json!({"path": "../x"}))).contains(".."));
assert!(err(&call(&t, "read_file", json!({"path": ".env"}))).contains("credential"));
#[cfg(unix)]
assert!(
err(&call(&t, "read_file", json!({"path": "link.txt"}))).contains("outside the workspace")
);
}
#[cfg(unix)]
#[test]
fn a_write_through_a_symlinked_directory_is_judged_on_the_file_it_reaches() {
let w = worktree();
std::os::unix::fs::symlink(w.path().join("specs/001-x"), w.path().join("notes")).unwrap();
let t = tools(w.path());
let out = call(
&t,
"write_file",
json!({"path": "notes/tasks.md", "content": "- [X] T001 a\n"}),
);
let refused = err(&out);
assert!(refused.contains("protected"), "{refused}");
assert!(
fs::read_to_string(w.path().join("specs/001-x/tasks.md"))
.unwrap()
.contains("- [ ] T001")
);
let resolved = call(
&t,
"resolve",
json!({"tool": "write_file", "arguments": {"path": "notes/tasks.md"}}),
);
assert_eq!(
resolved["object"]["canonical"], "specs/001-x/tasks.md",
"{resolved}"
);
assert_eq!(resolved["object"]["asked"], "notes/tasks.md");
let cmd = call(
&t,
"resolve",
json!({"tool": "run_command", "arguments": {"argv": ["true", "x"], "cwd": "src"}}),
);
assert_eq!(cmd["object"]["cwd"], "src", "{cmd}");
let on_path = std::env::split_paths(&std::env::var_os("PATH").unwrap())
.map(|d| d.join("true"))
.find(|p| p.is_file())
.unwrap();
assert_eq!(
cmd["object"]["executable"].as_str().map(PathBuf::from),
Some(fs::canonicalize(on_path).unwrap()),
"{cmd}"
);
let none = call(
&t,
"resolve",
json!({"tool": "read_file", "arguments": {"path": "x"}}),
);
assert_eq!(none, json!({"ok": true, "object": null}));
}
#[test]
fn a_write_in_another_letter_case_is_judged_on_the_protected_file() {
let w = worktree();
write(w.path(), "Makefile", "all:\n");
let t = tools(w.path());
for path in [
"Speccoder.toml",
"makefile",
".CURSOR/rules.json",
".CLAUDE/settings.json",
"Specs/001-x/tasks.md",
] {
let out = call(&t, "write_file", json!({"path": path, "content": "x"}));
assert!(err(&out).contains("protected"), "{path}: {out}");
}
assert_eq!(
fs::read_to_string(w.path().join("speccoder.toml")).unwrap(),
"[checks.t]\nargv = [\"true\"]\n"
);
assert_eq!(
fs::read_to_string(w.path().join("Makefile")).unwrap(),
"all:\n"
);
}
#[cfg(unix)]
#[test]
fn a_write_that_reaches_another_file_is_refused_by_name() {
let w = worktree();
write(w.path(), "tests/existing.rs", "#[test]\nfn a() {}\n");
std::os::unix::fs::symlink(w.path().join("tests"), w.path().join("alias")).unwrap();
let t = tools(w.path());
let out = call(
&t,
"write_file",
json!({"path": "alias/existing.rs", "content": "#[test]\nfn a() {}\n// x\n"}),
);
assert!(err(&out).contains("reaches `tests/existing.rs`"), "{out}");
assert!(
!fs::read_to_string(w.path().join("tests/existing.rs"))
.unwrap()
.contains("// x")
);
let ok = call(
&t,
"write_file",
json!({"path": "tests/existing.rs", "content": "#[test]\nfn a() {}\n// x\n"}),
);
assert_eq!(ok["ok"], true, "{ok}");
}
#[test]
fn every_optional_argument_accepts_null() {
let w = worktree();
let t = tools(w.path());
for spec in speccoder_runtime::tools::catalog() {
let schema = (spec.parameters)();
let props = schema["properties"]
.as_object()
.cloned()
.unwrap_or_default();
let required: Vec<&str> = schema["required"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
for (name, prop) in &props {
let nullable = prop["type"]
.as_array()
.is_some_and(|ts| ts.iter().any(|t| t == "null"))
|| prop["anyOf"]
.as_array()
.is_some_and(|bs| bs.iter().any(|b| b["type"] == "null"));
assert!(
required.contains(&name.as_str()),
"{}: `{name}` is not required: {schema}",
spec.name
);
if nullable {
let mut args = json!({});
for (other, p) in &props {
if other == name {
args[other] = Value::Null;
} else {
args[other] = match p["type"].as_str() {
Some("string") => json!("src/lib.rs"),
Some("array") => json!(["echo", "x"]),
_ => Value::Null,
};
}
}
let v = call(&t, spec.name, args.clone());
let error = v["error"].as_str().unwrap_or_default();
assert!(
!error.contains("invalid") && !error.contains("expected"),
"{} with {args}: {v}",
spec.name
);
}
}
}
}
#[test]
fn edit_file_reads_a_null_replace_all_as_one_occurrence() {
let w = worktree();
let t = tools(w.path());
let v = call(
&t,
"edit_file",
json!({"path": "src/lib.rs", "old_string": "{ 1 }", "new_string": "{ 10 }", "replace_all": null}),
);
assert_eq!(
(v["ok"].clone(), v["replacements"].clone()),
(json!(true), json!(1)),
"{v}"
);
}
#[test]
fn closing_a_foreign_schema_only_narrows_it() {
let close_objects = |v: serde_json::Value| agentplane::tools::closed_schema(&v);
let required_only = close_objects(
json!({"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}),
);
assert_eq!(required_only["additionalProperties"], false);
assert!(agentplane::model::strict_schema_problem(&required_only).is_none());
let free = json!({"type": "object"});
assert_eq!(close_objects(free.clone()), free);
let empty = close_objects(json!({"type": "object", "additionalProperties": false}));
assert_eq!(
(empty["properties"].clone(), empty["required"].clone()),
(json!({}), json!([]))
);
assert!(agentplane::model::strict_schema_problem(&empty).is_none());
let optional =
close_objects(json!({"type": "object", "properties": {"limit": {"type": "integer"}}}));
assert!(optional["properties"]["limit"]["type"] == "integer");
assert!(agentplane::model::strict_schema_problem(&optional).is_some());
let composed = json!({"type": "object", "allOf": [{"properties": {"a": {"type": "string"}}}]});
assert_eq!(close_objects(composed.clone()), composed);
}
#[test]
fn edit_file_replaces_one_unique_occurrence_and_reports_hashes() {
let w = worktree();
let t = tools(w.path());
let v = call(
&t,
"edit_file",
json!({"path": "src/lib.rs", "old_string": "{ 1 }", "new_string": "{ 10 }"}),
);
assert_eq!(v["ok"], true, "{v}");
assert_ne!(v["before"], v["after"]);
assert!(
fs::read_to_string(w.path().join("src/lib.rs"))
.unwrap()
.contains("{ 10 }")
);
let before = fs::read(w.path().join("src/lib.rs")).unwrap();
assert!(
err(&call(
&t,
"edit_file",
json!({"path": "src/lib.rs", "old_string": "pub fn", "new_string": "fn"})
))
.contains("occurs 2 times")
);
assert!(
err(&call(
&t,
"edit_file",
json!({"path": "src/lib.rs", "old_string": "nope", "new_string": "x"})
))
.contains("not found")
);
assert_eq!(
before,
fs::read(w.path().join("src/lib.rs")).unwrap(),
"failed edits change nothing"
);
let all = call(
&t,
"edit_file",
json!({"path": "src/lib.rs", "old_string": "pub fn", "new_string": "pub(crate) fn", "replace_all": true}),
);
assert_eq!(all["replacements"], 2);
}
#[test]
fn writes_that_remove_a_test_are_refused_before_they_happen() {
let w = worktree();
let t = tools(w.path());
let lib =
"pub fn greet() {}\n\n#[cfg(test)]\nmod tests {\n #[test]\n fn it_works() {}\n}\n";
write(w.path(), "src/greet.rs", lib);
let edit = call(
&t,
"edit_file",
json!({"path": "src/greet.rs", "old_string": " #[test]\n fn it_works() {}\n", "new_string": ""}),
);
assert!(err(&edit).contains("removes the test `it_works`"), "{edit}");
let rewrite = call(
&t,
"write_file",
json!({"path": "src/greet.rs", "content": "pub fn greet() {}\n"}),
);
assert!(
err(&rewrite).contains("report the task blocked"),
"{rewrite}"
);
assert_eq!(
fs::read_to_string(w.path().join("src/greet.rs")).unwrap(),
lib
);
let added = call(
&t,
"edit_file",
json!({"path": "src/greet.rs", "old_string": " fn it_works() {}\n", "new_string": " fn it_works() {}\n\n #[test]\n fn greets() {}\n"}),
);
assert_eq!(added["ok"], true, "{added}");
}
#[test]
fn write_file_creates_nested_files_with_absent_before_hash() {
let w = worktree();
let t = tools(w.path());
let v = call(
&t,
"write_file",
json!({"path": "src/models/user.rs", "content": "pub struct User;\n"}),
);
assert_eq!(v["before"], "absent");
assert_eq!(
fs::read_to_string(w.path().join("src/models/user.rs")).unwrap(),
"pub struct User;\n"
);
}
#[test]
fn every_protected_class_is_refused_for_writing() {
let w = worktree();
let t = tools(w.path());
for path in [
"specs/001-x/tasks.md",
".specify/memory/constitution.md",
"speccoder.toml",
".github/workflows/ci.yml",
".git/config",
".env",
"keys/deploy.pem",
] {
let v = call(&t, "write_file", json!({"path": path, "content": "x"}));
assert!(err(&v).starts_with("denied"), "{path}: {v}");
}
assert_eq!(
fs::read_to_string(w.path().join("specs/001-x/tasks.md")).unwrap(),
"## Phase 1\n- [ ] T001 a\n"
);
}
#[test]
fn run_command_executes_allowed_argv_with_closed_stdin() {
let w = worktree();
let t = tools(w.path());
let v = call(&t, "run_command", json!({"argv": ["echo", "hello"]}));
assert_eq!(v["ok"], true, "{v}");
assert_eq!(v["stdout"], "hello\n");
let cat = call(
&t,
"run_command",
json!({"argv": ["cat"], "timeout_secs": 5}),
);
assert_eq!(cat["exit_code"], 0, "stdin is closed, so cat ends at once");
let slow = call(
&t,
"run_command",
json!({"argv": ["sleep", "5"], "timeout_secs": 1}),
);
assert_eq!(slow["timed_out"], true);
for cwd in [".", "./", ""] {
let here = call(
&t,
"run_command",
json!({"argv": ["echo", "x"], "cwd": cwd}),
);
assert_eq!(here["ok"], true, "{cwd:?}: {here}");
}
}
#[cfg(unix)]
#[test]
fn a_speckit_command_gets_specify_s_python_unless_one_is_set() {
use std::os::unix::fs::PermissionsExt as _;
let w = worktree();
let fake = tempfile::tempdir().unwrap();
let fake = fs::canonicalize(fake.path()).unwrap();
let python = fake.join("venv/python");
write(
&fake,
"venv/python",
"#!/bin/sh\ncase \"$2\" in *'import sys, yaml'*) [ -f \"$(dirname \"$0\")/yaml.py\" ] && exit 0;; esac\nexit 1\n",
);
write(&fake, "venv/yaml.py", "");
write(
&fake,
"bin/specify",
&format!("#!{}\nprint('specify')\n", python.display()),
);
for f in [&python, &fake.join("bin/specify")] {
fs::set_permissions(f, fs::Permissions::from_mode(0o755)).unwrap();
}
let path = format!("{}:/usr/bin:/bin", fake.join("bin").display());
let tools = |extra: Vec<(String, String)>| {
let mut env = vec![("PATH".to_owned(), path.clone())];
env.extend(extra);
WorkspaceTools::new(
w.path(),
Policy::new(&[], &[vec!["sh".to_owned()]])
.rules(&speccoder_runtime::format::speckit::RULES),
)
.unwrap()
.with_env(env)
};
let echo = json!({"argv": ["sh", "-c", "printf %s \"$SPECKIT_PYTHON_EXECUTABLE\""]});
let v = call(&tools(Vec::new()), "run_command", echo.clone());
assert_eq!(v["stdout"], python.display().to_string(), "{v}");
let mine = vec![(
"SPECKIT_PYTHON_EXECUTABLE".to_owned(),
"/mine/python3".to_owned(),
)];
let v = call(&tools(mine), "run_command", echo);
assert_eq!(v["stdout"], "/mine/python3", "{v}");
}
#[test]
fn denied_commands_never_run() {
let w = worktree();
let t = tools(w.path());
for argv in [
json!(["git", "push"]),
json!(["git", "reset", "--hard"]),
json!(["sudo", "true"]),
] {
let v = call(&t, "run_command", json!({ "argv": argv }));
assert!(err(&v).starts_with("denied"), "{v}");
}
}
#[test]
fn git_status_and_diff_show_edits() {
let w = worktree();
let t = tools(w.path());
call(
&t,
"edit_file",
json!({"path": "README.md", "old_string": "# demo", "new_string": "# demo 2"}),
);
call(
&t,
"write_file",
json!({"path": "NOTES.md", "content": "n\n"}),
);
let status = call(&t, "git_status", json!({}));
let entries = status["entries"].as_array().unwrap();
assert!(entries.iter().any(|e| e == " M README.md"), "{status}");
assert!(entries.iter().any(|e| e == "?? NOTES.md"), "{status}");
let diff = call(&t, "git_diff", json!({"path": "README.md"}));
assert!(diff["diff"].as_str().unwrap().contains("+# demo 2"));
}
#[test]
fn invalid_arguments_are_reported_not_panicked() {
let w = worktree();
let t = tools(w.path());
assert!(err(&call(&t, "read_file", json!({"file": "x"}))).contains("invalid arguments"));
assert!(err(&call(&t, "nope", json!({}))).contains("unknown tool"));
}
#[test]
fn declarations_cover_every_tool_with_a_schema() {
let catalog = speccoder_runtime::tools::catalog();
let names: Vec<&str> = catalog.iter().map(|t| t.name).collect();
assert_eq!(
names,
[
"read_file",
"list_files",
"search_code",
"write_file",
"edit_file",
"run_command",
"git_status",
"git_diff"
]
);
for t in &catalog {
let schema = (t.parameters)();
assert_eq!(schema["type"], "object", "{}: {schema}", t.name);
assert_eq!(
agentplane::model::strict_schema_problem(&schema),
None,
"{} is not strict-compatible: {schema}",
t.name
);
assert_eq!(
t.mutates,
!t.model_fields.is_empty(),
"{}: mutating tools declare model-authored fields",
t.name
);
}
assert_eq!(
speccoder_runtime::tools::declarations().len(),
catalog.len()
);
}
#[test]
fn every_tool_and_answer_schema_is_strict_for_both_providers() {
use agentplane::model::StrictSubset;
use speccoder_runtime::substrate::{acceptance, attempt, converge, host, monitor, propose};
let mut schemas: Vec<(String, serde_json::Value)> = speccoder_runtime::tools::catalog()
.iter()
.map(|t| (t.name.to_owned(), (t.parameters)()))
.collect();
schemas.extend([
("attempt report".to_owned(), attempt::report_schema()),
("propose".to_owned(), propose::schema()),
("acceptance".to_owned(), acceptance::schema()),
("converge".to_owned(), converge::schema()),
("monitor".to_owned(), monitor::schema()),
("host".to_owned(), host::schema()),
]);
for (name, schema) in &schemas {
for subset in [StrictSubset::OpenAi, StrictSubset::Anthropic] {
assert_eq!(
subset.problem(schema),
None,
"{name} under {subset:?}: {schema}"
);
}
}
}
#[test]
fn the_model_writes_no_review_record_and_nothing_of_the_state_directory() {
let p = Policy::new(&[], &[]);
let review = p.classify(
"write_file",
&json!({ "path": "specs/001-x/review.json", "content": "{\"oracle\":{}}" }),
None,
);
assert!(
!matches!(review, speccoder_runtime::tools::policy::Decision::Allow),
"{review:?}"
);
let state = tempfile::tempdir().unwrap();
for outside in [
format!("{}/oracle/001-x/US1/draft.json", state.path().display()),
format!(
"{}/pre-frozen/001-x/US1/record.json",
state.path().display()
),
format!("{}/comments/001-x.json", state.path().display()),
"../state/held-out/001-x/req-US1/manifest.json".to_owned(),
] {
assert!(
speccoder_runtime::tools::paths::normalise(&outside).is_err(),
"{outside} is reachable"
);
}
let wt = worktree();
let t = tools(wt.path());
let v = call(
&t,
"write_file",
json!({ "path": format!("{}/comments/x.json", state.path().display()), "content": "{}" }),
);
assert_eq!(v["ok"], false, "{v}");
assert!(!state.path().join("comments/x.json").exists());
}
#[test]
fn a_copied_secret_value_never_reaches_the_model_in_any_form() {
use base64::Engine as _;
let w = worktree();
let value = "tools-mask-Tz42 secret";
speccoder_runtime::secrets::register("DB_PASSWORD", value);
let b64 = base64::engine::general_purpose::STANDARD.encode(value);
write(
w.path(),
"notes.txt",
&format!("plain {value}\nencoded {b64}\n"),
);
let t = tools(w.path());
for v in [
call(&t, "run_command", json!({"argv": ["cat", "notes.txt"]})),
call(&t, "read_file", json!({"path": "notes.txt"})),
call(&t, "search_code", json!({"pattern": "Tz42"})),
] {
let shown = v.to_string();
assert!(!shown.contains(value) && !shown.contains(&b64), "{shown}");
}
let v = call(&t, "run_command", json!({"argv": ["cat", "notes.txt"]}));
assert_eq!(
v["stdout"],
"plain [redacted:DB_PASSWORD]\nencoded [redacted:DB_PASSWORD]\n"
);
}
#[test]
fn a_copied_value_at_a_cut_is_masked_before_it() {
let w = worktree();
let value = "cut-straddle-Hk2m value";
speccoder_runtime::secrets::register("CUT_SECRET", value);
let pad = "x".repeat(speccoder_runtime::tools::READ_CAP - 5);
write(w.path(), "big.txt", &format!("{pad}{value}\n"));
let line = "y".repeat(speccoder_runtime::tools::LINE_CAP - 5);
write(w.path(), "line.txt", &format!("{line}{value}\n"));
let t = tools(w.path());
for v in [
call(&t, "read_file", json!({"path": "big.txt"})),
call(&t, "search_code", json!({"pattern": "yyyy"})),
] {
assert!(
!v.to_string().contains("cut-s"),
"a prefix shown: {}",
&v.to_string()[v.to_string().len().saturating_sub(300)..]
);
}
}
#[test]
fn every_spelling_the_policy_allows_the_tool_carries_out() {
use speccoder_runtime::tools::policy::Decision;
let w = worktree();
let t = tools(w.path());
let p = Policy::new(&[], &[vec!["echo".to_owned()]]);
let dirs = [
None,
Some(""),
Some("."),
Some("./"),
Some("src"),
Some("src/"),
Some("./src"),
Some("./src/"),
];
let mut calls = Vec::new();
for d in dirs {
let with = |mut args: Value, key: &str| {
if let Some(d) = d {
args[key] = json!(d);
}
args
};
calls.push(("list_files", with(json!({}), "path")));
calls.push(("search_code", with(json!({"pattern": "fn"}), "path")));
calls.push(("run_command", with(json!({"argv": ["echo", "hi"]}), "cwd")));
}
for f in ["src/lib.rs", "./src/lib.rs", "src//lib.rs", "src/./lib.rs"] {
calls.push(("read_file", json!({"path": f})));
}
let mut checked = 0;
for (tool, args) in calls {
if !matches!(p.classify(tool, &args, None), Decision::Deny(_)) {
let out = call(&t, tool, args.clone());
assert_eq!(out["ok"], true, "{tool} {args}: allowed but refused: {out}");
checked += 1;
}
}
assert!(
checked >= 25,
"the policy allowed only {checked} of the calls"
);
}