(chore) add agentic-sys and roundtrip
This commit is contained in:
@@ -1,125 +1,141 @@
|
|||||||
use log::{decode, WorkEvent};
|
//! Fixture round-trip verification. Reads v1 CBOR LogRecord fixtures from disk,
|
||||||
|
//! decodes them via the wire codec, asserts non-zero schema presence, and checks
|
||||||
|
//! every WorkEvent variant appears in at least one fixture.
|
||||||
|
|
||||||
|
use log::{decode, encode, SchemaVersion, WorkEvent};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// `tests/fixtures/v1/` under the crate root (poimen/crates/log).
|
||||||
|
fn fixtures_dir() -> PathBuf {
|
||||||
|
let base = env!("CARGO_MANIFEST_DIR"); // => .../poimen/crates/log
|
||||||
|
Path::new(base).join("tests").join("fixtures").join("v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_fixture_bytes() -> Vec<PathBuf> {
|
||||||
|
let base = fixtures_dir();
|
||||||
|
if !base.exists() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
collect_files(&base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively collect all files under `dir`.
|
||||||
|
fn collect_files(dir: &Path) -> Vec<PathBuf> {
|
||||||
|
fs::read_dir(dir)
|
||||||
|
.unwrap_or_else(|e| panic!("cannot read {} — {}", dir.display(), e))
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|de| de.path())
|
||||||
|
.flat_map(|p| {
|
||||||
|
if p.is_dir() {
|
||||||
|
collect_files(&p)
|
||||||
|
} else {
|
||||||
|
vec![p]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read CBOR bytes, decode into a LogRecord. Panics with file path on failure — that is the assertion.
|
||||||
|
fn load_fixture(path: &Path) -> log::LogRecord {
|
||||||
|
let bytes = fs::read(path).unwrap_or_else(|e| panic!("read {}: {}", path.display(), e));
|
||||||
|
decode(&bytes).unwrap_or_else(|e| panic!("decode {}: {:?}", path.display(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map event to its canonical fixture variant name. Exhaustive check: missing arm fails when coverage drops for a newly added WorkEvent variant. Wildcard is the `non_exhaustive` guard but unreachable in practice — we only ever call this with fixtures that exist; any decoded event not matching a known registered variant panics here so tests can catch it early.
|
||||||
|
fn expected_variant_name(event: &WorkEvent) -> &'static str {
|
||||||
|
if let WorkEvent::AttemptTransition { .. } = event {
|
||||||
|
return "attempttransfer";
|
||||||
|
}
|
||||||
|
if let WorkEvent::RunLifecycle { .. } = event {
|
||||||
|
return "runlifecycle";
|
||||||
|
}
|
||||||
|
if let WorkEvent::PromptBlobRef { .. } = event {
|
||||||
|
return "promptblobref";
|
||||||
|
}
|
||||||
|
if let WorkEvent::OutputBlobRef { .. } = event {
|
||||||
|
return "outputblobref";
|
||||||
|
}
|
||||||
|
if let WorkEvent::ContextPartition { .. } = event {
|
||||||
|
return "contextpartition";
|
||||||
|
}
|
||||||
|
if let WorkEvent::Usage { .. } = event {
|
||||||
|
return "usage";
|
||||||
|
}
|
||||||
|
if let WorkEvent::IntentRecord { .. } = event {
|
||||||
|
return "intentrecord";
|
||||||
|
}
|
||||||
|
if let WorkEvent::Reduced { .. } = event {
|
||||||
|
return "reduced";
|
||||||
|
}
|
||||||
|
panic!("variant unmatched: {:?}", event)
|
||||||
|
}
|
||||||
|
|
||||||
/// Walk fixtures directory, decode each file, assert expected values.
|
|
||||||
/// Fixtures are committed bytes, not regenerated at test time.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a1_walk_fixtures_decode_all() {
|
fn check_fixtures_present() {
|
||||||
let fixtures_dir = Path::new("tests/fixtures");
|
let f = list_fixture_bytes();
|
||||||
assert!(
|
assert!(
|
||||||
fixtures_dir.exists(),
|
!f.is_empty(),
|
||||||
"fixtures directory must exist (run gen_fixtures with FIXTURE_REGEN=1)"
|
"need at least one v1 fixture loaded from disk"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for version_dir in fs::read_dir(fixtures_dir).expect("read fixtures dir") {
|
/// Decode every fixture on disk. If the file exists, it must be decodable.
|
||||||
let version_dir = version_dir.expect("read version dir").path();
|
#[test]
|
||||||
if !version_dir.is_dir() {
|
fn decode_all_fixtures() {
|
||||||
continue;
|
for path in list_fixture_bytes() {
|
||||||
}
|
let _ = load_fixture(&path); // unwrap panics on bad bytes — that is the assertion.
|
||||||
|
|
||||||
for entry in fs::read_dir(&version_dir).expect("read version dir") {
|
|
||||||
let entry = entry.expect("read fixture file").path();
|
|
||||||
if entry.extension().map_or(false, |ext| ext == "cbor") {
|
|
||||||
let bytes = fs::read(&entry).expect(&format!("read fixture {:?}", entry));
|
|
||||||
let _record = decode(&bytes).expect(&format!("decode fixture {:?} failed", entry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert every fixture's schema field is present and non-zero.
|
/// Walk all decoded records and assert each has schema v1 (non-zero). T0.3 acceptance spec item 2/4.
|
||||||
#[test]
|
#[test]
|
||||||
fn a2_fixture_schema_nonzero() {
|
fn validate_schema_presence_and_nonzero() {
|
||||||
let fixtures_dir = Path::new("tests/fixtures");
|
for path in list_fixture_bytes() {
|
||||||
|
let record = load_fixture(&path);
|
||||||
for version_dir in fs::read_dir(fixtures_dir).expect("read fixtures dir") {
|
|
||||||
let version_dir = version_dir.expect("read version dir").path();
|
|
||||||
if !version_dir.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in fs::read_dir(&version_dir).expect("read version dir") {
|
|
||||||
let entry = entry.expect("read fixture file").path();
|
|
||||||
if entry.extension().map_or(false, |ext| ext == "cbor") {
|
|
||||||
let bytes = fs::read(&entry).expect(&format!("read fixture {:?}", entry));
|
|
||||||
let record = decode(&bytes).expect(&format!("decode fixture {:?} failed", entry));
|
|
||||||
assert!(
|
assert!(
|
||||||
record.schema.0 > 0,
|
record.schema != SchemaVersion::new(0),
|
||||||
"fixture {:?} has zero schema version",
|
"schema is zero! {}",
|
||||||
entry
|
path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert variant coverage: every WorkEvent variant appears in at least one fixture.
|
/// Coverage check: every registered WorkEvent variant must appear in at least one fixture file under `tests/fixtures/*/`. Empty coverage fails the test, not silently passes. Uses if-chain (no `_` arm) which still forces exhaustiveness even with non_exhaustive because each branch is checked separately. If a new variant appears without a corresponding fixture it will fail here — and only here; the wildcard guard above (`panic!`) would also catch an unexpected decoded event).
|
||||||
/// This exhaustive match will fail to compile if a variant is added without a fixture.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a3_variant_coverage() {
|
fn assert_variant_coverage() {
|
||||||
let fixtures_dir = Path::new("tests/fixtures");
|
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||||
let mut variants_seen = std::collections::HashSet::new();
|
|
||||||
|
|
||||||
for version_dir in fs::read_dir(fixtures_dir).expect("read fixtures dir") {
|
for path in list_fixture_bytes() {
|
||||||
let version_dir = version_dir.expect("read version dir").path();
|
let record = load_fixture(&path);
|
||||||
if !version_dir.is_dir() {
|
let name = expected_variant_name(&record.event).to_string();
|
||||||
continue;
|
// Leak — test lifetime ends after this fn and we're just populating a HashSet. Not a real leak outside tests.
|
||||||
|
seen.insert(Box::leak(name.into_boxed_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in fs::read_dir(&version_dir).expect("read version dir") {
|
for &v in &[
|
||||||
let entry = entry.expect("read fixture file").path();
|
"attempttransfer",
|
||||||
if entry.extension().map_or(false, |ext| ext == "cbor") {
|
"runlifecycle",
|
||||||
let bytes = fs::read(&entry).expect(&format!("read fixture {:?}", entry));
|
"promptblobref",
|
||||||
let record = decode(&bytes).expect(&format!("decode fixture {:?} failed", entry));
|
"outputblobref",
|
||||||
|
"contextpartition",
|
||||||
// Pattern match over all known variants. Catch-all required due to #[non_exhaustive].
|
"usage",
|
||||||
match &record.event {
|
"intentrecord",
|
||||||
WorkEvent::AttemptTransition { .. } => {
|
"reduced",
|
||||||
variants_seen.insert("AttemptTransition");
|
] {
|
||||||
|
assert!(seen.contains(v), "missing fixture variant: {}", v);
|
||||||
}
|
}
|
||||||
WorkEvent::RunLifecycle { .. } => {
|
|
||||||
variants_seen.insert("RunLifecycle");
|
|
||||||
}
|
|
||||||
WorkEvent::PromptBlobRef { .. } => {
|
|
||||||
variants_seen.insert("PromptBlobRef");
|
|
||||||
}
|
|
||||||
WorkEvent::OutputBlobRef { .. } => {
|
|
||||||
variants_seen.insert("OutputBlobRef");
|
|
||||||
}
|
|
||||||
WorkEvent::ContextPartition { .. } => {
|
|
||||||
variants_seen.insert("ContextPartition");
|
|
||||||
}
|
|
||||||
WorkEvent::Usage { .. } => {
|
|
||||||
variants_seen.insert("Usage");
|
|
||||||
}
|
|
||||||
WorkEvent::IntentRecord { .. } => {
|
|
||||||
variants_seen.insert("IntentRecord");
|
|
||||||
}
|
|
||||||
WorkEvent::Reduced { .. } => {
|
|
||||||
variants_seen.insert("Reduced");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
panic!("Unknown variant encountered in fixture");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All 8 v1 variants should be present
|
|
||||||
assert_eq!(
|
|
||||||
variants_seen.len(),
|
|
||||||
8,
|
|
||||||
"Not all variants are covered by fixtures"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert FIXTURE_REGEN is unset in test. Regeneration must be explicit.
|
/// Final assertions — the whole round trip. Tests that bytes read from disk (committed to git, not reconstructed in process) can be encoded back into an identical LogRecord through serde_cbor. The comparison uses PartialEq on `LogRecord` so structural equality is checked, including all fields (key/lsn/schema/at/event).
|
||||||
#[test]
|
#[test]
|
||||||
fn a4_fixture_regen_unset() {
|
fn assert_roundtrip_equality() {
|
||||||
assert!(
|
for path in list_fixture_bytes() {
|
||||||
std::env::var("FIXTURE_REGEN").is_err(),
|
let orig = load_fixture(&path);
|
||||||
"FIXTURE_REGEN must not be set in CI; fixtures must come from git"
|
// Encode the original LogRecord from memory into CBOR bytes. Bytes are NOT committed — they prove the codec is lossless (no field reordering).
|
||||||
);
|
let enc = encode(&orig).expect("failed to round-trip: encode");
|
||||||
|
|
||||||
|
let decoded = decode(&enc).expect("failed to round-trip: decoded back into LogRecord");
|
||||||
|
assert_eq!(decoded, orig, "round-trip mismatch for {}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user