use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use chio_kernel::{
MemoryProvenanceAppend, MemoryProvenanceStore, ProvenanceVerification, UnverifiedReason,
MEMORY_PROVENANCE_GENESIS_PREV_HASH,
};
use chio_store_sqlite::SqliteMemoryProvenanceStore;
use chio_test_support::prelude::*;
fn unique_db_path(prefix: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.test_expect("system time before unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("{prefix}-{nonce}.sqlite3"))
}
fn sample_append(key: &str, receipt: &str, when: u64) -> MemoryProvenanceAppend {
MemoryProvenanceAppend {
store: "agent-context".to_string(),
key: key.to_string(),
capability_id: "cap-1".to_string(),
receipt_id: receipt.to_string(),
written_at: when,
}
}
#[test]
fn append_assigns_genesis_prev_hash_for_first_entry() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let entry = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("append");
assert_eq!(entry.prev_hash, MEMORY_PROVENANCE_GENESIS_PREV_HASH);
assert_eq!(entry.hash.len(), 64);
assert!(entry.hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn append_is_idempotent_by_receipt_id() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let input = sample_append("doc-1", "rcpt-1", 100);
let first = store
.append(input.clone())
.test_expect("first append succeeds");
let replay = store.append(input).test_expect("replay succeeds");
assert_eq!(replay, first);
assert!(store.append(sample_append("doc-2", "rcpt-1", 100)).is_err());
}
#[test]
fn append_links_successive_entries_via_prev_hash() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let first = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("first append");
let second = store
.append(sample_append("doc-2", "rcpt-2", 101))
.test_expect("second append");
assert_eq!(second.prev_hash, first.hash);
assert_ne!(second.hash, first.hash);
assert_eq!(store.chain_digest().test_expect("digest"), second.hash);
}
#[test]
fn latest_for_key_returns_most_recent_entry() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let _earlier = store
.append(sample_append("doc-9", "rcpt-earlier", 100))
.test_expect("earlier");
let later = store
.append(sample_append("doc-9", "rcpt-later", 150))
.test_expect("later");
let latest = store
.latest_for_key("agent-context", "doc-9")
.test_expect("latest_for_key")
.test_expect("entry for doc-9");
assert_eq!(latest.entry_id, later.entry_id);
assert_eq!(latest.receipt_id, "rcpt-later");
}
#[test]
fn latest_for_key_returns_none_for_unknown_key() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let result = store
.latest_for_key("agent-context", "doc-ghost")
.test_expect("latest_for_key");
assert!(result.is_none());
}
#[test]
fn verify_entry_accepts_valid_chain() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let _first = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("first append");
let second = store
.append(sample_append("doc-2", "rcpt-2", 101))
.test_expect("second append");
let verification = store
.verify_entry(&second.entry_id)
.test_expect("verify_entry");
match verification {
ProvenanceVerification::Verified {
entry,
chain_digest,
} => {
assert_eq!(entry.entry_id, second.entry_id);
assert_eq!(chain_digest, second.hash);
}
ProvenanceVerification::Unverified { reason } => {
panic!("expected verified chain, got unverified: {reason:?}");
}
}
}
#[test]
fn verify_entry_detects_hash_tamper() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let entry = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("append");
let forged = "b".repeat(64);
let updated = store
.tamper_entry_hash(&entry.entry_id, &forged)
.test_expect("tamper helper");
assert!(updated, "tamper helper should find the row");
let verification = store
.verify_entry(&entry.entry_id)
.test_expect("verify_entry");
assert!(
matches!(
verification,
ProvenanceVerification::Unverified {
reason: UnverifiedReason::ChainTampered
}
),
"expected ChainTampered, got {verification:?}"
);
}
#[test]
fn verify_entry_detects_broken_link_when_prev_row_mutated() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let first = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("first");
let second = store
.append(sample_append("doc-2", "rcpt-2", 101))
.test_expect("second");
let forged = "c".repeat(64);
store
.tamper_entry_hash(&first.entry_id, &forged)
.test_expect("tamper helper");
let verification = store
.verify_entry(&second.entry_id)
.test_expect("verify_entry on second");
assert!(
matches!(
verification,
ProvenanceVerification::Unverified {
reason: UnverifiedReason::ChainLinkBroken
}
),
"expected ChainLinkBroken, got {verification:?}"
);
}
#[test]
fn verify_entry_flags_unknown_entry_as_no_provenance() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let verification = store
.verify_entry("missing-id")
.test_expect("verify_entry on unknown id");
assert!(matches!(
verification,
ProvenanceVerification::Unverified {
reason: UnverifiedReason::NoProvenance
}
));
}
#[test]
fn chain_digest_is_genesis_on_empty_store() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
assert_eq!(
store.chain_digest().test_expect("digest"),
MEMORY_PROVENANCE_GENESIS_PREV_HASH
);
}
#[test]
fn chain_persists_across_reopen() {
let path = unique_db_path("chio-mem-prov");
let first_hash;
let first_entry_id;
{
let store = SqliteMemoryProvenanceStore::open(&path).test_expect("open on disk");
let entry = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("append");
first_hash = entry.hash.clone();
first_entry_id = entry.entry_id.clone();
}
let reopened = SqliteMemoryProvenanceStore::open(&path).test_expect("reopen");
assert_eq!(reopened.chain_digest().test_expect("digest"), first_hash);
let second = reopened
.append(sample_append("doc-2", "rcpt-2", 200))
.test_expect("second append on reopened store");
assert_eq!(second.prev_hash, first_hash);
let verification = reopened
.verify_entry(&first_entry_id)
.test_expect("verify_entry persisted row");
assert!(matches!(
verification,
ProvenanceVerification::Verified { .. }
));
let _ = fs::remove_file(path);
}
#[test]
fn get_entry_returns_the_committed_row() {
let store = SqliteMemoryProvenanceStore::open_in_memory().test_expect("open in-memory store");
let entry = store
.append(sample_append("doc-1", "rcpt-1", 100))
.test_expect("append");
let fetched = store
.get_entry(&entry.entry_id)
.test_expect("get_entry")
.test_expect("row should exist after append");
assert_eq!(fetched, entry);
}