Files
yvgude--lean-ctx/rust/tests/rules_template_tool_names.rs
wehub-resource-sync 26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

113 lines
3.9 KiB
Rust

//! Parity guard: every `ctx_*` tool name shipped in an agent template must map
//! to a real registered MCP tool.
//!
//! Pi's `AGENTS.md` silently carried renamed tools (`ctx_grep`/`ctx_find`/
//! `ctx_ls` → `ctx_search`/`ctx_glob`/`ctx_tree`) because it was a static
//! template with no test tying it back to the registry. This locks every
//! shipped template to the canonical tool surface so future renames cannot
//! drift unnoticed (#548).
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use lean_ctx::server::registry::build_registry;
fn templates_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/templates")
}
/// Extract distinct `ctx_<name>` identifiers from `text`, where `<name>` is one
/// or more ASCII alphanumerics / underscores. Bare `ctx_` and globs like
/// `ctx_*` carry no tool name and are skipped.
fn ctx_tool_tokens(text: &str) -> BTreeSet<String> {
let bytes = text.as_bytes();
let mut tokens = BTreeSet::new();
let mut i = 0;
while i + 4 <= bytes.len() {
if &bytes[i..i + 4] == b"ctx_" {
let mut end = i + 4;
while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
end += 1;
}
if end > i + 4 {
tokens.insert(text[i..end].to_string());
}
i = end.max(i + 4);
} else {
i += 1;
}
}
tokens
}
/// Shipped templates the agent installers write verbatim (`.md` / `.txt`).
fn template_files() -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = std::fs::read_dir(templates_dir())
.expect("templates dir is readable")
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| matches!(p.extension().and_then(|e| e.to_str()), Some("md" | "txt")))
.collect();
files.sort();
files
}
#[test]
fn templates_only_reference_registered_ctx_tools() {
let registry = build_registry();
let mut violations: Vec<String> = Vec::new();
let files = template_files();
assert!(!files.is_empty(), "expected at least one shipped template");
for path in files {
let text = std::fs::read_to_string(&path).expect("template is readable");
for token in ctx_tool_tokens(&text) {
if !registry.contains(&token) {
violations.push(format!(
"{}: `{token}` is not a registered MCP tool",
path.file_name().and_then(|n| n.to_str()).unwrap_or("?")
));
}
}
}
assert!(
violations.is_empty(),
"shipped agent templates reference unknown/renamed ctx_* tools:\n {}",
violations.join("\n ")
);
}
#[test]
fn pi_template_tracks_canonical_search_glob_tree_tools() {
// Pi's AGENTS.md mapping table must track the canonical tool surface
// (rules_canonical::BULLETS): grep → ctx_search, find → ctx_glob,
// ls → ctx_tree. Guards both directions so the curated Pi template stays in
// parity with the single source of truth.
let pi = std::fs::read_to_string(templates_dir().join("PI_AGENTS.md"))
.expect("PI_AGENTS.md is readable");
for expected in ["ctx_search", "ctx_glob", "ctx_tree"] {
assert!(
pi.contains(expected),
"PI_AGENTS.md must reference `{expected}` (canonical tool surface)"
);
}
for renamed in ["ctx_grep", "ctx_find", "ctx_ls"] {
assert!(
!pi.contains(renamed),
"PI_AGENTS.md still references renamed tool `{renamed}`"
);
}
}
#[test]
fn token_extractor_ignores_globs_and_bare_prefix() {
assert!(ctx_tool_tokens("use ctx_* tools and ctx_ alone").is_empty());
let tokens = ctx_tool_tokens("`ctx_read`/ctx_shell, ctx_search(pattern)");
assert!(tokens.contains("ctx_read"));
assert!(tokens.contains("ctx_shell"));
assert!(tokens.contains("ctx_search"));
}