#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::sync::{Arc, Mutex};
use chio_core::canonical::canonical_json_bytes;
use chio_core::crypto::Keypair;
use chio_core::receipt::{
body::chio_receipt_id, body::ChioReceipt, body::ChioReceiptBody, decision::Decision,
decision::ToolCallAction, kinds::TrustLevel, metadata::GuardEvidence,
};
use chio_kernel::settlement_observer::{
self, SettlementObserverStatus, SETTLEMENT_OBSERVER_STATUS_SCHEMA,
};
use chio_settle::{SettlementHook, SettlementHookError, SettlementObservation, SettlementOutcome};
struct RecordingHook {
observations: Mutex<Vec<SettlementObservation>>,
}
impl RecordingHook {
fn new() -> Self {
Self {
observations: Mutex::new(Vec::new()),
}
}
fn snapshot(&self) -> Vec<SettlementObservation> {
self.observations
.lock()
.expect("recording hook lock")
.clone()
}
}
impl SettlementHook for RecordingHook {
fn observe(
&self,
observation: &SettlementObservation,
_idempotency_key: &chio_settle::SettlementIdempotencyKey,
) -> Result<SettlementOutcome, SettlementHookError> {
self.observations
.lock()
.expect("recording hook lock")
.push(observation.clone());
Ok(SettlementOutcome::accepted(format!(
"ts-{}",
observation.receipt_id
)))
}
}
fn build_receipt(index: u64, kp: &Keypair) -> (ChioReceipt, String) {
let metadata = serde_json::json!({
"financial": {
"cost_charged": 100 + index, "currency": "USD"
}
});
let action = ToolCallAction::from_parameters(serde_json::json!({"i": index}))
.expect("test action constructs");
let mut body = ChioReceiptBody {
id: format!("rcpt-{index:03}"),
timestamp: 1_000 + index,
capability_id: format!("cap-{index}"),
tool_server: "srv".to_string(),
tool_name: "tool".to_string(),
action,
decision: Some(Decision::Allow),
receipt_kind: Default::default(),
boundary_class: Default::default(),
observation_outcome: None,
tool_origin: Default::default(),
redaction_mode: Default::default(),
actor_chain: Vec::new(),
content_hash: format!("ch-{index}"),
policy_hash: "policy-1".to_string(),
evidence: vec![GuardEvidence {
guard_name: "G".to_string(),
verdict: true,
details: None,
}],
metadata: Some(metadata),
trust_level: TrustLevel::default(),
tenant_id: None,
kernel_key: kp.public_key(),
bbs_projection_version: None,
};
body.id = chio_receipt_id(&body).expect("canonical receipt id computes");
let receipt = ChioReceipt::sign(body, kp).expect("test receipt signs");
let canonical_id = receipt.id.clone();
(receipt, canonical_id)
}
#[test]
fn ten_receipts_produce_ten_settlements_with_byte_identical_receipts() {
let kp = Keypair::generate();
let signed: Vec<(ChioReceipt, String)> = (0..10).map(|i| build_receipt(i, &kp)).collect();
let receipts: Vec<&ChioReceipt> = signed.iter().map(|(r, _)| r).collect();
let expected_ids: Vec<&str> = signed.iter().map(|(_, id)| id.as_str()).collect();
let baseline_bytes: Vec<Vec<u8>> = receipts
.iter()
.map(|receipt| canonical_json_bytes(*receipt).expect("baseline canonical bytes"))
.collect();
let hook = Arc::new(RecordingHook::new());
let hook_handle: Arc<dyn SettlementHook> = hook.clone();
let mut statuses = Vec::with_capacity(receipts.len());
for receipt in &receipts {
statuses.push(settlement_observer::run_observer(
Some(&hook_handle),
receipt,
std::slice::from_ref(&receipt.kernel_key),
&chio_settle::SettlementIdempotencyKey {
receipt_id: receipt.id.clone(),
row_version: 1,
},
));
}
assert_eq!(statuses.len(), 10);
let observed = hook.snapshot();
assert_eq!(observed.len(), 10);
let mut accepted_transcripts = Vec::new();
for status in &statuses {
match status {
SettlementObserverStatus::Observed {
outcome: SettlementOutcome::Accepted { transcript_id, .. },
} => accepted_transcripts.push(transcript_id.clone()),
other => panic!("expected accepted outcome, got {other:?}"),
}
}
assert_eq!(accepted_transcripts.len(), 10);
for (receipt, baseline) in receipts.iter().zip(baseline_bytes.iter()) {
let after = canonical_json_bytes(*receipt).expect("post-observer canonical bytes");
assert_eq!(
&after, baseline,
"settlement hook must NEVER mutate receipt bytes"
);
}
for (i, observation) in observed.iter().enumerate() {
assert_eq!(observation.receipt_id, expected_ids[i]);
assert_eq!(observation.finalized_at, 1_000 + i as u64);
let key = observation.ordering_key();
assert_eq!(key.0, observation.finalized_at);
assert_eq!(key.1, observation.receipt_id.as_str());
}
assert_eq!(
SETTLEMENT_OBSERVER_STATUS_SCHEMA,
"chio.settle.observer-status.v1"
);
}
#[test]
fn no_settlement_baseline_matches_with_settlement_canonical_bytes() {
let kp = Keypair::generate();
let receipts_no_hook: Vec<ChioReceipt> = (0..10).map(|i| build_receipt(i, &kp).0).collect();
let receipts_with_hook: Vec<ChioReceipt> = (0..10).map(|i| build_receipt(i, &kp).0).collect();
let hook: Arc<dyn SettlementHook> = Arc::new(RecordingHook::new());
for receipt in &receipts_with_hook {
let _status = settlement_observer::run_observer(
Some(&hook),
receipt,
std::slice::from_ref(&receipt.kernel_key),
&chio_settle::SettlementIdempotencyKey {
receipt_id: receipt.id.clone(),
row_version: 1,
},
);
}
for (no_hook, with_hook) in receipts_no_hook.iter().zip(receipts_with_hook.iter()) {
let baseline = canonical_json_bytes(no_hook).expect("baseline canonical bytes");
let observed = canonical_json_bytes(with_hook).expect("observed canonical bytes");
assert_eq!(
baseline, observed,
"receipts byte-equivalent under no-settlement vs with-settlement"
);
}
}