use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defattest")]
pub struct AttestSpec {
pub id: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub counts_hash: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub severity: String,
}
pub const KNOWN_KINDS: &[&str] = &["pin", "min", "max"];
pub const KNOWN_SEVERITIES: &[&str] = &["info", "warn", "error"];
#[must_use]
pub fn is_known_kind(kind: &str) -> bool {
KNOWN_KINDS.contains(&kind)
}
#[must_use]
pub fn is_known_severity(severity: &str) -> bool {
KNOWN_SEVERITIES.contains(&severity)
}
#[must_use]
pub fn compute_summary_hash(summary: &str) -> String {
crate::hash::compute_blake3_128_hex(summary.as_bytes())
}
impl AttestSpec {
#[must_use]
pub fn effective_kind(&self) -> &str {
crate::strutil::default_if_empty(&self.kind, "pin")
}
#[must_use]
pub fn effective_severity(&self) -> &str {
crate::strutil::default_if_empty(&self.severity, "error")
}
#[must_use]
pub fn has_valid_hash_format(&self) -> bool {
crate::hash::is_blake3_128_hex(&self.counts_hash)
}
#[must_use]
pub fn is_empty_hash(&self) -> bool {
self.counts_hash.is_empty()
}
#[must_use]
pub fn evaluate(&self, actual_hash: &str) -> AttestResult {
if self.is_empty_hash() {
return AttestResult::Skipped;
}
if self.counts_hash == actual_hash {
AttestResult::Ok
} else {
AttestResult::Drift {
expected: self.counts_hash.clone(),
actual: actual_hash.to_string(),
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttestResult {
Ok,
Drift { expected: String, actual: String },
Skipped,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_summary_hash_is_stable_and_lowercase_hex() {
let a = compute_summary_hash("keybinds=10 cmds=5 options=20");
let b = compute_summary_hash("keybinds=10 cmds=5 options=20");
assert_eq!(a, b, "hash must be deterministic");
assert_eq!(a.len(), 32, "BLAKE3-128 hex is 32 chars");
assert!(
a.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)),
"hash must be lowercase hex only",
);
}
#[test]
fn compute_summary_hash_distinguishes_different_summaries() {
let a = compute_summary_hash("keybinds=10");
let b = compute_summary_hash("keybinds=11");
assert_ne!(a, b);
}
#[test]
fn effective_kind_defaults_to_pin() {
let s = AttestSpec { id: "x".into(), ..Default::default() };
assert_eq!(s.effective_kind(), "pin");
let s = AttestSpec { id: "x".into(), kind: "min".into(), ..Default::default() };
assert_eq!(s.effective_kind(), "min");
}
#[test]
fn effective_severity_defaults_to_error() {
let s = AttestSpec { id: "x".into(), ..Default::default() };
assert_eq!(s.effective_severity(), "error");
let s = AttestSpec {
id: "x".into(),
severity: "warn".into(),
..Default::default()
};
assert_eq!(s.effective_severity(), "warn");
}
#[test]
fn kind_and_severity_classifiers_match_known_vocab() {
for k in KNOWN_KINDS {
assert!(is_known_kind(k));
}
assert!(!is_known_kind("strict"));
for s in KNOWN_SEVERITIES {
assert!(is_known_severity(s));
}
assert!(!is_known_severity("critical"));
}
#[test]
fn hash_format_requires_32_lowercase_hex() {
let s = AttestSpec {
id: "x".into(),
counts_hash: "af42c0d18e9b3f4aa18b7c3ef1de93a4".into(),
..Default::default()
};
assert!(s.has_valid_hash_format());
let s = AttestSpec {
id: "x".into(),
counts_hash: "AF42c0d18e9b3f4aa18b7c3ef1de93a4".into(),
..Default::default()
};
assert!(!s.has_valid_hash_format());
let s = AttestSpec {
id: "x".into(),
counts_hash: "af42".into(),
..Default::default()
};
assert!(!s.has_valid_hash_format());
let s = AttestSpec {
id: "x".into(),
counts_hash: "zz42c0d18e9b3f4aa18b7c3ef1de93a4".into(),
..Default::default()
};
assert!(!s.has_valid_hash_format());
let s = AttestSpec { id: "x".into(), ..Default::default() };
assert!(!s.has_valid_hash_format());
assert!(s.is_empty_hash());
}
#[test]
fn evaluate_resolves_ok_drift_and_skipped() {
let s = AttestSpec {
id: "x".into(),
counts_hash: compute_summary_hash("keybinds=1"),
..Default::default()
};
assert_eq!(
s.evaluate(&compute_summary_hash("keybinds=1")),
AttestResult::Ok,
);
match s.evaluate(&compute_summary_hash("keybinds=2")) {
AttestResult::Drift { expected, actual } => {
assert_eq!(expected, s.counts_hash);
assert_ne!(expected, actual);
}
other => panic!("expected Drift, got {other:?}"),
}
let s = AttestSpec { id: "x".into(), ..Default::default() };
assert_eq!(s.evaluate("whatever"), AttestResult::Skipped);
}
}