use std::path::PathBuf;
use antares_format::{AntReader, AntRecord};
fn golden_dir() -> PathBuf {
match std::env::var_os("ANT_CONFORMANCE_GOLDEN") {
Some(dir) => PathBuf::from(dir),
None => {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../openantares/conformance/golden")
}
}
}
fn golden(name: &str) -> Vec<u8> {
let dir = golden_dir();
let path = dir.join(name);
std::fs::read(&path).unwrap_or_else(|e| {
panic!(
"missing golden {}: {e}\n\
The conformance goldens were not found. Set ANT_CONFORMANCE_GOLDEN to \
the directory holding them (e.g. the openantares/ant checkout) if they \
do not live at {}.",
path.display(),
dir.display()
)
})
}
#[test]
fn basic_golden_verifies_with_expected_counts() {
let bytes = golden("basic.ant");
let mut r = AntReader::new(&bytes[..]).expect("golden must open");
assert_eq!(r.manifest.tenant_id, 1);
assert_eq!(r.manifest.project_id, 1);
let mut kinds = Vec::new();
while let Some(rec) = r.next_record().expect("golden must read clean") {
kinds.push(match rec {
AntRecord::SchemaType { .. } => "schema_type",
AntRecord::Vertex { .. } => "vertex",
AntRecord::Edge { .. } => "edge",
AntRecord::Observation { .. } => "observation",
AntRecord::Evidence { .. } => "evidence",
AntRecord::Belief { .. } => "belief",
AntRecord::Vector { .. } => "vector",
AntRecord::VertexTombstone { .. } => "vertex_tombstone",
AntRecord::EdgeTombstone { .. } => "edge_tombstone",
AntRecord::Manifest(_) | AntRecord::Trailer { .. } => unreachable!(),
});
}
assert!(r.verified, "trailer sha256 + counts must verify");
assert_eq!(
kinds,
vec![
"vertex",
"vertex",
"edge",
"observation",
"evidence",
"belief",
"vector"
],
"golden record sequence drifted — regenerate deliberately or fix the format"
);
}
#[test]
fn forward_compat_golden_skips_unknown_kind_and_verifies() {
let bytes = golden("forward_compat.ant");
let mut r = AntReader::new(&bytes[..]).unwrap();
let mut n = 0;
while let Some(rec) = r.next_record().unwrap() {
assert!(matches!(rec, AntRecord::Vertex { .. }));
n += 1;
}
assert!(r.verified);
assert_eq!(n, 1, "the hologram record must be skipped, the vertex kept");
}
use antares_format::{Counts, FormatVersion, Manifest, FORMAT_VERSION};
use sha2::{Digest, Sha256};
fn stream_at(version: &str, extra_kind: Option<&str>) -> Vec<u8> {
let manifest = Manifest {
format: "antares".into(),
version: version.into(),
tenant_id: 1,
project_id: 1,
selection: None,
created_at: None,
producer: Some("version-policy-test".into()),
};
let m = serde_json::to_string(&AntRecord::Manifest(manifest)).unwrap();
let v = serde_json::to_string(&AntRecord::Vertex {
data: ant_types::Vertex {
id: ant_types::VertexId("v1".into()),
name: "V1".into(),
label: ant_types::TypeName("Antares.Deal".into()),
properties: Default::default(),
},
})
.unwrap();
let mut lines: Vec<String> = vec![m, v];
if let Some(kind) = extra_kind {
lines.push(format!(r#"{{"kind":"{kind}","data":{{"future":true}}}}"#));
}
let mut hasher = Sha256::new();
for l in &lines {
hasher.update(l.as_bytes());
hasher.update(b"\n");
}
let trailer = AntRecord::Trailer {
counts: Counts {
vertices: 1,
..Default::default()
},
sha256: format!("{:x}", hasher.finalize()),
};
let mut raw = lines.join("\n");
raw.push('\n');
raw.push_str(&serde_json::to_string(&trailer).unwrap());
raw.push('\n');
zstd::stream::encode_all(raw.as_bytes(), 0).unwrap()
}
fn read_all(bytes: &[u8]) -> Result<(bool, bool, usize), antares_format::AntError> {
let mut r = AntReader::new(bytes)?;
let mut n = 0;
while r.next_record()?.is_some() {
n += 1;
}
Ok((r.verified, r.minor_ahead, n))
}
#[test]
fn version_parses_major_minor() {
assert_eq!(
FormatVersion::parse("0.1"),
Some(FormatVersion { major: 0, minor: 1 })
);
assert_eq!(
FormatVersion::parse("0.2"),
Some(FormatVersion { major: 0, minor: 2 })
);
assert_eq!(
FormatVersion::parse("1"),
Some(FormatVersion { major: 1, minor: 0 }),
"a bare major means .0"
);
assert_eq!(FormatVersion::parse("nonsense"), None);
assert_eq!(FormatVersion::CURRENT.to_string(), FORMAT_VERSION);
}
#[test]
fn same_major_newer_minor_is_readable() {
let (verified, ahead, n) = read_all(&stream_at("0.4", None)).expect("v0.4 must be readable");
assert!(verified, "trailer still verifies across a minor bump");
assert_eq!(n, 1);
assert!(ahead, "the reader must know the file is ahead of it");
}
#[test]
fn same_major_newer_minor_with_unknown_kind_skips_cleanly() {
let (verified, ahead, n) =
read_all(&stream_at("0.9", Some("tombstone_from_the_future"))).expect("must be readable");
assert!(
verified,
"an unknown kind is hashed and skipped, so integrity holds"
);
assert_eq!(n, 1, "only the known vertex is surfaced");
assert!(ahead);
}
#[test]
fn older_minor_is_readable_and_not_flagged_ahead() {
let (verified, ahead, n) = read_all(&stream_at("0.0", None)).expect("v0.0 must be readable");
assert!(verified);
assert_eq!(n, 1);
assert!(!ahead, "an older file is not ahead of this reader");
}
#[test]
fn different_major_is_refused_with_a_reason() {
let err = read_all(&stream_at("1.0", None)).expect_err("v1.0 must be refused");
let msg = format!("{err}");
assert!(msg.contains("v1.0"), "must name the file's version: {msg}");
assert!(
msg.contains(FORMAT_VERSION),
"must name the reader's version: {msg}"
);
assert!(
msg.contains("Major versions are not compatible"),
"must say WHY, not just 'mismatch': {msg}"
);
assert!(
msg.contains("Upgrade the reader") && msg.contains("re-export"),
"must tell the operator what to do about it: {msg}"
);
}
#[test]
fn a_malformed_version_is_refused_with_a_reason() {
let err = read_all(&stream_at("banana", None)).expect_err("must be refused");
let msg = format!("{err}");
assert!(
msg.contains("MAJOR.MINOR"),
"must say what shape was expected: {msg}"
);
}
#[test]
fn tombstones_golden_surfaces_both_planes_and_counts_them() {
let bytes = golden("tombstones.ant");
let mut r = AntReader::new(&bytes[..]).expect("golden must open");
let mut kinds = Vec::new();
let mut tomb_ids = Vec::new();
while let Some(rec) = r.next_record().expect("golden must read clean") {
match rec {
AntRecord::Vertex { .. } => kinds.push("vertex"),
AntRecord::Edge { .. } => kinds.push("edge"),
AntRecord::VertexTombstone { data } => {
tomb_ids.push(data.id.clone());
assert!(
data.author.is_some(),
"the vertex tombstone golden carries an author stamp"
);
kinds.push("vertex_tombstone");
}
AntRecord::EdgeTombstone { data } => {
tomb_ids.push(data.id.clone());
assert!(
data.author.is_none(),
"the edge tombstone golden omits the author — both shapes must parse"
);
kinds.push("edge_tombstone");
}
other => panic!("unexpected record in tombstones.ant: {other:?}"),
}
}
assert!(r.verified, "trailer sha256 + counts must verify");
assert_eq!(
kinds,
vec!["vertex", "edge", "vertex_tombstone", "edge_tombstone"],
);
assert_eq!(
tomb_ids,
vec!["deal_gone", "deal_gone->acct_1:belongsTo"],
"tombstone ids must arrive intact — they are what the importer deletes by"
);
}
#[test]
fn major_version_golden_is_refused() {
let bytes = golden("major_version.ant");
let err = read_all(&bytes).expect_err("a v1.0 file must be refused by a 0.x reader");
let msg = format!("{err}");
assert!(msg.contains("v1.0"), "must name the file's version: {msg}");
assert!(
msg.contains("Major versions are not compatible"),
"must be refused for the VERSION, not for some other defect: {msg}"
);
}
#[test]
fn basic_golden_carries_the_v0_3_typed_properties() {
use ant_types::PropertyValue as P;
let bytes = golden("basic.ant");
let mut r = AntReader::new(&bytes[..]).expect("golden must open");
let mut deal = None;
while let Some(rec) = r.next_record().expect("golden must read clean") {
if let AntRecord::Vertex { data } = rec {
if data.id.0 == "deal_1" {
deal = Some(data);
}
}
}
let p = deal.expect("deal_1 in the golden").properties;
match p.get("exact_amount") {
Some(P::Decimal(d)) => assert_eq!(
d.to_string(),
"12345678901234567.89",
"the decimal lost digits — this is what parsing money as f64 looks like"
),
other => panic!("exact_amount is {other:?}, not a Decimal"),
}
match p.get("signed_at") {
Some(P::Timestamp(t)) => assert_eq!(
t.to_rfc3339(),
"2026-08-10T09:00:00+02:00",
"the offset was rewritten; TIMESTAMPTZ must keep it"
),
other => panic!("signed_at is {other:?}, not a Timestamp"),
}
assert!(matches!(p.get("closed_on"), Some(P::Date(_))));
assert!(matches!(p.get("review_at"), Some(P::Time(_))));
assert!(matches!(p.get("external_id"), Some(P::Uuid(_))));
assert_eq!(p.get("seal"), Some(&P::Bytes(vec![0x00, 0x01, 0xfe, 0xff])));
assert_eq!(p.get("headcount"), Some(&P::Int32(1200)));
assert_eq!(p.get("region_code"), Some(&P::Int16(-7)));
assert_eq!(
p.get("tags"),
Some(&P::Array(vec![
P::Text("enterprise".into()),
P::Text("renewal".into()),
]))
);
assert_eq!(p.get("amount"), Some(&P::Long(48000)));
assert_eq!(p.get("stage"), Some(&P::Text("proposal".into())));
assert!(matches!(p.get("meta"), Some(P::Json(_))));
}