use std::collections::{HashMap, HashSet, VecDeque};
use std::path::Path;
use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use serde::Serialize;
use crate::cli::CliOutput;
use crate::identity::sign::SignableLink;
#[derive(clap::Args, Debug)]
pub struct VerifyChainArgs {
pub memory_id: String,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
#[arg(long)]
pub include_signed_events: bool,
}
#[derive(Debug, Serialize)]
pub struct EdgeResult {
pub source_id: String,
pub target_id: String,
pub signature_hex: Option<String>,
pub attest_level: String,
pub verified: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SignedEventSummary {
pub memory_id: String,
pub event_id: String,
pub event_type: String,
pub attest_level: String,
pub timestamp: String,
pub signature_present: bool,
}
#[derive(Debug, Serialize)]
pub struct ChainReport {
pub ok: bool,
pub root_id: String,
pub n_memories: usize,
pub chain_depth: usize,
pub edges_verified: usize,
pub edges_failed: usize,
pub edges: Vec<EdgeResult>,
pub max_reflection_depth_per_namespace: HashMap<String, i32>,
pub bounded_status: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub signed_events: Vec<SignedEventSummary>,
pub generated_at: String,
}
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn fetch_memory_meta(conn: &Connection, id: &str) -> Result<Option<(String, String, i32)>> {
let mut stmt =
conn.prepare("SELECT id, namespace, reflection_depth FROM memories WHERE id = ?1")?;
let mut rows = stmt.query(params![id])?;
if let Some(row) = rows.next()? {
Ok(Some((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i32>(2).unwrap_or(0),
)))
} else {
Ok(None)
}
}
struct EdgeRow {
target_id: String,
signature: Option<Vec<u8>>,
observed_by: Option<String>,
attest_level: Option<String>,
valid_from: Option<String>,
valid_until: Option<String>,
}
fn fetch_reflects_on_edges(conn: &Connection, source_id: &str) -> Result<Vec<EdgeRow>> {
let mut stmt = conn.prepare(
"SELECT target_id, signature, observed_by, attest_level, valid_from, valid_until \
FROM memory_links \
WHERE source_id = ?1 AND relation = 'reflects_on'",
)?;
let rows = stmt.query_map(params![source_id], |row| {
Ok(EdgeRow {
target_id: row.get::<_, String>(0)?,
signature: row.get::<_, Option<Vec<u8>>>(1)?,
observed_by: row.get::<_, Option<String>>(2)?,
attest_level: row.get::<_, Option<String>>(3)?,
valid_from: row.get::<_, Option<String>>(4)?,
valid_until: row.get::<_, Option<String>>(5)?,
})
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(Into::into)
}
fn fetch_signed_events_for(conn: &Connection, ids: &[String]) -> Result<Vec<SignedEventSummary>> {
if ids.is_empty() {
return Ok(Vec::new());
}
let placeholders: String = ids
.iter()
.enumerate()
.map(|(i, _)| format!("?{}", i + 1))
.collect::<Vec<_>>()
.join(", ");
let sql = format!(
"SELECT id, agent_id, event_type, payload_hash, signature, attest_level, timestamp \
FROM signed_events \
WHERE agent_id IN ({placeholders}) \
ORDER BY timestamp ASC, id ASC \
LIMIT 1000"
);
let mut stmt = conn.prepare(&sql)?;
let param_refs: Vec<&dyn rusqlite::ToSql> =
ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
let rows = stmt.query_map(param_refs.as_slice(), |row| {
Ok(SignedEventSummary {
event_id: row.get::<_, String>(0)?,
memory_id: row.get::<_, String>(1)?,
event_type: row.get::<_, String>(2)?,
signature_present: row.get::<_, Option<Vec<u8>>>(4)?.is_some(),
attest_level: row.get::<_, String>(5)?,
timestamp: row.get::<_, String>(6)?,
})
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(Into::into)
}
fn governance_cap_for_namespace(conn: &Connection, namespace: &str) -> Option<u32> {
crate::db::resolve_governance_policy(conn, namespace).and_then(|p| p.core.max_reflection_depth)
}
pub fn build_chain_report(
conn: &Connection,
root_id: &str,
include_signed_events: bool,
) -> Result<ChainReport> {
build_chain_report_at(conn, root_id, include_signed_events, None)
}
pub fn build_chain_report_at(
conn: &Connection,
root_id: &str,
include_signed_events: bool,
generated_at_override: Option<&str>,
) -> Result<ChainReport> {
let generated_at = generated_at_override
.map(ToString::to_string)
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
let mut visited: HashSet<String> = HashSet::new();
let mut queue: VecDeque<(String, usize)> = VecDeque::new();
queue.push_back((root_id.to_string(), 0));
let mut edges: Vec<EdgeResult> = Vec::new();
let mut max_depth_per_ns: HashMap<String, i32> = HashMap::new();
let mut chain_depth: usize = 0;
let mut all_ids: Vec<String> = Vec::new();
let mut any_governance_row = false;
let mut cap_exceeded = false;
while let Some((current_id, hop)) = queue.pop_front() {
if visited.contains(¤t_id) {
continue;
}
visited.insert(current_id.clone());
all_ids.push(current_id.clone());
if hop > chain_depth {
chain_depth = hop;
}
if let Some((_id, ns, rd)) = fetch_memory_meta(conn, ¤t_id)? {
let entry = max_depth_per_ns.entry(ns.clone()).or_insert(0_i32);
if rd > *entry {
*entry = rd;
}
if let Some(cap) = governance_cap_for_namespace(conn, &ns) {
any_governance_row = true;
#[allow(clippy::cast_sign_loss)]
if rd > 0 && rd as u32 > cap {
cap_exceeded = true;
}
}
}
let out_edges = fetch_reflects_on_edges(conn, ¤t_id)?;
for row in out_edges {
let attest_level = row
.attest_level
.clone()
.unwrap_or_else(|| crate::models::AttestLevel::Unsigned.as_str().to_string());
let (verified, failure_reason, signature_hex) = verify_edge(
¤t_id,
&row.target_id,
row.signature.as_deref(),
row.observed_by.as_deref(),
row.valid_from.as_deref(),
row.valid_until.as_deref(),
&attest_level,
);
let target_id = row.target_id.clone();
edges.push(EdgeResult {
source_id: current_id.clone(),
target_id: target_id.clone(),
signature_hex,
attest_level,
verified,
failure_reason,
});
if !visited.contains(&target_id) {
queue.push_back((target_id, hop + 1));
}
}
}
let edges_failed = edges.iter().filter(|e| !e.verified).count();
let edges_verified = edges.len() - edges_failed;
let bounded_status = if cap_exceeded {
"exceeded_cap"
} else if any_governance_row {
"within_cap"
} else {
"no_cap_configured"
}
.to_string();
let signed_events = if include_signed_events {
fetch_signed_events_for(conn, &all_ids).unwrap_or_default()
} else {
Vec::new()
};
let ok = edges_failed == 0 && bounded_status != "exceeded_cap";
Ok(ChainReport {
ok,
root_id: root_id.to_string(),
n_memories: visited.len(),
chain_depth,
edges_verified,
edges_failed,
edges,
max_reflection_depth_per_namespace: max_depth_per_ns,
bounded_status,
signed_events,
generated_at,
})
}
fn verify_edge(
source_id: &str,
target_id: &str,
sig_blob: Option<&[u8]>,
observed_by: Option<&str>,
valid_from: Option<&str>,
valid_until: Option<&str>,
attest_level: &str,
) -> (bool, Option<String>, Option<String>) {
let signature_hex = sig_blob.map(bytes_to_hex);
let Some(sig) = sig_blob else {
return (true, None, None);
};
let Some(agent_id) = observed_by else {
return (
false,
Some(
"signature present but observed_by is NULL — \
cannot resolve public key"
.to_string(),
),
signature_hex,
);
};
if agent_id.is_empty() {
return (
false,
Some("observed_by is empty — cannot resolve public key".to_string()),
signature_hex,
);
}
let pub_key = crate::identity::verify::lookup_peer_public_key(agent_id);
let Some(pub_key) = pub_key else {
return (
false,
Some(format!(
"no public key enrolled for '{agent_id}' \
(attest_level={attest_level})"
)),
signature_hex,
);
};
let link = SignableLink {
src_id: source_id,
dst_id: target_id,
relation: crate::models::MemoryLinkRelation::ReflectsOn.as_str(),
observed_by: Some(agent_id),
valid_from,
valid_until,
};
match crate::identity::verify::verify(&pub_key, &link, sig) {
Ok(()) => (true, None, signature_hex),
Err(e) => (false, Some(e.to_string()), signature_hex),
}
}
pub(super) fn render_text(report: &ChainReport, out: &mut CliOutput<'_>) -> Result<()> {
writeln!(
out.stdout,
"verify-reflection-chain: root={} memories={} depth={} edges={} failed={}",
report.root_id,
report.n_memories,
report.chain_depth,
report.edges.len(),
report.edges_failed,
)?;
writeln!(out.stdout, "bounded_status: {}", report.bounded_status)?;
writeln!(out.stdout, "generated_at: {}", report.generated_at)?;
if !report.max_reflection_depth_per_namespace.is_empty() {
writeln!(out.stdout, "\nmax_reflection_depth per namespace:")?;
let mut ns_vec: Vec<_> = report.max_reflection_depth_per_namespace.iter().collect();
ns_vec.sort_by_key(|(ns, _)| ns.as_str());
for (ns, depth) in ns_vec {
writeln!(out.stdout, " {ns}: {depth}")?;
}
}
if !report.edges.is_empty() {
writeln!(out.stdout, "\nedges:")?;
for e in &report.edges {
let status = if e.verified { "OK" } else { "FAIL" };
let src_short = &e.source_id[..e.source_id.len().min(8)];
let tgt_short = &e.target_id[..e.target_id.len().min(8)];
write!(
out.stdout,
" [{status}] {src_short} -> {tgt_short} attest={}",
e.attest_level,
)?;
if let Some(ref reason) = e.failure_reason {
write!(out.stdout, " reason=\"{reason}\"")?;
}
writeln!(out.stdout)?;
}
}
if !report.signed_events.is_empty() {
writeln!(
out.stdout,
"\nsigned_events ({} rows):",
report.signed_events.len()
)?;
for ev in &report.signed_events {
writeln!(
out.stdout,
" {} | {} | {} | sig={}",
ev.event_id,
ev.event_type,
ev.timestamp,
if ev.signature_present { "yes" } else { "no" }
)?;
}
}
Ok(())
}
pub fn run(db_path: &Path, args: &VerifyChainArgs, out: &mut CliOutput<'_>) -> Result<i32> {
let json = args.format.to_ascii_lowercase() == "json";
let conn = crate::db::open(db_path).context("open db")?;
let report = build_chain_report(&conn, &args.memory_id, args.include_signed_events)?;
if json {
let payload = serde_json::to_string_pretty(&report).context("serialise chain report")?;
writeln!(out.stdout, "{payload}")?;
} else {
render_text(&report, out)?;
}
if report.ok { Ok(0) } else { Ok(2) }
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use rusqlite::params;
use tempfile::TempDir;
use crate::db;
use crate::identity::keypair as kp_mod;
use crate::identity::sign;
use crate::models::{Memory, Tier};
fn open_test_db(tmp: &TempDir) -> (rusqlite::Connection, std::path::PathBuf) {
let db_path = tmp.path().join("ai-memory.db");
let conn = db::open(&db_path).expect("db::open");
(conn, db_path)
}
fn insert_mem(conn: &rusqlite::Connection, ns: &str, depth: i32) -> String {
let id = uuid::Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
let mem = Memory {
id: id.clone(),
tier: Tier::Mid,
namespace: ns.to_string(),
title: format!("t-{depth}"),
content: format!("c-{depth}"),
reflection_depth: depth,
created_at: now.clone(),
updated_at: now,
..Default::default()
};
db::insert(conn, &mem).expect("insert");
id
}
fn link_unsigned(conn: &rusqlite::Connection, src: &str, tgt: &str) {
conn.execute(
"INSERT OR IGNORE INTO memory_links \
(source_id, target_id, relation, created_at, attest_level) \
VALUES (?1, ?2, 'reflects_on', ?3, 'unsigned')",
params![src, tgt, Utc::now().to_rfc3339()],
)
.expect("link_unsigned");
}
fn set_cap(conn: &rusqlite::Connection, ns: &str, cap: u32) {
use crate::models::default_metadata;
let now = Utc::now().to_rfc3339();
let policy = crate::models::GovernancePolicy {
core: crate::models::CorePolicy {
max_reflection_depth: Some(cap),
..crate::models::CorePolicy::default()
},
..crate::models::GovernancePolicy::default()
};
let mut metadata = default_metadata();
if let Some(obj) = metadata.as_object_mut() {
obj.insert("agent_id".into(), serde_json::Value::String("test".into()));
obj.insert("governance".into(), serde_json::to_value(&policy).unwrap());
}
let standard = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: format!("_standards-{ns}"),
title: format!("standard for {ns}"),
content: "policy".into(),
created_at: now.clone(),
updated_at: now,
metadata,
..Default::default()
};
let sid = db::insert(conn, &standard).expect("insert standard");
db::set_namespace_standard(conn, ns, &sid, None).expect("set_namespace_standard");
}
#[test]
fn single_memory_no_edges_gives_empty_report() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let id = insert_mem(&conn, "ns", 0);
let report = build_chain_report(&conn, &id, false).expect("report");
assert_eq!(report.root_id, id);
assert_eq!(report.n_memories, 1);
assert_eq!(report.chain_depth, 0);
assert_eq!(report.edges.len(), 0);
assert_eq!(report.edges_failed, 0);
assert_eq!(report.edges_verified, 0);
assert_eq!(report.bounded_status, "no_cap_configured");
assert!(report.signed_events.is_empty());
}
#[test]
fn unsigned_chain_depth2_all_verified() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let d0 = insert_mem(&conn, "ns", 0);
let d1 = insert_mem(&conn, "ns", 1);
let d2 = insert_mem(&conn, "ns", 2);
link_unsigned(&conn, &d2, &d1);
link_unsigned(&conn, &d1, &d0);
let report = build_chain_report(&conn, &d2, false).expect("report");
assert_eq!(report.n_memories, 3);
assert_eq!(report.chain_depth, 2);
assert_eq!(report.edges_failed, 0);
assert!(report.edges.iter().all(|e| e.verified));
}
#[test]
fn cap_exceeded_reported_in_bounded_status() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
set_cap(&conn, "cap-ns", 0);
let d0 = insert_mem(&conn, "cap-ns", 0);
let d1 = insert_mem(&conn, "cap-ns", 1); link_unsigned(&conn, &d1, &d0);
let report = build_chain_report(&conn, &d1, false).expect("report");
assert_eq!(report.bounded_status, "exceeded_cap");
}
#[test]
fn tampered_sig_edge_marked_failed() {
let _g = kp_mod::key_dir_env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = TempDir::new().unwrap();
let keys_tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let agent = kp_mod::generate("tester-l13").expect("gen");
kp_mod::save(&agent, keys_tmp.path()).expect("save");
let d0 = insert_mem(&conn, "ns", 0);
let d1 = insert_mem(&conn, "ns", 1);
let now = Utc::now().to_rfc3339();
let link = sign::SignableLink {
src_id: &d1,
dst_id: &d0,
relation: "reflects_on",
observed_by: Some(&agent.agent_id),
valid_from: Some(&now),
valid_until: None,
};
let mut sig = sign::sign(&agent, &link).expect("sign");
sig[0] ^= 0x01;
conn.execute(
"INSERT OR IGNORE INTO memory_links \
(source_id, target_id, relation, created_at, valid_from, \
signature, observed_by, attest_level) \
VALUES (?1, ?2, 'reflects_on', ?3, ?3, ?4, ?5, 'self_signed')",
params![d1, d0, now, sig, agent.agent_id],
)
.expect("insert tampered");
unsafe {
std::env::set_var("AI_MEMORY_KEY_DIR", keys_tmp.path());
}
let report = build_chain_report(&conn, &d1, false).expect("report");
unsafe {
std::env::remove_var("AI_MEMORY_KEY_DIR");
}
assert_eq!(report.edges_failed, 1, "tampered edge must count as failed");
assert!(
report.edges[0].failure_reason.is_some(),
"tampered edge must carry a reason"
);
}
#[test]
fn include_signed_events_flag_returns_vec() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let id = insert_mem(&conn, "se-ns", 0);
let r = build_chain_report(&conn, &id, false).expect("report");
assert!(r.signed_events.is_empty());
let r2 = build_chain_report(&conn, &id, true).expect("report-se");
let _ = r2.signed_events; }
#[test]
fn bytes_to_hex_matches_format_pattern() {
let b = vec![0x00, 0x0f, 0xff, 0xab];
assert_eq!(bytes_to_hex(&b), "000fffab");
}
#[test]
fn fetch_memory_meta_returns_none_for_unknown_id() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let r = fetch_memory_meta(&conn, "nonexistent-id-xxxxxx").expect("query");
assert!(r.is_none(), "unknown id must return None");
}
#[test]
fn fetch_signed_events_for_empty_ids_returns_empty() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let v = fetch_signed_events_for(&conn, &[]).expect("call");
assert!(v.is_empty());
}
#[test]
fn fetch_signed_events_for_seeded_rows_returns_summaries() {
let tmp = TempDir::new().unwrap();
let (conn, _) = open_test_db(&tmp);
let agent_id = "seeded-actor";
let payload = b"hello";
let event = crate::signed_events::SignedEvent {
id: uuid::Uuid::new_v4().to_string(),
agent_id: agent_id.to_string(),
event_type: crate::signed_events::event_types::MEMORY_LINK_CREATED.to_string(),
payload_hash: crate::signed_events::payload_hash(payload),
signature: Some(vec![0xab; 64]),
attest_level: "self_signed".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
..crate::signed_events::SignedEvent::default()
};
crate::signed_events::append_signed_event(&conn, &event).expect("append");
let v =
fetch_signed_events_for(&conn, &[agent_id.to_string()]).expect("fetch with seeded row");
assert_eq!(v.len(), 1);
assert!(v[0].signature_present, "signature blob should be detected");
assert_eq!(v[0].memory_id, agent_id);
}
#[test]
fn verify_edge_unsigned_returns_verified_with_no_reason() {
let (verified, reason, sig_hex) = verify_edge(
"src-id",
"tgt-id",
None,
Some("alice"),
None,
None,
"unsigned",
);
assert!(verified);
assert!(reason.is_none());
assert!(sig_hex.is_none());
}
#[test]
fn verify_edge_signed_but_no_observed_by_fails() {
let sig = vec![0xff; 64];
let (verified, reason, sig_hex) =
verify_edge("src", "tgt", Some(&sig), None, None, None, "self_signed");
assert!(!verified);
let reason = reason.expect("reason set");
assert!(reason.contains("observed_by is NULL"), "got: {reason}");
assert!(sig_hex.is_some());
}
#[test]
fn verify_edge_signed_with_empty_observed_by_fails() {
let sig = vec![0xff; 64];
let (verified, reason, _) = verify_edge(
"src",
"tgt",
Some(&sig),
Some(""),
None,
None,
"self_signed",
);
assert!(!verified);
let reason = reason.expect("reason set");
assert!(reason.contains("empty"), "got: {reason}");
}
#[test]
fn verify_edge_signed_with_unknown_agent_fails() {
let _g = kp_mod::key_dir_env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let keys_tmp = TempDir::new().unwrap();
unsafe {
std::env::set_var("AI_MEMORY_KEY_DIR", keys_tmp.path());
}
let sig = vec![0xff; 64];
let (verified, reason, _) = verify_edge(
"src",
"tgt",
Some(&sig),
Some("never-enrolled-agent"),
None,
None,
"self_signed",
);
unsafe {
std::env::remove_var("AI_MEMORY_KEY_DIR");
}
assert!(!verified);
let reason = reason.expect("reason set");
assert!(reason.contains("no public key enrolled"), "got: {reason}");
}
#[test]
fn render_text_emits_ns_table_and_failure_reasons() {
use std::collections::HashMap;
let mut ns = HashMap::new();
ns.insert("ns-one".to_string(), 3);
ns.insert("ns-two".to_string(), 1);
let report = ChainReport {
ok: false,
root_id: "0123456789abcdef0123".to_string(),
n_memories: 2,
chain_depth: 1,
edges_verified: 0,
edges_failed: 1,
edges: vec![EdgeResult {
source_id: "src-id-long-1234".to_string(),
target_id: "tgt-id-long-5678".to_string(),
signature_hex: Some("aabb".to_string()),
attest_level: "self_signed".to_string(),
verified: false,
failure_reason: Some("tampered".to_string()),
}],
max_reflection_depth_per_namespace: ns,
bounded_status: "within_cap".to_string(),
signed_events: vec![SignedEventSummary {
memory_id: "agent-x".to_string(),
event_id: "ev-1".to_string(),
event_type: crate::signed_events::event_types::MEMORY_STORED.to_string(),
attest_level: "self_signed".to_string(),
timestamp: "2026-05-13T00:00:00Z".to_string(),
signature_present: true,
}],
generated_at: "2026-05-13T00:00:00Z".to_string(),
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
render_text(&report, &mut out).expect("render");
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("ns-one: 3"), "ns table line missing: {s}");
assert!(s.contains("ns-two: 1"), "ns table line missing: {s}");
assert!(s.contains("FAIL"), "edge status missing: {s}");
assert!(s.contains("tampered"), "failure reason missing: {s}");
assert!(s.contains("signed_events"), "signed_events footer: {s}");
assert!(s.contains("ev-1"), "event id missing: {s}");
assert!(s.contains("sig=yes"), "signature flag: {s}");
}
#[test]
fn render_text_signed_event_without_signature_says_no() {
let report = ChainReport {
ok: true,
root_id: "root-id-here".to_string(),
n_memories: 1,
chain_depth: 0,
edges_verified: 0,
edges_failed: 0,
edges: vec![],
max_reflection_depth_per_namespace: std::collections::HashMap::new(),
bounded_status: "no_cap_configured".to_string(),
signed_events: vec![SignedEventSummary {
memory_id: "agent-y".to_string(),
event_id: "ev-2".to_string(),
event_type: crate::signed_events::event_types::MEMORY_TOUCH.to_string(),
attest_level: "unsigned".to_string(),
timestamp: "2026-05-13T01:00:00Z".to_string(),
signature_present: false,
}],
generated_at: "2026-05-13T00:00:00Z".to_string(),
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
render_text(&report, &mut out).expect("render");
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("sig=no"), "must mark unsigned event: {s}");
}
#[test]
fn run_json_format_emits_pretty_payload() {
let tmp = TempDir::new().unwrap();
let (_, db_path) = open_test_db(&tmp);
let id = insert_mem(&open_test_db(&tmp).0, "ns", 0);
let args = VerifyChainArgs {
memory_id: id,
format: "json".to_string(),
include_signed_events: false,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
let _ = run(&db_path, &args, &mut out);
}
#[test]
fn run_against_real_db_emits_text_report_and_exit_0() {
let tmp = TempDir::new().unwrap();
let (conn, db_path) = open_test_db(&tmp);
let d0 = insert_mem(&conn, "ns", 0);
let d1 = insert_mem(&conn, "ns", 1);
link_unsigned(&conn, &d1, &d0);
drop(conn);
let args = VerifyChainArgs {
memory_id: d1,
format: "text".to_string(),
include_signed_events: false,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
let code = run(&db_path, &args, &mut out).expect("run");
assert_eq!(code, 0);
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("verify-reflection-chain"));
assert!(s.contains("memories=2"));
}
#[test]
fn run_with_cap_exceeded_returns_exit_code_1() {
let tmp = TempDir::new().unwrap();
let (conn, db_path) = open_test_db(&tmp);
set_cap(&conn, "limit-ns", 0);
let d0 = insert_mem(&conn, "limit-ns", 0);
let d1 = insert_mem(&conn, "limit-ns", 1);
link_unsigned(&conn, &d1, &d0);
drop(conn);
let args = VerifyChainArgs {
memory_id: d1,
format: "json".to_string(),
include_signed_events: false,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
let code = run(&db_path, &args, &mut out).expect("run");
assert_eq!(code, 2, "exceeded cap must exit 2");
}
#[test]
fn run_json_format_with_include_signed_events_emits_field() {
let tmp = TempDir::new().unwrap();
let (conn, db_path) = open_test_db(&tmp);
let id = insert_mem(&conn, "ns", 0);
drop(conn);
let args = VerifyChainArgs {
memory_id: id,
format: "json".to_string(),
include_signed_events: true,
};
let mut stdout = Vec::<u8>::new();
let mut stderr = Vec::<u8>::new();
let mut out = crate::cli::CliOutput::from_std(&mut stdout, &mut stderr);
let code = run(&db_path, &args, &mut out).expect("run");
assert_eq!(code, 0);
let s = String::from_utf8(stdout).unwrap();
assert!(s.contains("\"root_id\""), "got: {s}");
assert!(s.contains("\"bounded_status\""), "got: {s}");
}
}