#![allow(clippy::expect_used)]
use std::collections::BTreeSet;
use std::path::PathBuf;
use utoipa::OpenApi;
fn docs_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../docs")
}
fn spec_routes() -> BTreeSet<(String, String)> {
let spec = serde_json::to_value(talea_server::http::openapi::ApiDoc::openapi())
.expect("openapi doc serializes");
let mut out = BTreeSet::new();
for (path, ops) in spec["paths"].as_object().expect("paths object") {
for method in ops.as_object().expect("operations object").keys() {
out.insert((method.to_uppercase(), path.clone()));
}
}
out
}
fn is_separator(cell: &str) -> bool {
let dashes = cell.trim_start_matches(':').trim_end_matches(':');
!dashes.is_empty() && dashes.chars().all(|c| c == '-')
}
fn doc_routes(doc: &str) -> BTreeSet<(String, String)> {
let after = doc
.split("drift-guard:endpoints")
.nth(1)
.expect("docs/AGENTS-INTEGRATION.md: missing drift-guard:endpoints marker");
let mut rows = BTreeSet::new();
let mut in_table = false;
for line in after.lines() {
let line = line.trim();
if !line.starts_with('|') {
if in_table {
break; }
continue; }
in_table = true;
let cells: Vec<&str> = line
.trim_matches('|')
.split('|')
.map(|c| c.trim().trim_matches('`'))
.collect();
assert!(cells.len() >= 2, "endpoint table row with <2 cells: {line}");
if cells[0] == "Method" || is_separator(cells[0]) {
continue; }
rows.insert((cells[0].to_string(), cells[1].to_string()));
}
assert!(
!rows.is_empty(),
"drift-guard endpoint table has no data rows"
);
rows
}
#[test]
fn agent_doc_endpoint_table_matches_openapi() {
let doc = std::fs::read_to_string(docs_dir().join("AGENTS-INTEGRATION.md"))
.expect("docs/AGENTS-INTEGRATION.md must exist");
let spec = spec_routes();
let table = doc_routes(&doc);
let missing: Vec<_> = spec.difference(&table).collect();
let extra: Vec<_> = table.difference(&spec).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"AGENTS-INTEGRATION.md endpoint table drifted from the OpenAPI document.\n\
missing from doc: {missing:?}\nextra in doc: {extra:?}"
);
}
fn md_link_targets(text: &str) -> Vec<String> {
const SITE: &str = "https://mooze-labs.github.io/talea/";
let mut out = Vec::new();
let mut rest = text;
while let Some(i) = rest.find("](") {
rest = &rest[i + 2..];
let Some(end) = rest.find(')') else { break };
let target = rest[..end].trim_end_matches('>').trim_start_matches('<');
rest = &rest[end..];
let target = target.split_whitespace().next().unwrap_or("");
let target = target.strip_prefix(SITE).unwrap_or(target);
if target.starts_with("http://") || target.starts_with("https://") {
continue; }
let file = target.split('#').next().unwrap_or_default();
if file.ends_with(".md") {
out.push(file.to_string());
}
}
out
}
#[test]
fn agent_doc_and_llms_txt_links_resolve() {
for name in ["AGENTS-INTEGRATION.md", "llms.txt"] {
let text = std::fs::read_to_string(docs_dir().join(name))
.unwrap_or_else(|e| panic!("docs/{name} must exist: {e}"));
let links = md_link_targets(&text);
assert!(
!links.is_empty(),
"docs/{name}: no .md links found — parser broken?"
);
for link in links {
assert!(
docs_dir().join(&link).exists(),
"docs/{name}: broken link `{link}`"
);
}
}
}