use std::error::Error;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Mutex};
use frame_core::capability::{
Capability, CapabilityDenied, CapabilityKind, CapabilityScope, CheckVerdict,
};
use frame_core::component::ComponentId;
use haematite::{CheckoutError, checkout};
use super::{TestDirectory, TestResult, component, install};
use crate::handle::ResolutionChecker;
use crate::types::{MetaState, ReferenceTargetState};
use crate::{ComponentStateStore, CrossComponentRef, EntityId, StateError};
struct RecordingChecker {
component: ComponentId,
allowed: AtomicBool,
declared: bool,
checks: AtomicUsize,
denials: Mutex<Vec<CapabilityDenied>>,
}
impl RecordingChecker {
fn new(component: ComponentId, allowed: bool, declared: bool) -> Self {
Self {
component,
allowed: AtomicBool::new(allowed),
declared,
checks: AtomicUsize::new(0),
denials: Mutex::new(Vec::new()),
}
}
fn revoke(&self) {
self.allowed.store(false, Ordering::Release);
}
fn check_count(&self) -> usize {
self.checks.load(Ordering::Acquire)
}
fn events(&self) -> Vec<CapabilityDenied> {
self.denials.lock().expect("denial recorder lock").clone()
}
}
impl ResolutionChecker for RecordingChecker {
fn component_id(&self) -> ComponentId {
self.component
}
fn check(
&self,
capability: &Capability,
) -> Result<CheckVerdict, frame_core::capability::CapabilityCheckError> {
self.checks.fetch_add(1, Ordering::AcqRel);
if self.allowed.load(Ordering::Acquire) {
return Ok(CheckVerdict::Allowed);
}
let denial = CapabilityDenied {
component_id: self.component,
kind: capability.kind(),
scope: capability.scope(),
declared: self.declared,
};
self.denials
.lock()
.expect("denial recorder lock")
.push(denial.clone());
Ok(CheckVerdict::Denied(denial))
}
}
fn denial(error: StateError) -> Result<CapabilityDenied, Box<dyn Error>> {
match error {
StateError::CapabilityDenied(denial) => Ok(denial),
other => Err(format!("expected capability denial, got {other}").into()),
}
}
fn dangling_state(error: StateError) -> Result<ReferenceTargetState, Box<dyn Error>> {
match error {
StateError::DanglingReference { state, .. } => Ok(state),
other => Err(format!("expected dangling reference, got {other}").into()),
}
}
#[test]
fn cross_component_ref_v1_round_trips_as_entity_content_and_rejects_corruption() -> TestResult {
let directory = TestDirectory::new("cross-ref-codec")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("codec-reader");
let target = component("codec-target");
let reader_handle = install(&store, reader)?;
let target_handle = install(&store, target)?;
let entity = target_handle.put(b"linked bytes")?;
target_handle.commit()?;
let reference = CrossComponentRef::new(target, entity);
let encoded = reference.to_bytes();
let stored_link = reader_handle.put(&encoded)?;
reader_handle.commit()?;
let decoded = CrossComponentRef::from_bytes(
&reader_handle
.get(stored_link)?
.expect("stored link entity exists"),
)?;
assert_eq!(decoded, reference);
let mut wrong_version = encoded;
wrong_version[0] = wrong_version[0].wrapping_add(1);
assert!(matches!(
CrossComponentRef::from_bytes(&wrong_version),
Err(StateError::CorruptRecord { detail })
if detail.contains("cross-component reference") && detail.contains("version")
));
assert!(matches!(
CrossComponentRef::from_bytes(&encoded[..encoded.len() - 1]),
Err(StateError::CorruptRecord { detail })
if detail.contains("cross-component reference") && detail.contains("bytes")
));
Ok(())
}
#[test]
fn ungranted_flavors_are_verbatim_and_evented_once_per_resolution() -> TestResult {
let directory = TestDirectory::new("cross-ref-denials")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("denied-reader");
let target = component("denied-target");
let handle = install(&store, reader)?;
let reference = CrossComponentRef::new(target, EntityId::of(b"absent"));
for declared in [true, false] {
let checker = RecordingChecker::new(reader, false, declared);
let observed = denial(
handle
.resolve_cross_component_with(&checker, reference)
.expect_err("ungranted resolution must fail"),
)?;
assert_eq!(observed.component_id, reader);
assert_eq!(observed.kind, CapabilityKind::CrossComponentRead);
assert_eq!(observed.scope, CapabilityScope::Component(target));
assert_eq!(observed.declared, declared);
assert_eq!(checker.check_count(), 1);
assert_eq!(checker.events(), vec![observed]);
}
Ok(())
}
#[test]
fn denial_is_identical_before_target_inspection_for_every_target_state() -> TestResult {
let reader = component("no-probe-reader");
let target = component("no-probe-target");
let entity = EntityId::of(b"no-probe");
let mut observed = Vec::new();
for state in ["active", "archived", "never-installed"] {
let directory = TestDirectory::new(&format!("no-probe-{state}"))?;
let store = ComponentStateStore::open(directory.path())?;
let handle = install(&store, reader)?;
if state != "never-installed" {
let target_handle = install(&store, target)?;
target_handle.put(b"no-probe")?;
target_handle.commit()?;
if state == "archived" {
store.archive_storage(target)?;
}
}
let checker = RecordingChecker::new(reader, false, true);
observed.push(denial(
handle
.resolve_cross_component_with(&checker, CrossComponentRef::new(target, entity))
.expect_err("denial precedes every target state"),
)?);
assert_eq!(checker.check_count(), 1);
assert_eq!(checker.events().len(), 1);
}
assert!(observed.windows(2).all(|pair| pair[0] == pair[1]));
Ok(())
}
#[test]
fn granted_link_content_round_trip_and_committed_root_visibility() -> TestResult {
let directory = TestDirectory::new("cross-ref-content")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("content-reader");
let target = component("content-target");
let reader_handle = install(&store, reader)?;
let target_handle = install(&store, target)?;
let entity = target_handle.put(b"committed target")?;
let reference = CrossComponentRef::new(target, entity);
let link_entity = reader_handle.put(&reference.to_bytes())?;
reader_handle.commit()?;
let stored = reader_handle
.get(link_entity)?
.expect("link persisted as entity content");
let stored = CrossComponentRef::from_bytes(&stored)?;
let checker = RecordingChecker::new(reader, true, true);
assert_eq!(
reader_handle.resolve_cross_component_with(&checker, stored)?,
None,
"uncommitted target content is invisible"
);
target_handle.commit()?;
assert_eq!(
reader_handle.resolve_cross_component_with(&checker, stored)?,
Some(b"committed target".to_vec())
);
assert_eq!(checker.check_count(), 2, "one check per fresh resolution");
Ok(())
}
#[test]
fn retained_handle_and_checker_refuse_the_next_act_after_revoke() -> TestResult {
let directory = TestDirectory::new("cross-ref-revoke")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("revoke-reader");
let target = component("revoke-target");
let handle = install(&store, reader)?;
let target_handle = install(&store, target)?;
let entity = target_handle.put(b"before revoke")?;
target_handle.commit()?;
let reference = CrossComponentRef::new(target, entity);
let checker = RecordingChecker::new(reader, true, true);
assert_eq!(
handle.resolve_cross_component_with(&checker, reference)?,
Some(b"before revoke".to_vec())
);
checker.revoke();
let refused = denial(
handle
.resolve_cross_component_with(&checker, reference)
.expect_err("next consuming act must observe revoke"),
)?;
assert!(refused.declared);
assert_eq!(checker.check_count(), 2);
assert_eq!(checker.events(), vec![refused]);
Ok(())
}
#[test]
fn confused_deputy_and_self_target_have_no_shortcuts() -> TestResult {
let directory = TestDirectory::new("cross-ref-uniformity")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("uniform-reader");
let deputy = component("uniform-deputy");
let handle = install(&store, reader)?;
let reference = CrossComponentRef::new(reader, EntityId::of(b"self"));
let wrong_checker = RecordingChecker::new(deputy, true, true);
assert!(matches!(
handle.resolve_cross_component_with(&wrong_checker, reference),
Err(StateError::CapabilityBinding {
handle_component,
checker_component
}) if handle_component == reader && checker_component == deputy
));
assert_eq!(wrong_checker.check_count(), 0);
assert!(wrong_checker.events().is_empty());
let own_checker = RecordingChecker::new(reader, false, true);
let self_denial = denial(
handle
.resolve_cross_component_with(&own_checker, reference)
.expect_err("self-target requires the same grant"),
)?;
assert_eq!(self_denial.scope, CapabilityScope::Component(reader));
assert_eq!(own_checker.check_count(), 1);
assert_eq!(own_checker.events(), vec![self_denial]);
Ok(())
}
#[test]
fn dangling_states_are_typed_and_archived_content_remains_recoverable() -> TestResult {
let directory = TestDirectory::new("cross-ref-dangling")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("dangling-reader");
let target = component("dangling-target");
let handle = install(&store, reader)?;
let checker = RecordingChecker::new(reader, true, true);
let entity = EntityId::of(b"recoverable");
let reference = CrossComponentRef::new(target, entity);
assert_eq!(
dangling_state(
handle
.resolve_cross_component_with(&checker, reference)
.expect_err("never-installed target dangles")
)?,
ReferenceTargetState::NeverInstalled
);
let target_handle = install(&store, target)?;
target_handle.put(b"recoverable")?;
target_handle.commit()?;
assert_eq!(
handle.resolve_cross_component_with(&checker, reference)?,
Some(b"recoverable".to_vec())
);
let archive = store.archive_storage(target)?;
assert_eq!(
dangling_state(
handle
.resolve_cross_component_with(&checker, reference)
.expect_err("archived target dangles")
)?,
ReferenceTargetState::Archived {
generation: archive.generation
}
);
assert_eq!(
store.get_archived(target, archive.generation, entity)?,
Some(b"recoverable".to_vec())
);
Ok(())
}
#[test]
fn transitional_dangling_states_name_the_durable_meta_state() -> TestResult {
let directory = TestDirectory::new("cross-ref-transitional")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("transition-reader");
let installing = component("transition-installing");
let archiving = component("transition-archiving");
let handle = install(&store, reader)?;
let checker = RecordingChecker::new(reader, true, true);
{
let mut storage = store.lock_storage()?;
let reader_record = storage
.meta_record(reader)?
.expect("reader meta record exists");
let mut installing_record = reader_record.clone();
installing_record.component = installing;
installing_record.state = MetaState::Installing;
storage.write_meta(&installing_record)?;
}
assert_eq!(
dangling_state(
handle
.resolve_cross_component_with(
&checker,
CrossComponentRef::new(installing, EntityId::of(b"missing")),
)
.expect_err("installing target dangles"),
)?,
ReferenceTargetState::Installing
);
install(&store, archiving)?;
{
let mut storage = store.lock_storage()?;
let mut record = storage
.meta_record(archiving)?
.expect("archiving meta record exists");
record.state = MetaState::Archiving {
generation: record.incarnation,
abandoned_work: Vec::new(),
};
storage.write_meta(&record)?;
}
assert_eq!(
dangling_state(
handle
.resolve_cross_component_with(
&checker,
CrossComponentRef::new(archiving, EntityId::of(b"missing")),
)
.expect_err("archiving target dangles"),
)?,
ReferenceTargetState::Archiving { generation: 0 }
);
Ok(())
}
#[test]
fn stale_reader_fence_precedes_target_checkout() -> TestResult {
let directory = TestDirectory::new("cross-ref-stale-reader")?;
let store = ComponentStateStore::open(directory.path())?;
let reader = component("stale-reader");
let target = component("stale-target");
let stale = install(&store, reader)?;
let target_handle = install(&store, target)?;
let entity = target_handle.put(b"target")?;
target_handle.commit()?;
store.archive_storage(reader)?;
let fresh = install(&store, reader)?;
let checker = RecordingChecker::new(reader, true, true);
assert!(matches!(
stale.resolve_cross_component_with(&checker, CrossComponentRef::new(target, entity)),
Err(StateError::StaleHandle {
component: stale_component,
held: 0,
current: 1
}) if stale_component == reader
));
assert_eq!(fresh.get(entity)?, None);
assert_eq!(checker.check_count(), 1);
Ok(())
}
#[test]
fn archive_racing_resolution_has_exactly_bytes_or_typed_miss() -> TestResult {
let directory = TestDirectory::new("cross-ref-archive-race")?;
let store = Arc::new(ComponentStateStore::open(directory.path())?);
let reader = component("race-reader");
let target = component("race-target");
let handle = install(&store, reader)?;
let target_handle = install(&store, target)?;
let entity = target_handle.put(b"valid root")?;
target_handle.commit()?;
let reference = CrossComponentRef::new(target, entity);
let checker = RecordingChecker::new(reader, true, true);
assert_eq!(
handle.resolve_cross_component_with(&checker, reference)?,
Some(b"valid root".to_vec()),
"bytes-at-valid-root is reachable before the race"
);
let barrier = Arc::new(Barrier::new(2));
let archive_store = Arc::clone(&store);
let archive_barrier = Arc::clone(&barrier);
let archive = std::thread::spawn(move || {
archive_barrier.wait();
archive_store.archive_storage(target)
});
barrier.wait();
while !archive.is_finished() {
match handle.resolve_cross_component_with(&checker, reference) {
Ok(Some(bytes)) if bytes == b"valid root" => {}
Err(StateError::DanglingReference { target: missed, .. }) if missed == target => {}
other => {
return Err(format!("outcome outside exhaustive race set: {other:?}").into());
}
}
}
archive.join().expect("archive thread did not panic")?;
assert!(
matches!(
handle.resolve_cross_component_with(&checker, reference),
Err(StateError::DanglingReference { target: missed, .. }) if missed == target
),
"typed-miss is reachable after the race"
);
Ok(())
}
#[test]
fn checkout_write_surface_refuses_and_public_resolution_returns_only_owned_bytes() -> TestResult {
let directory = TestDirectory::new("cross-ref-read-only")?;
let store = ComponentStateStore::open(directory.path())?;
let target = component("read-only-target");
let target_handle = install(&store, target)?;
target_handle.put(b"immutable view")?;
let root = target_handle.commit()?;
let storage = store.lock_storage()?;
let view = checkout(&storage.nodes, root);
assert!(matches!(
view.put(b"key", b"value"),
Err(CheckoutError::ReadOnly)
));
assert!(matches!(view.delete(b"key"), Err(CheckoutError::ReadOnly)));
assert!(matches!(view.commit(), Err(CheckoutError::ReadOnly)));
for source in [
include_str!("../lib.rs"),
include_str!("../handle.rs"),
include_str!("../store.rs"),
include_str!("../types.rs"),
include_str!("../archive.rs"),
include_str!("../codec.rs"),
include_str!("../engine.rs"),
include_str!("../error.rs"),
include_str!("../schema.rs"),
] {
for pattern in [
"-> ReadOnlyView",
"-> haematite::ReadOnlyView",
"Result<ReadOnlyView",
"Result<haematite::ReadOnlyView",
"Box<ReadOnlyView",
"= ReadOnlyView",
"= haematite::ReadOnlyView",
"pub use haematite::ReadOnlyView",
] {
assert!(
!source.contains(pattern),
"frame-state source must not return, alias, or re-export a checkout view (matched {pattern:?})"
);
}
}
Ok(())
}