use std::collections::HashSet;
use std::fs;
use std::path::Path;
fn repo_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
}
fn extract_example_names_from_md(text: &str) -> HashSet<String> {
let mut names = HashSet::new();
for line in text.lines() {
let trimmed = line.trim();
if !trimmed.starts_with('|') {
continue;
}
if let Some(start) = trimmed.find('`') {
let after_first = &trimmed[start + 1..];
if let Some(end) = after_first.find('`') {
let name = &after_first[..end];
if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !name.is_empty()
&& name.to_lowercase() != "example"
{
names.insert(name.to_string());
}
}
}
}
names
}
fn extract_example_names_from_cargo_toml(text: &str) -> HashSet<String> {
let mut names = HashSet::new();
let mut in_example_block = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "[[example]]" {
in_example_block = true;
continue;
}
if in_example_block && trimmed.starts_with('[') {
in_example_block = false;
continue;
}
if in_example_block {
if let Some(rest) = trimmed.strip_prefix("name") {
if let Some(val) = rest.split_once('"').and_then(|(_, v)| v.split_once('"')) {
names.insert(val.0.to_string());
}
}
}
}
names
}
fn example_rs_files() -> HashSet<String> {
let dir = repo_root().join("examples");
let mut files = HashSet::new();
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
files.insert(stem.to_string());
}
}
}
}
files
}
#[test]
fn capabilities_json_exists_and_is_valid() {
let path = repo_root().join("llmrust.capabilities.json");
let text = fs::read_to_string(&path).expect("llmrust.capabilities.json not found");
let _cap: serde_json::Value =
serde_json::from_str(&text).expect("llmrust.capabilities.json is not valid JSON");
}
#[test]
fn capabilities_json_has_name_and_version() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(cap["name"], "llmrust", "name must be 'llmrust'");
assert!(
!cap["version"].as_str().unwrap_or("").is_empty(),
"version is missing"
);
}
#[test]
fn capabilities_json_providers_complete() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
let providers = cap["providers"]
.as_object()
.expect("providers must be a JSON object");
let required: HashSet<&str> = [
"openai",
"deepseek",
"moonshot",
"openrouter",
"anthropic",
"google",
"ollama",
]
.into_iter()
.collect();
let present: HashSet<&str> = providers.keys().map(|k| k.as_str()).collect();
let missing: Vec<_> = required.difference(&present).copied().collect();
assert!(missing.is_empty(), "missing provider entries: {missing:?}");
let extra: Vec<_> = present.difference(&required).copied().collect();
assert!(extra.is_empty(), "unexpected provider entries: {extra:?}");
}
#[test]
fn capabilities_json_proxy_auth_config() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
let auth = &cap["proxy"]["auth"];
assert_eq!(
auth["config"].as_str().unwrap_or(""),
"LLMRUST_PROXY_KEY",
"proxy.auth.config must be 'LLMRUST_PROXY_KEY'"
);
}
#[test]
fn capabilities_json_n_policy() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
let n_policy = cap["proxy"]["n_policy"]
.as_str()
.expect("proxy.n_policy must be a string")
.to_lowercase();
let has_accepts = n_policy.contains("accepts") || n_policy.contains("accept");
let has_n1 = n_policy.contains("n = 1") || n_policy.contains("n=1");
let has_reject = n_policy.contains("n = 0")
|| n_policy.contains("n=0")
|| n_policy.contains("n > 1")
|| n_policy.contains("n>1");
assert!(
has_accepts,
"proxy.n_policy must mention accepting missing n or n=1, got: {n_policy:?}"
);
assert!(has_n1, "proxy.n_policy must mention n=1, got: {n_policy:?}");
assert!(
has_reject,
"proxy.n_policy must mention rejecting n=0 or n>1, got: {n_policy:?}"
);
}
#[test]
fn capabilities_json_description_has_disclaimer() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
let desc = cap["description"].as_str().unwrap_or("").to_lowercase();
assert!(
desc.contains("actual upstream model support may vary"),
"capabilities.json description missing disclaimer"
);
}
#[test]
fn examples_readme_lists_only_registered_examples() {
let readme_text = fs::read_to_string(repo_root().join("examples").join("README.md"))
.expect("examples/README.md not found");
let cargo_toml =
fs::read_to_string(repo_root().join("Cargo.toml")).expect("Cargo.toml not found");
let listed = extract_example_names_from_md(&readme_text);
let cargo_examples = extract_example_names_from_cargo_toml(&cargo_toml);
let rs_files = example_rs_files();
assert!(
!listed.is_empty(),
"no examples found in examples/README.md table"
);
for ex in &listed {
assert!(
cargo_examples.contains(ex),
"'{ex}' listed in examples/README.md but not in Cargo.toml [[example]]"
);
assert!(
rs_files.contains(ex),
"'{ex}' listed in examples/README.md but examples/{ex}.rs does not exist"
);
}
}
#[test]
fn proxy_server_command_includes_features_proxy() {
let readme_text = fs::read_to_string(repo_root().join("examples").join("README.md"))
.expect("examples/README.md not found");
let has_proxy_cmd = readme_text.contains("proxy_server");
if has_proxy_cmd {
let features_near =
readme_text.contains("--features proxy") && readme_text.contains("proxy_server");
assert!(
features_near,
"proxy_server in examples/README.md must include --features proxy in its command"
);
}
}
const DANGEROUS_PHRASES: &[(&str, &str)] = &[
(
"subtle crate",
"refers to nonexistent subtle crate dependency",
),
(
"Reject n != 1 (or absent)",
"incorrect n policy (missing n is accepted)",
),
(
"reject n != 1 (or absent)",
"incorrect n policy (missing n is accepted)",
),
(
"all 8 provider implementations",
"hardcodes provider count (should not hardcode)",
),
];
const AGENT_FILES: &[&str] = &[
"AGENTS.md",
"docs/CONTRACTS.md",
"docs/CAPABILITIES.md",
"examples/README.md",
"llmrust.capabilities.json",
];
#[test]
fn agent_docs_free_of_dangerous_phrases() {
for fname in AGENT_FILES {
let path = repo_root().join(fname);
if !path.exists() {
continue;
}
let text = fs::read_to_string(&path).unwrap_or_default();
let lower = text.to_lowercase();
for (phrase, explanation) in DANGEROUS_PHRASES {
assert!(
!lower.contains(&phrase.to_lowercase()),
"{fname}: contains '{phrase}' — {explanation}"
);
}
if lower.contains("cargo run --example proxy_server") {
if !lower.contains("--features proxy") {
let cmd_idx = lower.find("cargo run --example proxy_server").unwrap();
let window = &lower[cmd_idx..];
if !window.contains("--features proxy") {
panic!("{fname}: proxy_server command without --features proxy");
}
}
}
}
}
#[test]
fn capabilities_md_has_disclaimer() {
let text = fs::read_to_string(repo_root().join("docs").join("CAPABILITIES.md"))
.expect("docs/CAPABILITIES.md not found");
assert!(
text.contains("Actual upstream model support may vary"),
"docs/CAPABILITIES.md missing disclaimer: 'Actual upstream model support may vary'"
);
}
#[test]
fn capabilities_json_version_matches_crate_version() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(
cap["version"].as_str().unwrap_or(""),
env!("CARGO_PKG_VERSION"),
"llmrust.capabilities.json version must match the crate version in Cargo.toml"
);
}
#[test]
fn capabilities_json_proxy_lists_embeddings_endpoint() {
let text = fs::read_to_string(repo_root().join("llmrust.capabilities.json")).unwrap();
let cap: serde_json::Value = serde_json::from_str(&text).unwrap();
let endpoints = cap["features"]["proxy"]["endpoints"]
.as_array()
.expect("features.proxy.endpoints must be an array");
assert!(
endpoints
.iter()
.any(|e| e.as_str() == Some("POST /v1/embeddings")),
"features.proxy.endpoints must include 'POST /v1/embeddings'"
);
}