use super::*;
use crate::event::{EventHeader, EventKind, HashChain};
use crate::store::segment::sidx::{kind_to_raw, read_entries_unauthenticated, SidxEntryCollector};
use std::io::Cursor;
fn frames_then_sdx3_footer(payloads: &[&str]) -> (Vec<u8>, u64) {
use crate::store::segment::sidx::SidxEntry;
let mut bytes = Vec::new();
let mut collector = SidxEntryCollector::new();
for (idx, p) in payloads.iter().enumerate() {
let frame_offset = bytes.len() as u64;
let frame = frame_encode(&serde_json::json!({ "payload": p })).expect("encode frame");
let frame_length = u32::try_from(frame.len()).expect("frame length fits u32");
bytes.extend_from_slice(&frame);
let entry = SidxEntry {
event_id: idx as u128 + 1,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash: [1; 32],
frame_offset,
frame_length,
global_sequence: idx as u64 + 1,
correlation_id: 1,
causation_id: 0,
};
collector
.record(entry, "entity:test", "scope:test")
.expect("intern test strings");
}
let frames_end = bytes.len() as u64;
let mut cursor = Cursor::new(&mut bytes);
cursor.seek(SeekFrom::End(0)).expect("seek to footer start");
collector
.write_footer(&mut cursor, 7)
.expect("write footer");
(bytes, frames_end)
}
fn frame_total_len_at(bytes: &[u8], offset: u64) -> u64 {
let start = usize::try_from(offset).expect("offset fits usize");
let header: [u8; 4] = bytes[start..start + 4]
.try_into()
.expect("4-byte frame length prefix");
let payload_len = u64::from(u32::from_be_bytes(header));
8 + payload_len
}
fn corroboratable_frame(
frame_offset: u64,
seq: u64,
payload: &serde_json::Value,
) -> (Vec<u8>, crate::store::segment::sidx::SidxEntry) {
let payload_bytes = crate::encoding::to_bytes(payload).expect("encode payload");
let event_hash = crate::event::hash::compute_hash(&payload_bytes);
let header = EventHeader::new(
seq as u128,
seq as u128,
None,
0,
crate::coordinate::DagPosition::root(),
u32::try_from(payload_bytes.len()).expect("payload size fits u32"),
EventKind::custom(0x1, 1),
);
let event = crate::event::Event {
header,
payload: payload_bytes,
hash_chain: Some(HashChain {
prev_hash: [0; 32],
event_hash,
}),
};
let frame_payload = FramePayload {
event,
entity: "entity:test".to_owned(),
scope: "scope:test".to_owned(),
receipt_extensions: std::collections::BTreeMap::new(),
};
let frame = frame_encode(&frame_payload).expect("encode frame");
let frame_length = u32::try_from(frame.len()).expect("frame length fits u32");
let entry = crate::store::segment::sidx::SidxEntry {
event_id: seq as u128,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash,
frame_offset,
frame_length,
global_sequence: seq,
correlation_id: seq as u128,
causation_id: 0,
};
(frame, entry)
}
fn append_untrusted_footer(
bytes: &mut Vec<u8>,
entries: &[crate::store::segment::sidx::SidxEntry],
) -> u64 {
let footer_start = bytes.len() as u64;
let mut collector = SidxEntryCollector::new();
for entry in entries.iter().cloned() {
collector
.record(entry, "entity:test", "scope:test")
.expect("intern test strings");
}
let mut cursor = Cursor::new(&mut *bytes);
cursor.seek(SeekFrom::End(0)).expect("seek to footer start");
collector
.write_footer(&mut cursor, 7)
.expect("write footer");
let corrupt_at = usize::try_from(footer_start).expect("footer_start fits usize");
bytes[corrupt_at] ^= 0xFF;
footer_start
}
#[test]
fn untrusted_entries_parse_is_crc_independent() {
let (mut bytes, e0) = {
let (f0, e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "a"}));
(f0, e0)
};
let (f1, mut e1) = corroboratable_frame(bytes.len() as u64, 2, &serde_json::json!({"v": "b"}));
e1.frame_offset = bytes.len() as u64;
bytes.extend_from_slice(&f1);
append_untrusted_footer(&mut bytes, &[e0.clone(), e1.clone()]);
let mut cursor = Cursor::new(bytes);
let parsed = read_entries_unauthenticated(&mut cursor, 7).expect("must not error");
assert_eq!(
parsed.len(),
2,
"PROPERTY: entries parse from a CRC-failed footer (decode_from is CRC-independent)"
);
assert_eq!(
parsed[0].event_hash, e0.event_hash,
"PROPERTY: parsed untrusted entry preserves the content event_hash for corroboration"
);
assert_eq!(parsed[1].frame_offset, e1.frame_offset);
}
#[test]
fn untrusted_garbage_entry_table_parses_to_zero_entries() {
let mut bytes = vec![0xA5u8; 200];
let trailer_start = bytes.len() - 16;
bytes[trailer_start..trailer_start + 8].copy_from_slice(&0u64.to_le_bytes());
bytes[trailer_start + 8..trailer_start + 12].copy_from_slice(&u32::MAX.to_le_bytes());
bytes[trailer_start + 12..trailer_start + 16]
.copy_from_slice(crate::store::segment::sidx::SIDX_MAGIC);
let mut cursor = Cursor::new(bytes);
let parsed = read_entries_unauthenticated(&mut cursor, 7).expect("must not error");
assert!(
parsed.is_empty(),
"PROPERTY: an absurd entry_count yields zero entries (fall back), never a forged manifest"
);
}
#[test]
fn resolve_untrusted_fails_closed_on_torn_last_committed_frame() {
let (f0, mut e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "first"}));
e0.frame_offset = 0;
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, mut e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "second"}));
e1.frame_offset = f1_off;
bytes.extend_from_slice(&f1);
let f2_off = bytes.len() as u64;
let (f2_full, mut e2) = corroboratable_frame(f2_off, 3, &serde_json::json!({"v": "third"}));
e2.frame_offset = f2_off;
bytes.extend_from_slice(&f2_full[..9]);
append_untrusted_footer(&mut bytes, &[e0, e1, e2]);
let file_len = bytes.len() as u64;
{
let mut old_cursor = Cursor::new(bytes.clone());
let old = crc_valid_frames_end(&mut old_cursor, 0, file_len, 7).expect(
"OLD primitive recovers the prefix (the bug): torn tail has nothing valid after",
);
assert_eq!(
old, f2_off,
"STASH-VERIFY: the manifest-blind walk recovers the prefix and silently drops the \
torn committed frame (recovery stop == start of the torn frame 2)"
);
}
let mut cursor = Cursor::new(bytes);
let result = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, true);
assert!(
matches!(
result,
Err(StoreError::CorruptSegment { segment_id: 7, .. })
),
"PROPERTY: a corroborated SIDX manifest attesting to a torn/missing last committed frame \
must FAIL CLOSED, not silently recover the prefix; got {result:?}"
);
let mut cursor2 = {
let (f0, mut e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "first"}));
e0.frame_offset = 0;
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, mut e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "second"}));
e1.frame_offset = f1_off;
bytes.extend_from_slice(&f1);
let f2_off = bytes.len() as u64;
let (f2_full, mut e2) = corroboratable_frame(f2_off, 3, &serde_json::json!({"v": "third"}));
e2.frame_offset = f2_off;
bytes.extend_from_slice(&f2_full[..9]);
append_untrusted_footer(&mut bytes, &[e0, e1, e2]);
Cursor::new(bytes)
};
let len2 = cursor2.get_ref().len() as u64;
let result_recover = resolve_untrusted_frames_end(&mut cursor2, 0, len2, 7, None, false);
assert!(
matches!(result_recover, Err(StoreError::CorruptSegment { .. })),
"PROPERTY: a corroborated missing committed frame fails closed even under \
RecoverTornTail policy (data loss is policy-independent); got {result_recover:?}"
);
}
#[test]
fn resolve_untrusted_recovers_all_when_frames_intact_and_footer_corrupt() {
let (f0, mut e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "a"}));
e0.frame_offset = 0;
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, mut e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "b"}));
e1.frame_offset = f1_off;
bytes.extend_from_slice(&f1);
let f2_off = bytes.len() as u64;
let (f2, mut e2) = corroboratable_frame(f2_off, 3, &serde_json::json!({"v": "c"}));
e2.frame_offset = f2_off;
bytes.extend_from_slice(&f2);
let frames_end = bytes.len() as u64;
append_untrusted_footer(&mut bytes, &[e0, e1, e2]);
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let recovered = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, true)
.expect("intact frames under a corrupt footer must recover, not fail closed")
.frames_end;
assert_eq!(
recovered, frames_end,
"PROPERTY: a corrupt footer over intact, corroborated frames recovers ALL committed frames"
);
}
#[test]
fn resolve_untrusted_falls_back_to_prefix_when_no_entry_corroborates() {
let (f0, _e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "a"}));
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, _e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "b"}));
bytes.extend_from_slice(&f1);
let frames_end = bytes.len() as u64;
append_untrusted_footer(&mut bytes, &[]);
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let recovered = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, false)
.expect("an empty/unparseable manifest must fall back to prefix recovery under permissive")
.frames_end;
assert_eq!(
recovered, frames_end,
"PROPERTY: with no corroborating entry, the default permissive posture falls back to the \
CRC-valid prefix (no false fail-closed)"
);
}
#[test]
fn resolve_untrusted_recovers_for_garbage_entry_table() {
let (f0, _e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "x"}));
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, _e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "y"}));
bytes.extend_from_slice(&f1);
let frames_end = bytes.len() as u64;
let mut trailer = [0u8; 16];
trailer[0..8].copy_from_slice(&frames_end.to_le_bytes());
trailer[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
trailer[12..16].copy_from_slice(crate::store::segment::sidx::SIDX_MAGIC);
bytes.extend_from_slice(&trailer);
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let recovered = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, false)
.expect("a garbage entry table must fall back to prefix recovery under permissive")
.frames_end;
assert_eq!(
recovered, frames_end,
"PROPERTY: a garbage/unparseable entry table degrades to prefix recovery under the default \
permissive posture (no false fail-closed)"
);
}
#[test]
fn resolve_untrusted_legacy_sdx2_intact_frames_recovers() {
let (f0, mut e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "a"}));
e0.frame_offset = 0;
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, mut e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "b"}));
e1.frame_offset = f1_off;
bytes.extend_from_slice(&f1);
let frames_end = bytes.len() as u64;
append_untrusted_footer(&mut bytes, &[e0, e1]);
let n = bytes.len();
bytes[n - 4..n].copy_from_slice(crate::store::segment::sidx::SIDX_MAGIC_LEGACY_SDX2);
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let recovered = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, true)
.expect("legacy SDX2 over intact frames must recover")
.frames_end;
assert_eq!(
recovered, frames_end,
"PROPERTY: a legacy SDX2 manifest corroborates intact frames and recovers them all"
);
}
#[test]
fn resolve_untrusted_still_fails_closed_on_mid_stream_corruption() {
let (bytes, frames_end) = frames_then_sdx3_footer(&["a", "b", "c", "d", "e"]);
let mut bytes = bytes;
let f2_start = frame_total_len_at(&bytes, 0);
let f3_start = f2_start + frame_total_len_at(&bytes, f2_start);
let third_payload_byte = usize::try_from(f3_start + 8).expect("offset fits usize");
assert!((third_payload_byte as u64) < frames_end);
bytes[third_payload_byte] ^= 0x01; let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let result = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, true);
assert!(
matches!(
result,
Err(StoreError::CorruptSegment { segment_id: 7, .. })
),
"PROPERTY: mid-stream corruption still fails closed via the manifest path; got {result:?}"
);
}
#[test]
fn corroborate_property_missing_trailing_frame_fails_intact_recovers() {
let recovered: RecoveredFrameMap = [
(
0u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([7u8; 32]),
},
),
(
64u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([8u8; 32]),
},
),
]
.into_iter()
.collect();
let p = 128u64;
let entry = |frame_offset: u64, frame_length: u32, event_hash: [u8; 32]| {
crate::store::segment::sidx::SidxEntry {
event_id: 1,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash,
frame_offset,
frame_length,
global_sequence: 1,
correlation_id: 1,
causation_id: 0,
}
};
let intact = vec![entry(0, 64, [7; 32]), entry(64, 64, [8; 32])];
assert_eq!(
corroborate_untrusted_entries(&intact, &recovered, p, None, true),
UntrustedRecovery::RecoverPrefix(p),
"PROPERTY: a fully-corroborated manifest with nothing past P recovers the prefix"
);
let missing = vec![entry(0, 64, [7; 32]), entry(128, 64, [9; 32])];
assert_eq!(
corroborate_untrusted_entries(&missing, &recovered, p, None, true),
UntrustedRecovery::FailClosedCorroboratedLoss,
"PROPERTY: an anchored manifest attesting to a committed frame at/after P missing from R \
always fails closed (proven loss)"
);
let forged = vec![entry(0, 64, [0xAA; 32]), entry(128, 64, [0xBB; 32])];
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, None, false),
UntrustedRecovery::RecoverPrefix(p),
"PROPERTY: an un-anchored (forged) manifest is inert under the permissive posture — fall \
back to prefix, no false fail-closed"
);
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, None, true),
UntrustedRecovery::FailClosedUnprovableTail,
"PROPERTY: under the strict FailClosed posture the same un-anchored manifest over a \
non-empty recovered prefix refuses the unprovable tail (truncation cannot be ruled out)"
);
}
#[test]
fn corroborate_runs_the_present_check_when_a_frame_sits_at_or_past_the_stop() {
let entry = |frame_offset: u64, frame_length: u32, event_hash: [u8; 32]| {
crate::store::segment::sidx::SidxEntry {
event_id: 1,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash,
frame_offset,
frame_length,
global_sequence: 1,
correlation_id: 1,
causation_id: 0,
}
};
let p = 200u64;
let recovered: RecoveredFrameMap = [
(
0u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([7u8; 32]),
},
),
(
200u64,
RecoveredFrame {
frame_length: 70,
event_hash: Some([9u8; 32]),
},
),
]
.into_iter()
.collect();
let exact = vec![entry(0, 64, [7; 32]), entry(200, 70, [9; 32])];
assert_eq!(
corroborate_untrusted_entries(&exact, &recovered, p, None, true),
UntrustedRecovery::RecoverPrefix(p),
"an anchored manifest whose entry at P matches the recovered frame's length AND content \
hash recovers the prefix; a flipped length/hash equality would falsely fail closed"
);
let hash_mismatch = vec![entry(0, 64, [7; 32]), entry(200, 70, [0xAA; 32])];
assert_eq!(
corroborate_untrusted_entries(&hash_mismatch, &recovered, p, None, true),
UntrustedRecovery::FailClosedCorroboratedLoss,
"an anchored manifest whose entry at P matches length but NOT content hash must fail \
closed; a `&&`→`||` or a hash `==`→`!=` mutation would falsely recover"
);
}
fn two_intact_frames_then_untrusted_empty_footer() -> (Vec<u8>, u64) {
let (f0, _e0) = corroboratable_frame(0, 1, &serde_json::json!({"v": "a"}));
let mut bytes = f0;
let f1_off = bytes.len() as u64;
let (f1, _e1) = corroboratable_frame(f1_off, 2, &serde_json::json!({"v": "b"}));
bytes.extend_from_slice(&f1);
let frames_end = bytes.len() as u64;
append_untrusted_footer(&mut bytes, &[]);
(bytes, frames_end)
}
#[test]
fn strict_untrusted_footer_nonempty_prefix_no_corroboration_fails_closed() {
let (bytes, frames_end) = two_intact_frames_then_untrusted_empty_footer();
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let result = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, true);
assert!(
matches!(
&result,
Err(StoreError::CorruptSegment { segment_id: 7, .. })
),
"PROPERTY: strict posture must REFUSE an unprovable non-empty tail with \
CorruptSegment{{segment_id:7}}, not recover ({frames_end}); got {result:?}"
);
let detail = match result {
Err(StoreError::CorruptSegment { detail, .. }) => detail,
_ => String::new(),
};
assert!(
detail.contains("unprovable tail") && detail.contains("NO corroborating"),
"the strict refusal must carry the distinct unprovable-tail detail (not the \
corroborated-loss message); got detail: {detail}"
);
}
#[test]
fn permissive_untrusted_footer_nonempty_prefix_no_corroboration_recovers() {
let (bytes, frames_end) = two_intact_frames_then_untrusted_empty_footer();
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let recovered = resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, None, false)
.expect("LEG 2: the default permissive posture must recover a benign corrupt-footer store")
.frames_end;
assert_eq!(
recovered, frames_end,
"PROPERTY: the default posture recovers the entire CRC-valid prefix (both intact frames) — \
the DEFAULT path is unchanged by the feature"
);
}
#[test]
fn corroborated_proven_loss_fails_closed_under_both_policies() {
let recovered: RecoveredFrameMap = [(
0u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([7u8; 32]),
},
)]
.into_iter()
.collect();
let p = 64u64; let entry = |frame_offset: u64, frame_length: u32, event_hash: [u8; 32]| {
crate::store::segment::sidx::SidxEntry {
event_id: 1,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash,
frame_offset,
frame_length,
global_sequence: 1,
correlation_id: 1,
causation_id: 0,
}
};
let manifest = vec![entry(0, 64, [7; 32]), entry(64, 64, [9; 32])];
for fallback_fail_closed in [false, true] {
assert_eq!(
corroborate_untrusted_entries(&manifest, &recovered, p, None, fallback_fail_closed),
UntrustedRecovery::FailClosedCorroboratedLoss,
"PROPERTY: a corroborated missing committed frame is proven loss and fails closed \
regardless of tail policy (fallback_fail_closed = {fallback_fail_closed})"
);
}
}
fn non_corroborating_prefix_fixture() -> (
RecoveredFrameMap,
Vec<crate::store::segment::sidx::SidxEntry>,
u64,
) {
let recovered: RecoveredFrameMap = [
(
0u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([7u8; 32]),
},
),
(
64u64,
RecoveredFrame {
frame_length: 64,
event_hash: Some([8u8; 32]),
},
),
]
.into_iter()
.collect();
let entry = |frame_offset: u64, frame_length: u32, event_hash: [u8; 32]| {
crate::store::segment::sidx::SidxEntry {
event_id: 1,
entity_idx: 0,
scope_idx: 0,
kind: kind_to_raw(EventKind::custom(0x1, 1)),
wall_ms: 1,
clock: 1,
dag_lane: 0,
dag_depth: 0,
prev_hash: [0; 32],
event_hash,
frame_offset,
frame_length,
global_sequence: 1,
correlation_id: 1,
causation_id: 0,
}
};
let forged = vec![entry(0, 64, [0xAA; 32]), entry(64, 64, [0xBB; 32])];
(recovered, forged, 128)
}
#[test]
fn corroborate_footer_claimed_past_prefix_yields_truncation_evidence() {
let (recovered, forged, p) = non_corroborating_prefix_fixture();
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, Some(p + 64), true),
UntrustedRecovery::FailClosedEvidenceOfTruncation {
footer_claimed_end: p + 64,
},
"strict posture with a footer claiming frames past P must fail closed with POSITIVE \
truncation evidence, not the mere-absence unprovable-tail variant"
);
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, Some(p + 64), false),
UntrustedRecovery::RecoverPrefixWithTruncationEvidence {
end: p,
footer_claimed_end: p + 64,
},
"permissive posture recovers the prefix but records the footer-cross-checked truncation \
evidence (end = P, footer_claimed_end past P) for the caller"
);
}
#[test]
fn corroborate_footer_claimed_at_or_absent_is_not_truncation_evidence() {
let (recovered, forged, p) = non_corroborating_prefix_fixture();
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, Some(p), true),
UntrustedRecovery::FailClosedUnprovableTail,
"footer claiming frames end exactly AT P is a clean boundary, not truncation evidence \
(the gap guard is `<`, not `<=`)"
);
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, None, true),
UntrustedRecovery::FailClosedUnprovableTail,
"with no footer-claimed-end hint the strict posture keeps its pre-existing \
unprovable-tail refusal"
);
assert_eq!(
corroborate_untrusted_entries(&forged, &recovered, p, Some(p), false),
UntrustedRecovery::RecoverPrefix(p),
"a clean at-P boundary under the permissive posture recovers the plain prefix with no \
truncation evidence"
);
}
#[test]
fn resolve_out_of_bounds_footer_claim_degrades_to_unprovable_tail_not_evidence() {
let (bytes, frames_end) = two_intact_frames_then_untrusted_empty_footer();
let file_len = bytes.len() as u64;
let mut cursor = Cursor::new(bytes);
let result =
resolve_untrusted_frames_end(&mut cursor, 0, file_len, 7, Some(frames_end + 64), true);
assert!(
matches!(
&result,
Err(StoreError::CorruptSegment { segment_id: 7, .. })
),
"PROPERTY: strict posture must REFUSE an out-of-bounds untrusted footer with \
CorruptSegment{{segment_id:7}}; got {result:?}"
);
let detail = match result {
Err(StoreError::CorruptSegment { detail, .. }) => detail,
_ => String::new(),
};
assert!(
detail.contains("unprovable tail"),
"an OUT-OF-BOUNDS footer claim is forgery, not truncation evidence: the strict refusal must \
carry the absence-only unprovable-tail detail, NOT the POSITIVE-truncation-evidence one; \
got detail: {detail}"
);
}