use std::fs;
use std::path::{Path, PathBuf};
use super::client::{CanonClient, CanonError};
use super::types::{CanonCatalogue, CanonMatchRequest, CanonMatchResponse};
#[derive(Debug, Clone)]
pub struct MockCanonClient {
fixture_dir: PathBuf,
}
impl MockCanonClient {
pub fn new(fixture_dir: PathBuf) -> Self {
Self { fixture_dir }
}
pub fn from_env() -> Option<Self> {
let dir = std::env::var("ARISTO_CANON_FIXTURE").ok()?;
Some(Self::new(PathBuf::from(dir)))
}
fn load<T: for<'de> serde::Deserialize<'de>>(&self, rel: &Path) -> Result<T, CanonError> {
let path = self.fixture_dir.join(rel);
let raw = fs::read_to_string(&path)
.map_err(|e| CanonError::Fixture(format!("read fixture {}: {e}", path.display())))?;
toml::from_str(&raw)
.map_err(|e| CanonError::Fixture(format!("parse fixture {}: {e}", path.display())))
}
}
impl CanonClient for MockCanonClient {
fn match_annotations(
&self,
_req: &CanonMatchRequest,
) -> Result<CanonMatchResponse, CanonError> {
self.load(Path::new("match.toml"))
}
fn catalogue(&self) -> Result<CanonCatalogue, CanonError> {
self.load(Path::new("catalogue.toml"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canon::types::{AnnotationMatchInput, CanonMatch, PrefixTier, VerificationMetadata};
use tempfile::TempDir;
fn write_match_fixture(dir: &Path, body: &str) {
fs::write(dir.join("match.toml"), body).unwrap();
}
#[test]
fn match_annotations_loads_handwritten_fixture() {
let tmp = TempDir::new().unwrap();
let fixture = r#"
effective_scopes = [":vanilla"]
canon_version = "v0.2.0"
matched_at = "2026-06-15T09:14:22Z"
results = [
[
{ canon_id = "cell_written_exactly_once_per_page_edit", version = "v0.2.1", canonical_text = "edit_page writes each cell exactly once", confidence = 0.92, scope = ":vanilla", prefix_tier = "aristos:", backed_by = "specialized neural checker", linked = "arta_a1b2c3d4", verification = { coverage_level = "tight", test_binaries = ["monotonicity_property"] } }
]
]
"#;
write_match_fixture(tmp.path(), fixture);
let client = MockCanonClient::new(tmp.path().to_path_buf());
let req = CanonMatchRequest {
annotations: vec![AnnotationMatchInput {
annotation_text: "test".into(),
applies_to: vec!["fn".into()],
}],
confidence_threshold: 0.85,
include_suggestions: false,
};
let resp = client.match_annotations(&req).unwrap();
assert_eq!(resp.results.len(), 1);
assert_eq!(resp.results[0].len(), 1);
let m: &CanonMatch = &resp.results[0][0];
assert_eq!(m.canon_id, "cell_written_exactly_once_per_page_edit");
assert_eq!(m.version, "v0.2.1");
assert_eq!(m.prefix_tier, PrefixTier::Aristos);
assert_eq!(m.backed_by.as_deref(), Some("specialized neural checker"));
assert_eq!(m.scope, ":vanilla");
assert_eq!(resp.effective_scopes, vec![":vanilla".to_string()]);
assert_eq!(resp.canon_version, "v0.2.0");
}
#[test]
fn match_annotations_missing_fixture_surfaces_fixture_error() {
let tmp = TempDir::new().unwrap();
let client = MockCanonClient::new(tmp.path().to_path_buf());
let req = CanonMatchRequest {
annotations: vec![],
confidence_threshold: 0.5,
include_suggestions: false,
};
let err = client.match_annotations(&req).unwrap_err();
assert!(matches!(err, CanonError::Fixture(_)));
assert!(err.to_string().contains("match.toml"), "got: {err}");
}
#[test]
fn match_annotations_unparseable_fixture_surfaces_fixture_error() {
let tmp = TempDir::new().unwrap();
write_match_fixture(tmp.path(), "this is not valid TOML at all = =");
let client = MockCanonClient::new(tmp.path().to_path_buf());
let req = CanonMatchRequest {
annotations: vec![],
confidence_threshold: 0.5,
include_suggestions: false,
};
let err = client.match_annotations(&req).unwrap_err();
assert!(matches!(err, CanonError::Fixture(_)));
}
#[test]
fn catalogue_loads_fixture() {
use crate::canon::{CanonCatalogue, CanonCatalogueEntry};
let mut backed = std::collections::BTreeMap::new();
backed.insert(
":vanilla".to_string(),
"specialized neural checker".to_string(),
);
let cat = CanonCatalogue {
serving: None,
entries: vec![
CanonCatalogueEntry {
canon_id: "foo".into(),
version: "v0.2.1".into(),
canonical_text: "foo text".into(),
category: "invariants".into(),
applies_to: vec!["fn".into()],
backed_by: backed,
coverage_level: "tight".into(),
spec_refs: vec!["S-001".into()],
},
CanonCatalogueEntry {
canon_id: "bar".into(),
version: "v0.1.0".into(),
canonical_text: "bar text".into(),
category: "invariants".into(),
applies_to: vec!["fn".into()],
backed_by: std::collections::BTreeMap::new(),
coverage_level: "none".into(),
spec_refs: vec![],
},
],
};
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("catalogue.toml"),
toml::to_string(&cat).unwrap(),
)
.unwrap();
let client = MockCanonClient::new(tmp.path().to_path_buf());
let loaded = client.catalogue().unwrap();
assert_eq!(loaded, cat);
assert_eq!(loaded.entries[0].tier_label(), "aristos");
assert_eq!(loaded.entries[1].tier_label(), "kanon");
}
#[test]
fn from_env_returns_none_when_var_unset() {
if std::env::var("ARISTO_CANON_FIXTURE").is_err() {
assert!(MockCanonClient::from_env().is_none());
}
}
#[test]
fn mock_client_is_object_safe() {
let tmp = TempDir::new().unwrap();
let _boxed: Box<dyn CanonClient> = Box::new(MockCanonClient::new(tmp.path().to_path_buf()));
}
#[test]
fn match_response_round_trips_through_toml() {
let resp = CanonMatchResponse {
results: vec![vec![CanonMatch {
canon_id: "foo".into(),
version: "v0.1.0".into(),
canonical_text: "foo text".into(),
confidence: 0.9,
scope: ":vanilla".into(),
prefix_tier: PrefixTier::Kanon,
backed_by: None,
linked: Some("arta_xyz1".into()),
verification: VerificationMetadata {
coverage_level: "none".into(),
test_binaries: vec![],
instrumentation: None,
},
}]],
effective_scopes: vec![":vanilla".into()],
canon_version: "v0.2.0".into(),
matched_at: "2026-06-15T09:14:22Z".into(),
suggestions: None,
};
let tmp = TempDir::new().unwrap();
let toml_text = toml::to_string(&resp).unwrap();
fs::write(tmp.path().join("match.toml"), toml_text).unwrap();
let client = MockCanonClient::new(tmp.path().to_path_buf());
let req = CanonMatchRequest {
annotations: vec![],
confidence_threshold: 0.5,
include_suggestions: false,
};
let loaded = client.match_annotations(&req).unwrap();
assert_eq!(loaded, resp);
}
}