use std::collections::BTreeSet;
use std::sync::{Mutex, OnceLock};
pub const KNOWN_VIOLATIONS: &[(&str, &str)] = &[];
static VIOLATIONS: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
fn violations() -> &'static Mutex<Vec<String>> {
VIOLATIONS.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn observed_violations() -> Vec<String> {
violations().lock().expect("violations lock").clone()
}
pub fn clear_violations() {
violations().lock().expect("violations lock").clear();
}
pub fn observe(status: axum::http::StatusCode, body: &[u8]) -> Option<Vec<u8>> {
let task = successful_task(status, body)?;
record_observed(&task);
let msg = check(status, body)?;
eprintln!("RESPONSE-CONFORMANCE VIOLATION {msg}");
violations()
.lock()
.expect("violations lock")
.push(msg.clone());
Some(
serde_json::json!({
"error": "responseSchemaViolation",
"message": msg,
})
.to_string()
.into_bytes(),
)
}
fn successful_task(status: axum::http::StatusCode, body: &[u8]) -> Option<String> {
if !status.is_success() || body.is_empty() {
return None;
}
let doc = serde_json::from_slice::<serde_json::Value>(body).ok()?;
Some(doc.get("type")?.as_str()?.to_owned())
}
static OBSERVED: OnceLock<Mutex<BTreeSet<String>>> = OnceLock::new();
fn observed() -> &'static Mutex<BTreeSet<String>> {
OBSERVED.get_or_init(|| Mutex::new(BTreeSet::new()))
}
pub fn observed_tasks() -> Vec<String> {
observed()
.lock()
.expect("observed lock")
.iter()
.cloned()
.collect()
}
fn record_observed(task: &str) {
let first_time = observed()
.lock()
.expect("observed lock")
.insert(task.to_owned());
let Ok(dir) = std::env::var("TRUST_TASK_OBSERVED_DIR") else {
return;
};
if !first_time && already_written(task) {
return;
}
use std::io::Write;
let binary = std::env::args()
.next()
.and_then(|a| {
std::path::Path::new(&a)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
})
.unwrap_or_else(|| "unknown".to_owned());
let path = std::path::Path::new(&dir).join(format!("{binary}.{}.tasks", std::process::id()));
let line = format!("{task}\n");
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
Ok(mut f) => {
if f.write_all(line.as_bytes()).is_ok() {
written()
.lock()
.expect("written lock")
.insert(task.to_owned());
}
}
Err(e) => eprintln!("TRUST-TASK COVERAGE: cannot write {}: {e}", path.display()),
}
}
static WRITTEN: OnceLock<Mutex<BTreeSet<String>>> = OnceLock::new();
fn written() -> &'static Mutex<BTreeSet<String>> {
WRITTEN.get_or_init(|| Mutex::new(BTreeSet::new()))
}
fn already_written(task: &str) -> bool {
written().lock().expect("written lock").contains(task)
}
fn check(status: axum::http::StatusCode, body: &[u8]) -> Option<String> {
if !status.is_success() || body.is_empty() {
return None;
}
let doc = serde_json::from_slice::<serde_json::Value>(body).ok()?;
let ty = doc.get("type")?.as_str()?;
let schema = trust_tasks_rs::schema_index::schema_for(ty)?;
let payload = doc.get("payload")?;
let Err(e) = trust_tasks_rs::validate::against_schema(schema, payload) else {
return None;
};
Some(format!("{ty}: {e}"))
}
pub fn checkable_tasks() -> Vec<&'static str> {
crate::trust_tasks::dispatched_uris()
.into_iter()
.filter(|u| trust_tasks_rs::schema_index::schema_for(&format!("{u}#response")).is_some())
.collect()
}
#[test]
#[ignore = "needs a suite run first; driven by scripts/trust-task-coverage.sh"]
fn report_task_coverage() {
let dir = std::env::var("TRUST_TASK_OBSERVED_DIR")
.expect("set TRUST_TASK_OBSERVED_DIR, or run scripts/trust-task-coverage.sh");
let entries = std::fs::read_dir(&dir).expect("the observed directory must exist");
let mut files = 0usize;
let mut seen: BTreeSet<String> = BTreeSet::new();
for e in entries.flatten() {
if e.path().extension().is_none_or(|x| x != "tasks") {
continue;
}
files += 1;
for line in std::fs::read_to_string(e.path())
.unwrap_or_default()
.lines()
{
let l = line.trim();
if !l.is_empty() {
seen.insert(l.to_owned());
}
}
}
assert!(
files > 0,
"no coverage files in {dir} — the suite either did not run or did not \
see TRUST_TASK_OBSERVED_DIR, and a coverage figure over zero files \
would read as 0% rather than as 'not measured'"
);
let checkable = checkable_tasks();
let seen_bare: BTreeSet<&str> = seen
.iter()
.map(|u| u.strip_suffix("#response").unwrap_or(u))
.collect();
let mut uncovered: Vec<&str> = checkable
.iter()
.copied()
.filter(|u| !seen_bare.contains(u))
.collect();
uncovered.sort_unstable();
let total = checkable.len();
let covered = total - uncovered.len();
println!(
"\nTRUST-TASK RESPONSE COVERAGE {covered}/{total} ({:.0}%) — {} never exercised\n",
(covered as f64 / total.max(1) as f64) * 100.0,
uncovered.len()
);
for u in &uncovered {
println!(" UNCOVERED {u}");
}
println!();
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
#[test]
fn a_non_conforming_payload_is_reported() {
let body = br#"{
"type": "https://trusttasks.org/spec/vault/list/0.1#response",
"payload": {"nope": 1}
}"#;
let out = check(StatusCode::OK, body);
assert!(
out.is_some_and(|m| m.contains("vault/list")),
"a payload that fails its schema must be reported, and the message \
must name the task so a sweep of a run is readable"
);
}
#[test]
fn an_unpublished_task_is_not_a_violation() {
let body = br#"{"type": "https://example.invalid/not/a/task/9.9#response",
"payload": {}}"#;
assert!(check(StatusCode::OK, body).is_none());
}
#[test]
fn an_error_outcome_is_exempt() {
let body = br#"{
"type": "https://trusttasks.org/spec/vault/list/0.1#response",
"payload": {"nope": 1}
}"#;
assert!(
check(StatusCode::BAD_REQUEST, body).is_none(),
"a 4xx body is a framework reject document, not the task's payload"
);
}
#[test]
fn a_violation_replaces_the_response() {
clear_violations();
let body = br#"{
"type": "https://trusttasks.org/spec/vault/list/0.1#response",
"payload": {"nope": 1}
}"#;
let out = observe(StatusCode::OK, body).expect("a violation must be fatal");
let doc: serde_json::Value = serde_json::from_slice(&out).expect("error document");
assert_eq!(doc["error"], "responseSchemaViolation");
assert!(
doc["message"]
.as_str()
.is_some_and(|m| m.contains("vault/list")),
"the replacement must name the task, or the failing test says nothing"
);
assert_eq!(
observed_violations().len(),
1,
"it must still be recorded — raising fails one test, recording is \
what lets a whole run be swept"
);
}
#[test]
fn a_conforming_response_is_not_replaced() {
assert!(observe(StatusCode::NO_CONTENT, b"").is_none());
assert!(observe(StatusCode::BAD_REQUEST, b"{}").is_none());
}
#[test]
fn the_inventory_is_still_accurate() {
assert_eq!(
KNOWN_VIOLATIONS.len(),
0,
"the known-violation inventory changed. Fixed one? Remove its entry \
and lower this. Found a new one? That is a defect to fix, not an \
entry to add — this list records what was already true when the \
layer landed, and it is meant to reach zero."
);
for (uri, why) in KNOWN_VIOLATIONS {
assert!(
trust_tasks_rs::schema_index::schema_for(&format!("{uri}#response")).is_some(),
"{uri} has no published response schema, so it cannot be \
violating one — the entry is stale"
);
assert!(!why.is_empty(), "{uri} needs a reason, not just a URI");
}
}
}