//! Branching in-memory session state for fixtures and effective agent context //! restored through bounded durable reads (`SMH-SPEC-SPEC0001`, Sessions). //! //! Entry content is typed binary bytes; only arbitrary JSON payloads use //! canonical CBOR. A record carries traversal metadata ([`EntryKind`]) and //! views borrow validated resident spans. use crate::content_codec::EntryContentView; use crate::frame::Frame; use serde::{Deserialize, Serialize}; use smith::error::{MutationState, Result, SmithError}; use smith::id::{BranchId, EntryId, MessageId, SessionId, ToolCallId}; use smith::message::{ContentBlock, Message, Role}; use smith::structured::{AccessError, StructuredData, ToolArguments}; use std::collections::HashMap; /// Meta entry kind that marks a compaction boundary. pub const COMPACTION_KIND: &str = "compaction"; /// Metadata kind for one redacted provider HTTP effect record. pub const PROVIDER_HTTP_KIND: &str = "provider-http"; /// Metadata kind for a tool completion's typed mutation outcome. pub const TOOL_MUTATION_KIND: &str = "tool-mutation"; fn structured_depth_error(error: AccessError) -> SmithError { match error { AccessError::DepthExceeded { .. } => SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: format!( "JSON nesting exceeds {} containers; reduce nested arrays and objects to this depth or less", crate::json_scan::JSON_DEPTH_MAX ), }, AccessError::Malformed => SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: "malformed structured data".to_string(), }, } } /// Durable session identity, persisted as the first frame of a session file /// so reload reproduces the same IDs instead of minting new ones. #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct SessionHeader { /// Session identifier. pub session_id: SessionId, /// Identifier of the root branch. pub branch_id: BranchId, } /// Byte range of one entry's content in its session's payload arena. /// /// `u32` offsets cap one session arena at 4 GiB. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Span { /// First content byte. pub offset: u32, /// Content length in bytes. pub len: u32, } impl Span { /// The span of `len` bytes at `offset`, or an error past the arena cap. fn at(offset: usize, len: usize) -> Result { let full = || SmithError::Session { code: smith::error::SessionCode::SessionArenaFull, message: format!("entry content at {offset}+{len} exceeds the 4 GiB session arena"), }; let end = u32::try_from(offset.saturating_add(len)).map_err(|_| full())?; let offset = u32::try_from(offset).map_err(|_| full())?; Ok(Self { offset, len: end - offset, }) } const fn end(self) -> usize { self.offset as usize + self.len as usize } const fn range(self) -> std::ops::Range { self.offset as usize..self.end() } } /// What an entry recorded, reduced to the identifiers traversal pairs on. #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum EntryKind { /// Chat message. Message { /// Message identifier. id: MessageId, /// Who produced the message. role: Role, }, /// Tool call. ToolCall { /// Identifier pairing the call with its result. call_id: ToolCallId, }, /// Tool result. ToolResult { /// Identifier of the call this result answers. call_id: ToolCallId, /// Whether the effect succeeded. ok: bool, }, /// Session metadata. Meta { /// Whether the entry marks a compaction boundary. compaction: bool, }, } /// A fixed-size session entry record; its content lives in the session /// payload arena. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Entry { /// Entry ID. pub id: EntryId, /// Parent entry ID (None for root). pub parent: Option, /// Timestamp (Unix ms). pub timestamp_ms: i64, /// What the entry recorded. pub kind: EntryKind, /// Typed binary [`EntryContent`] bytes in the session arena. pub content: Span, } impl Entry { /// Whether this entry marks a compaction boundary. #[must_use] pub const fn is_compaction(&self) -> bool { matches!(self.kind, EntryKind::Meta { compaction: true }) } } /// Decoded frame metadata with opaque typed binary content bytes. #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct EntryFrame { /// Entry ID. pub id: EntryId, /// Parent entry ID (None for root). pub parent: Option, /// Timestamp (Unix ms). pub timestamp_ms: i64, /// What the entry recorded. pub kind: EntryKind, /// Typed binary [`EntryContent`] bytes. pub content: Vec, } impl EntryFrame { /// A frame for `content` with a fresh identity, outside any session. /// /// # Errors /// /// Returns [`SmithError::Session`] when the content cannot be encoded. pub fn new(parent: Option, content: &EntryContent) -> Result { let mut bytes = Vec::new(); content.encode_into(&mut bytes)?; Ok(Self { id: EntryId::new(), parent, timestamp_ms: now_ms(), kind: content.kind(), content: bytes, }) } } /// What one entry recorded; the decoded view of an entry's content. #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[serde(tag = "kind", content = "data")] pub enum EntryContent { /// Chat message. Message(Message), /// Tool call (recorded for replay). ToolCall { /// Invoked tool. name: String, /// Canonical tool arguments. input: ToolArguments, /// Identifier pairing this call with its result. call_id: ToolCallId, }, /// Tool result. ToolResult { /// Whether the effect succeeded. ok: bool, /// Recorded output or error text. output: String, /// Identifier of the call this result answers. call_id: ToolCallId, }, /// Session metadata event (compaction, branch selection). Meta { /// Metadata kind, for example [`COMPACTION_KIND`]. kind: String, /// Kind-specific payload. detail: StructuredData, }, } impl EntryContent { /// The record kind of this content. #[must_use] pub fn kind(&self) -> EntryKind { EntryContentRef::from(self).kind() } /// Decode content through the validated borrowed binary parser. /// /// # Errors /// /// Returns [`SmithError::Session`] when `bytes` are not entry content. pub fn decode(bytes: &[u8]) -> Result { EntryContentView::decode(bytes)?.to_owned() } /// Append versioned typed binary content to `out`. fn encode_into(&self, out: &mut Vec) -> Result<()> { EntryContentRef::from(self).encode_into(out) } } /// Recording-time borrowed Rust content, distinct from the validated /// decoded-from-bytes [`EntryContentView`]. #[derive(Clone, Copy, Debug, Serialize)] #[serde(tag = "kind", content = "data")] pub enum EntryContentRef<'a> { /// Chat message. Message(&'a Message), /// Tool call. ToolCall { /// Invoked tool. name: &'a str, /// Tool arguments. input: &'a ToolArguments, /// Identifier pairing this call with its result. call_id: ToolCallId, }, /// Tool result. ToolResult { /// Whether the effect succeeded. ok: bool, /// Output or error text. output: &'a str, /// Identifier of the call this result answers. call_id: ToolCallId, }, /// Session metadata event. Meta { /// Metadata kind. kind: &'a str, /// Kind-specific payload. detail: &'a StructuredData, }, } impl<'a> From<&'a EntryContent> for EntryContentRef<'a> { fn from(content: &'a EntryContent) -> Self { match content { EntryContent::Message(message) => Self::Message(message), EntryContent::ToolCall { name, input, call_id, } => Self::ToolCall { name, input, call_id: *call_id, }, EntryContent::ToolResult { ok, output, call_id, } => Self::ToolResult { ok: *ok, output, call_id: *call_id, }, EntryContent::Meta { kind, detail } => Self::Meta { kind, detail }, } } } impl EntryContentRef<'_> { /// The record kind of this content. #[must_use] pub fn kind(&self) -> EntryKind { match *self { Self::Message(message) => EntryKind::Message { id: message.id, role: message.role, }, Self::ToolCall { call_id, .. } => EntryKind::ToolCall { call_id }, Self::ToolResult { call_id, ok, .. } => EntryKind::ToolResult { call_id, ok }, Self::Meta { kind, .. } => EntryKind::Meta { compaction: kind == COMPACTION_KIND, }, } } /// Append typed binary content to `out`; structured payloads are /// re-ordered into canonical key order only when needed. fn encode_into(&self, out: &mut Vec) -> Result<()> { if let Self::Meta { kind, detail } = *self { crate::metadata::validate(kind, detail.view()).map_err(structured_depth_error)?; } let canonical_arguments: ToolArguments; let canonical_data: StructuredData; let canonical_message: Message; let view: EntryContentRef<'_> = match *self { Self::ToolCall { name, input, call_id, } if !input .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)? => { canonical_arguments = input .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)?; EntryContentRef::ToolCall { name, input: &canonical_arguments, call_id, } } Self::Meta { kind, detail } if !detail .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)? => { canonical_data = detail .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)?; EntryContentRef::Meta { kind, detail: &canonical_data, } } Self::Message(message) if !message_is_canonical(message)? => { canonical_message = canonical_message_of(message)?; EntryContentRef::Message(&canonical_message) } other => other, }; crate::content_codec::encode(&view, out) } } fn message_is_canonical(message: &Message) -> Result { for block in &message.blocks { if let ContentBlock::ToolUse { input, .. } = block && !input .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)? { return Ok(false); } } Ok(true) } fn canonical_message_of(message: &Message) -> Result { let mut blocks = Vec::with_capacity(message.blocks.len()); for block in &message.blocks { blocks.push(match block { ContentBlock::ToolUse { name, input, call_id, } => ContentBlock::ToolUse { name: name.clone(), input: input .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .map_err(structured_depth_error)?, call_id: *call_id, }, ContentBlock::Text(text) => ContentBlock::Text(text.clone()), ContentBlock::Thinking(thinking) => ContentBlock::Thinking(thinking.clone()), ContentBlock::ToolResult { call_id, ok, output, } => ContentBlock::ToolResult { call_id: *call_id, ok: *ok, output: output.clone(), }, }); } Ok(Message { id: message.id, role: message.role, blocks, timestamp_ms: message.timestamp_ms, }) } /// Wall-clock milliseconds since the Unix epoch, saturating on overflow. fn now_ms() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |elapsed| { i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX) }) } /// One root-to-leaf chain of a session. /// /// Only its [`Session`] mutates a branch, so every record's span points into /// that session's arena. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Branch { id: BranchId, entries: Vec, selected_index: usize, } impl Branch { const fn with_id(id: BranchId) -> Self { Self { id, entries: Vec::new(), selected_index: 0, } } /// Branch identity. #[must_use] pub const fn id(&self) -> BranchId { self.id } /// Entry records ordered root to leaf. #[must_use] pub fn entries(&self) -> &[Entry] { &self.entries } /// Append `entry` and select it. #[expect( clippy::large_types_passed_by_value, reason = "Entry ownership moves directly into the resident branch vector" )] fn append(&mut self, entry: Entry) -> &Entry { self.selected_index = self.entries.len(); self.entries.push(entry); &self.entries[self.selected_index] } /// The currently selected entry, if the branch has any. #[must_use] pub fn selected_entry(&self) -> Option<&Entry> { self.entries.get(self.selected_index) } } /// A session is a persistent, branching message/tool history. /// /// Fields stay private so the invariants hold: at least one branch, a valid /// active index, every span inside the arena, and a compaction boundary /// that names a recorded compaction entry. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Session { id: SessionId, /// All branches (default branch is index 0). branches: Vec, /// Typed binary content of every entry, addressed by [`Span`]. payloads: Vec, active_branch_index: usize, compaction_boundary: Option, } impl Default for Session { fn default() -> Self { Self::new() } } impl Session { /// A new session with one empty branch. #[must_use] pub fn new() -> Self { Self::new_with_id(SessionId::new()) } pub(crate) fn new_with_id(id: SessionId) -> Self { Self { id, branches: vec![Branch::with_id(BranchId::new())], payloads: Vec::new(), active_branch_index: 0, compaction_boundary: None, } } /// Session identifier. #[must_use] pub const fn id(&self) -> SessionId { self.id } /// All branches; the active one is [`Session::active_branch`]. #[must_use] pub fn branches(&self) -> &[Branch] { &self.branches } /// The latest compaction boundary entry, if any. #[must_use] pub const fn compaction_boundary(&self) -> Option { self.compaction_boundary } /// The selected branch. #[must_use] pub fn active_branch(&self) -> &Branch { &self.branches[self.active_branch_index] } /// Identifier of the selected entry, if any. #[must_use] pub fn selected_id(&self) -> Option { self.active_branch().selected_entry().map(|e| e.id) } /// Encode `content` once into the arena and append its record to the /// selected branch, selecting it. A compaction entry becomes the /// compaction boundary. /// /// # Errors /// /// Returns [`SmithError::Session`] when the content cannot be encoded or /// the arena is full; the session is unchanged then. pub fn append<'a>(&mut self, content: impl Into>) -> Result<&Entry> { self.append_with(content, |_, _| Ok(())) } /// [`Session::append`], running `persist` on the new record and its /// content bytes before the entry becomes part of the session. /// /// # Errors /// /// As [`Session::append`], or the error of `persist`; the session is /// unchanged then. pub fn append_with<'a>( &mut self, content: impl Into>, persist: impl FnOnce(&Entry, &[u8]) -> Result<()>, ) -> Result<&Entry> { let content = content.into(); let parent = self.selected_id(); let offset = self.payloads.len(); let staged = content .encode_into(&mut self.payloads) .and_then(|()| Span::at(offset, self.payloads.len() - offset)) .map(|span| Entry { id: EntryId::new(), parent, timestamp_ms: now_ms(), kind: content.kind(), content: span, }) .and_then(|entry| persist(&entry, &self.payloads[offset..]).map(|()| entry)); let entry = match staged { Ok(entry) => entry, Err(error) => { self.payloads.truncate(offset); return Err(error); } }; if entry.is_compaction() { self.compaction_boundary = Some(entry.id); } Ok(self.branches[self.active_branch_index].append(entry)) } /// The typed binary content bytes of `entry`; empty when `entry` does /// not belong to this session. #[must_use] pub fn bytes(&self, entry: &Entry) -> &[u8] { self.payloads.get(entry.content.range()).unwrap_or_default() } /// Decode the content of `entry` into the caller's scope, verifying it /// against the record's [`EntryKind`], which traversal trusts unchecked. /// /// # Errors /// /// Returns [`SmithError::Session`] with code `ENTRY_DECODE` when the /// bytes do not decode, or `ENTRY_KIND_MISMATCH` when they decode to /// content of another kind than the record claims. pub fn content(&self, entry: &Entry) -> Result { let content = EntryContent::decode(self.bytes(entry))?; let found = content.kind(); if found != entry.kind { return Err(SmithError::Session { code: smith::error::SessionCode::EntryKindMismatch, message: format!( "entry {} records {:?} but holds {found:?}", entry.id, entry.kind ), }); } Ok(content) } /// Best-known mutation state from selected durable history. /// /// A legacy result without completion metadata reconstructs as `Unknown`. /// /// # Errors /// /// Returns `ENTRY_DECODE` for malformed completion metadata. pub fn mutation_state(&self, call_id: ToolCallId) -> Result { for entry in self.active_branch().entries().iter().rev() { let EntryContent::Meta { kind, detail } = self.content(entry)? else { continue; }; if kind != TOOL_MUTATION_KIND { continue; } crate::metadata::validate(&kind, detail.view()).map_err(|_| SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: "invalid tool mutation state metadata".to_string(), })?; let detail: crate::metadata::ToolMutationMetadata = detail.deserialize_as().map_err(|_| SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: "invalid tool mutation state metadata".to_string(), })?; if detail.call_id == call_id { return Ok(detail.mutation_state); } } Ok(MutationState::Unknown) } /// The root-to-`at` entry chain, or `None` when `at` is unknown or its /// ancestry is broken. /// /// A branch stores its root-to-leaf chain in order, so the chain is the /// branch prefix ending at `at`; each parent link is verified against the /// preceding entry and no lookup table is built. fn ancestry(&self, at: EntryId) -> Option<&[Entry]> { let (branch, index) = self.branches.iter().find_map(|branch| { let index = branch.entries.iter().position(|entry| entry.id == at)?; Some((branch, index)) })?; let chain = &branch.entries[..=index]; let rooted = chain.first().is_some_and(|root| root.parent.is_none()); let linked = chain .windows(2) .all(|pair| pair[1].parent == Some(pair[0].id)); (rooted && linked).then_some(chain) } /// Fork the history ending at `at` into a new session. /// /// Entry IDs, order, and timestamps are preserved, `at` becomes the /// selected leaf, and the compaction boundary carries over when it lies /// on the retained path. The new session gets fresh session and branch /// identity, because it is a different durable history. The retained /// records are copied with the one arena prefix their spans cover, so /// spans stay valid unchanged. /// /// # Errors /// /// Returns [`SmithError::Session`] when `at` is not part of this session. pub fn fork_at(&self, at: EntryId) -> Result { let entries = self.ancestry(at).ok_or_else(|| SmithError::Session { code: smith::error::SessionCode::EntryNotFound, message: format!("entry {at} is not part of session {}", self.id), })?; let boundary = self .compaction_boundary .filter(|id| entries.iter().any(|entry| entry.id == *id)); let end = entries .iter() .map(|entry| entry.content.end()) .max() .unwrap_or(0); Ok(Self { id: SessionId::new(), branches: vec![Branch { id: BranchId::new(), entries: entries.to_vec(), selected_index: entries.len().saturating_sub(1), }], payloads: self.payloads.get(..end).unwrap_or_default().to_vec(), active_branch_index: 0, compaction_boundary: boundary, }) } /// Keep only the selected compaction entry as live context. /// /// Full ancestry, sibling branches, and their payloads remain durable in /// the session file; live agent context resumes from this boundary. pub(crate) fn retain_compacted_context(&mut self) { let Some(entry) = self.active_branch().selected_entry().copied() else { return; }; if !entry.is_compaction() || self.compaction_boundary != Some(entry.id) { return; } let branch_id = self.active_branch().id; let payloads = self.bytes(&entry).to_vec(); self.branches = vec![Branch { id: branch_id, entries: vec![Entry { content: Span { offset: 0, len: entry.content.len, }, ..entry }], selected_index: 0, }]; self.payloads = payloads; self.active_branch_index = 0; } /// The durable identity of this session's active branch. #[must_use] pub fn header(&self) -> SessionHeader { SessionHeader { session_id: self.id, branch_id: self.active_branch().id, } } /// Restore a selected chain from owned entry records. /// A chain may start at a compaction boundary with an older parent ID; /// historical content stays on disk. Unrelated branches are not retained. /// /// # Errors /// Returns a session error for broken ancestry or an arena overflow. pub fn from_chain(header: SessionHeader, entries: Vec) -> Result { let mut session = Self { id: header.session_id, branches: vec![Branch::with_id(header.branch_id)], payloads: Vec::new(), active_branch_index: 0, compaction_boundary: None, }; for entry in entries { session.push_restored(entry)?; } Ok(session) } /// Append one chronological restored entry without retaining a second /// full-history payload buffer. pub(crate) fn push_restored(&mut self, entry: EntryFrame) -> Result<()> { if entry.parent != self.selected_id() && !(self.selected_id().is_none() && entry.kind == (EntryKind::Meta { compaction: true })) { return Err(SmithError::Session { code: smith::error::SessionCode::SessionParent, message: format!("entry {} has broken ancestry", entry.id), }); } let span = Span::at(self.payloads.len(), entry.content.len())?; self.payloads.extend(entry.content); let record = Entry { id: entry.id, parent: entry.parent, timestamp_ms: entry.timestamp_ms, kind: entry.kind, content: span, }; if record.is_compaction() { self.compaction_boundary = Some(record.id); } self.branches[0].append(record); Ok(()) } /// Build in-memory fixture state from already-decoded frames. /// /// Durable file reopening uses [`crate::store::SessionReader`] instead. /// A header frame restores the session and branch identity, so state /// rebuilt from the same file is identical on every load. Stored parent /// links define the branches: each entry with no children is a leaf, and /// its root-to-leaf chain is one branch. The last selection record wins; /// otherwise the last stored entry is selected. Unknown frames are skipped, /// and compaction entries restore the compaction boundary. /// /// Content bytes are copied into the arena, never decoded; every /// collection is sized up front, so a linear history restores with a /// constant number of allocations. /// /// # Errors /// /// Returns [`SmithError::Session`] when the content exceeds the arena cap, /// a header is duplicated, or a header is not the first frame. pub fn from_frames(frames: &[Frame]) -> Result { let mut header = None; let (mut count, mut total) = (0, 0); let mut selection = None; for (index, frame) in frames.iter().enumerate() { match frame { Frame::Header { header: found } if header.is_some() => { return Err(SmithError::Session { code: smith::error::SessionCode::SessionHeaderDuplicate, message: "session file contains more than one header frame".to_string(), }); } Frame::Header { .. } if index != 0 => { return Err(SmithError::Session { code: smith::error::SessionCode::SessionHeaderNotFirst, message: "session header must be the first frame".to_string(), }); } Frame::Header { header: found } => header = Some(*found), Frame::Known { entry } => { count += 1; total += entry.content.len(); selection = None; } Frame::Selection { id } => selection = Some(*id), Frame::Unknown { .. } | Frame::Index { .. } | Frame::Checkpoint { .. } => {} } } Span::at(0, total)?; let mut payloads = Vec::with_capacity(total); let mut stored = Vec::with_capacity(count); let mut boundary = None; for frame in frames { let Frame::Known { entry } = frame else { continue; }; let record = Entry { id: entry.id, parent: entry.parent, timestamp_ms: entry.timestamp_ms, kind: entry.kind, content: Span::at(payloads.len(), entry.content.len())?, }; payloads.extend_from_slice(&entry.content); if record.is_compaction() { boundary = Some(record.id); } stored.push(record); } let header = header.unwrap_or_else(|| SessionHeader { session_id: SessionId::new(), branch_id: BranchId::new(), }); if let Some(id) = selection && !stored.iter().any(|entry| entry.id == id) { return Err(SmithError::Session { code: smith::error::SessionCode::EntryNotFound, message: format!("selected entry {id} is missing"), }); } let (branches, active_branch_index) = restore_branches(&stored, header.branch_id, selection); Ok(Self { id: header.session_id, branches, payloads, active_branch_index, compaction_boundary: boundary, }) } } /// Rebuild branches from stored records and their parent links, returning /// them with the index of the active branch. #[expect( clippy::large_types_passed_by_value, reason = "Copy selection ID is compared once while restoring branch metadata" )] fn restore_branches( stored: &[Entry], active_id: BranchId, selection: Option, ) -> (Vec, usize) { let Some(last) = stored.last() else { return (vec![Branch::with_id(active_id)], 0); }; // Entry id to its index and whether any stored entry names it as parent. let mut nodes: HashMap = HashMap::with_capacity(stored.len()); for (index, entry) in stored.iter().enumerate() { nodes.insert(entry.id, (index, false)); } for parent in stored.iter().filter_map(|entry| entry.parent) { if let Some(node) = nodes.get_mut(&parent) { node.1 = true; } } let nodes = &nodes; let is_leaf = |entry: &&Entry| nodes.get(&entry.id).is_some_and(|node| !node.1); // Root-ward walk; a missing parent ends the recoverable chain, and the // step cap ends a corrupt cycle. let chain = |leaf: &Entry| { let mut cursor = Some(leaf.id); std::iter::from_fn(move || { let (index, _) = nodes.get(&cursor?)?; let entry = stored.get(*index)?; cursor = entry.parent; Some(*entry) }) .take(stored.len()) }; let mut branches = Vec::with_capacity(stored.iter().filter(is_leaf).count()); let selected = selection.unwrap_or(last.id); let active_leaf = selection.map_or(Some(last.id), |selected| { stored .iter() .filter(is_leaf) .find(|leaf| leaf.id == selected) .or_else(|| { stored.iter().filter(is_leaf).find(|leaf| { leaf.id == last.id && chain(leaf).any(|entry| entry.id == selected) }) }) .or_else(|| { stored .iter() .filter(is_leaf) .find(|leaf| chain(leaf).any(|entry| entry.id == selected)) }) .map(|leaf| leaf.id) }); let mut active_branch_index = 0; for leaf in stored.iter().filter(is_leaf) { let mut entries = Vec::with_capacity(chain(leaf).count()); entries.extend(chain(leaf)); entries.reverse(); let is_active = active_leaf == Some(leaf.id); if is_active { active_branch_index = branches.len(); } let selected_index = if is_active { entries .iter() .position(|entry| entry.id == selected) .unwrap_or_else(|| entries.len().saturating_sub(1)) } else { entries.len().saturating_sub(1) }; branches.push(Branch { // The selected branch owns the persisted identity; sibling // branches derive theirs from their leaf, which keeps every // reload of the same file identical. id: if is_active { active_id } else { BranchId::from_u128(leaf.id.as_u128()) }, selected_index, entries, }); } (branches, active_branch_index) } #[cfg(test)] #[expect( clippy::unwrap_used, clippy::panic, reason = "tests may panic on invariant violations" )] mod tests { use super::*; fn message(text: &str) -> EntryContent { EntryContent::Message(Message::with_text(Role::User, text)) } fn compaction(detail: serde_json::Value) -> EntryContent { EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable(&detail).unwrap(), } } fn text_of(session: &Session, entry: &Entry) -> String { match session.content(entry).unwrap() { EntryContent::Message(m) => format!("{}", m.blocks.len()), EntryContent::Meta { kind, .. } => kind, other => panic!("unexpected content {other:?}"), } } /// The frame of every record in `entries`, content copied from `session`. fn frames_of(session: &Session, entries: &[Entry]) -> Vec { let mut frames = vec![Frame::Header { header: session.header(), }]; frames.extend(entries.iter().map(|entry| Frame::Known { entry: EntryFrame { id: entry.id, parent: entry.parent, timestamp_ms: entry.timestamp_ms, kind: entry.kind, content: session.bytes(entry).to_vec(), }, })); frames } fn frames(session: &Session) -> Vec { frames_of(session, &session.active_branch().entries) } /// Frames of entries built outside any session, under a fresh header; /// the only way to store parent links a session would not append. fn stored(entries: &[EntryFrame]) -> (SessionHeader, Vec) { let header = SessionHeader { session_id: SessionId::new(), branch_id: BranchId::new(), }; let mut frames = vec![Frame::Header { header }]; frames.extend(entries.iter().cloned().map(|entry| Frame::Known { entry })); (header, frames) } fn ids(branch: &Branch) -> Vec { branch.entries.iter().map(|entry| entry.id).collect() } #[test] fn restoring_rejects_duplicate_headers() { let header = Session::new().header(); let error = Session::from_frames(&[Frame::Header { header }, Frame::Header { header }]) .unwrap_err(); assert_eq!(error.code(), "SESSION_HEADER_DUPLICATE"); } #[test] fn restoring_rejects_a_header_after_an_entry() { let entry = EntryFrame::new(None, &message("before header")).unwrap(); let error = Session::from_frames(&[ Frame::Known { entry }, Frame::Header { header: Session::new().header(), }, ]) .unwrap_err(); assert_eq!(error.code(), "SESSION_HEADER_NOT_FIRST"); } #[test] fn selection_frame_restores_ancestor_without_minting_identity() { let root = EntryFrame::new(None, &message("root")).unwrap(); let child = EntryFrame::new(Some(root.id), &message("child")).unwrap(); let (header, mut frames) = stored(&[root.clone(), child]); frames.push(Frame::Selection { id: root.id }); let restored = Session::from_frames(&frames).unwrap(); assert_eq!(restored.selected_id(), Some(root.id)); assert_eq!(restored.active_branch().id(), header.branch_id); assert_eq!( restored .fork_at(root.id) .unwrap() .active_branch() .entries() .len(), 1 ); } #[test] fn restoring_rejects_selection_of_missing_entry() { let (_, mut frames) = stored(&[EntryFrame::new(None, &message("root")).unwrap()]); frames.push(Frame::Selection { id: EntryId::new() }); assert_eq!( Session::from_frames(&frames).unwrap_err().code(), "ENTRY_NOT_FOUND" ); } #[test] fn sibling_entries_restore_as_separate_branches() { // Stored parent links record two children of one root. let root = EntryFrame::new(None, &message("root")).unwrap(); let first = EntryFrame::new(Some(root.id), &message("first")).unwrap(); let second = EntryFrame::new(Some(root.id), &message("second")).unwrap(); let (header, stored) = stored(&[root.clone(), first.clone(), second.clone()]); let session = Session::from_frames(&stored).unwrap(); assert_eq!(session.branches.len(), 2); // The last stored entry selects the active branch and keeps the // persisted branch identity. assert_eq!(session.selected_id(), Some(second.id)); assert_eq!(session.active_branch().id, header.branch_id); assert_eq!(ids(session.active_branch()), vec![root.id, second.id]); let sibling = &session.branches[1 - session.active_branch_index]; assert_eq!(ids(sibling), vec![root.id, first.id]); // Shared-prefix records point at the same content bytes. assert_eq!( session.content(&sibling.entries[0]).unwrap(), EntryContent::decode(&root.content).unwrap() ); // Reconstruction is identical on a second pass, sibling ids included. let again = Session::from_frames(&stored).unwrap(); assert_eq!(again, session); } #[test] fn no_selection_restores_last_branch_and_compaction_boundary() { let root = EntryFrame::new(None, &message("root")).unwrap(); let first = EntryFrame::new(Some(root.id), &message("first")).unwrap(); let boundary = EntryFrame::new( Some(first.id), &compaction(serde_json::json!({"summary": ""})), ) .unwrap(); let sibling = EntryFrame::new(Some(root.id), &message("sibling")).unwrap(); let sibling_id = sibling.id; let last = EntryFrame::new(Some(sibling_id), &message("last")).unwrap(); let (header, stored) = stored(&[root.clone(), first, boundary.clone(), sibling, last.clone()]); let restored = Session::from_frames(&stored).unwrap(); assert_eq!(restored.selected_id(), Some(last.id)); assert_eq!(restored.active_branch().id, header.branch_id); assert_eq!( ids(restored.active_branch()), vec![root.id, sibling_id, last.id] ); assert_eq!(restored.compaction_boundary, Some(boundary.id)); } #[test] fn legacy_tool_result_without_mutation_metadata_is_unknown() { let mut session = Session::new(); let call_id = ToolCallId::new(); session .append(&EntryContent::ToolResult { ok: true, output: "legacy".to_string(), call_id, }) .unwrap(); assert_eq!( session.mutation_state(call_id).unwrap(), MutationState::Unknown ); } #[test] fn binary_content_borrows_text_and_decodes_all_variants() { let id = ToolCallId::new(); let mut message = Message::with_text(Role::Assistant, "text"); message.add_block(ContentBlock::Thinking("thought".into())); message.add_block(ContentBlock::tool_use( "write", ToolArguments::from_serializable(&serde_json::json!({"n": -1, "ok": true})).unwrap(), id, )); message.add_block(ContentBlock::tool_result(id, false, "failed")); for content in [ EntryContent::Message(message), EntryContent::ToolCall { name: "write".into(), input: ToolArguments::from_serializable(&serde_json::json!([null, 1.5, u64::MAX])) .unwrap(), call_id: id, }, EntryContent::ToolResult { ok: false, output: "output".into(), call_id: id, }, EntryContent::Meta { kind: "event".into(), detail: StructuredData::from_serializable( &serde_json::json!({"nested": [1, false]}), ) .unwrap(), }, ] { let bytes = EntryFrame::new(None, &content).unwrap().content; assert_eq!(EntryContent::decode(&bytes).unwrap(), content); assert_eq!( serde_json::to_value(EntryContentView::decode(&bytes).unwrap()).unwrap(), serde_json::to_value(&content).unwrap() ); if let EntryContentView::ToolResult { output, .. } = EntryContentView::decode(&bytes).unwrap() { assert_eq!(output.as_ptr(), bytes[6..].as_ptr()); } } } #[test] fn structured_payload_rejects_trailing_cbor_and_invalid_nested_values() { let content = EntryContent::Meta { kind: "k".into(), detail: StructuredData::from_serializable(&serde_json::json!([1, true])).unwrap(), }; let mut invalid = EntryFrame::new(None, &content).unwrap().content; let len_at = 1 + 4 + 1; let length = u32::from_le_bytes(invalid[len_at..len_at + 4].try_into().unwrap()); invalid[len_at..len_at + 4].copy_from_slice(&(length + 1).to_le_bytes()); invalid.push(0); assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); invalid.pop(); invalid[len_at..len_at + 4].copy_from_slice(&length.to_le_bytes()); invalid[len_at + 4] = 0xff; assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); } #[test] fn typed_content_rejects_malformed_lengths_tags_utf8_and_trailing_bytes() { let content = EntryContent::ToolResult { ok: true, output: "hello".into(), call_id: ToolCallId::new(), }; let bytes = EntryFrame::new(None, &content).unwrap().content; for end in 0..bytes.len() { assert_eq!( EntryContentView::decode(&bytes[..end]).unwrap_err().code(), "ENTRY_DECODE" ); } let mut invalid = bytes.clone(); invalid[1] = 2; assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); invalid = bytes.clone(); invalid[2..6].copy_from_slice(&u32::MAX.to_le_bytes()); assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); invalid = bytes.clone(); invalid[6] = 0xff; assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); invalid = bytes; invalid.push(0); assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); invalid[0] = 255; assert_eq!( EntryContentView::decode(&invalid).unwrap_err().code(), "ENTRY_DECODE" ); } #[test] fn appended_content_decodes_from_the_arena_and_records_its_kind() { let mut session = Session::new(); let content = EntryContent::ToolResult { ok: false, output: "boom".to_string(), call_id: ToolCallId::new(), }; let entry = *session.append(&content).unwrap(); assert_eq!(entry.kind, content.kind()); assert_eq!(session.content(&entry).unwrap(), content); assert_eq!(session.bytes(&entry).len(), entry.content.len as usize); // A record from elsewhere reads as empty bytes and fails to decode. let foreign = Entry { content: Span { offset: 1 << 20, len: 4, }, ..entry }; assert!(session.bytes(&foreign).is_empty()); assert_eq!( session.content(&foreign).unwrap_err().code(), "ENTRY_DECODE" ); } #[test] fn failed_persist_leaves_the_session_unchanged() { let mut session = Session::new(); session.append(&message("kept")).unwrap(); let before = session.clone(); let error = session .append_with( &EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable(&serde_json::json!({"summary": ""})) .unwrap(), }, |_, bytes| { assert!(!bytes.is_empty()); Err(SmithError::Cancelled) }, ) .unwrap_err(); assert_eq!(error.code(), "CANCELLED"); assert_eq!(session, before); } #[test] fn fork_preserves_ids_order_leaf_and_compaction_boundary() { let mut session = Session::new(); let root = *session.append(&message("root")).unwrap(); let boundary = *session .append(&compaction( serde_json::json!({"summary": "", "summarized": 1}), )) .unwrap(); let after = *session.append(&message("after")).unwrap(); let forked = session.fork_at(boundary.id).unwrap(); assert_ne!(forked.id, session.id); assert_eq!(ids(forked.active_branch()), vec![root.id, boundary.id]); assert_eq!(forked.selected_id(), Some(boundary.id)); assert_eq!(forked.compaction_boundary, Some(boundary.id)); assert_eq!( forked .active_branch() .entries .iter() .map(|entry| text_of(&forked, entry)) .collect::>(), vec!["1".to_string(), COMPACTION_KIND.to_string()] ); // The fork carries only the arena prefix its records cover. assert_eq!(forked.bytes(&after), &[] as &[u8]); // Forking before the boundary drops it, because it is not retained. let earlier = session.fork_at(root.id).unwrap(); assert_eq!(earlier.compaction_boundary, None); assert_eq!(earlier.active_branch().entries.len(), 1); // The source session is untouched. assert_eq!(session.active_branch().entries.len(), 3); assert_eq!(session.selected_id(), Some(after.id)); } #[test] fn cbor_map_keys_follow_encoded_length_then_byte_order() { let content = EntryContent::Meta { kind: "event".into(), detail: StructuredData::from_serializable(&serde_json::json!({"aa": 0, "b": 1})) .unwrap(), }; let bytes = EntryFrame::new(None, &content).unwrap().content; let short = bytes.windows(2).position(|pair| pair == b"\x61b").unwrap(); let long = bytes.windows(3).position(|pair| pair == b"\x62aa").unwrap(); assert!( short < long, "CBOR canonical order puts shorter encoded key first" ); } fn nested_json(depth: u8) -> serde_json::Value { let mut value = serde_json::Value::Null; for _ in 0..depth { value = serde_json::Value::Array(vec![value]); } value } #[test] fn canonical_json_accepts_64_containers_and_rejects_65() { let at_limit = StructuredData::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX)) .unwrap(); assert!( at_limit .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .unwrap() ); assert_eq!( at_limit .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .unwrap(), at_limit ); let over_limit = StructuredData::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX + 1)) .unwrap(); assert_eq!( over_limit .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .unwrap_err(), AccessError::DepthExceeded { depth_limit: crate::json_scan::JSON_DEPTH_MAX } ); assert_eq!( over_limit .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .unwrap_err(), AccessError::DepthExceeded { depth_limit: crate::json_scan::JSON_DEPTH_MAX } ); } #[test] fn canonical_json_orders_unordered_maps_within_the_depth_bound() { let unordered = StructuredData::from_serializable( &serde_json::json!({"long": {"z": 1, "a": 2}, "b": 3}), ) .unwrap(); assert!( !unordered .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .unwrap() ); let canonical = unordered .canonicalized(crate::json_scan::JSON_DEPTH_MAX) .unwrap(); assert!( canonical .is_canonical(crate::json_scan::JSON_DEPTH_MAX) .unwrap() ); assert_eq!( serde_json::to_value(canonical).unwrap(), serde_json::to_value(unordered).unwrap() ); } #[test] fn message_canonicalization_rejects_a_later_overdeep_tool_input() { let mut message = Message::new(Role::Assistant); message.add_block(ContentBlock::tool_use( "first", ToolArguments::from_serializable(&serde_json::json!({"z": 1, "a": 2})).unwrap(), ToolCallId::new(), )); message.add_block(ContentBlock::tool_use( "later", ToolArguments::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX + 1)) .unwrap(), ToolCallId::new(), )); assert_eq!( EntryFrame::new(None, &EntryContent::Message(message)) .unwrap_err() .code(), "ENTRY_DECODE" ); } #[test] fn tool_use_json_inside_messages_encodes_identically() { let call_id = ToolCallId::new(); let block = |input: serde_json::Value| { let mut message = Message::with_text(Role::Assistant, "call"); message.add_block(ContentBlock::ToolUse { name: "write".to_string(), input: ToolArguments::from_serializable(&input).unwrap(), call_id, }); message }; let first = block(serde_json::json!({"path": "p", "content": "c"})); let mut second = block(serde_json::json!({"content": "c", "path": "p"})); // Only key order may differ, so encodings must match. second.id = first.id; second.timestamp_ms = first.timestamp_ms; let mut session = Session::new(); let a = *session.append(&EntryContent::Message(first)).unwrap(); let b = *session.append(&EntryContent::Message(second)).unwrap(); assert_eq!(session.bytes(&a), session.bytes(&b)); } #[test] fn fork_rejects_unknown_entries() { let session = Session::new(); let error = session.fork_at(EntryId::new()).unwrap_err(); assert_eq!(error.code(), "ENTRY_NOT_FOUND"); } #[test] fn fork_rejects_broken_ancestry_and_forks_sibling_branches() { // A stored chain whose root parent is missing restores, but cannot fork. let orphan = EntryFrame::new(Some(EntryId::new()), &message("orphan")).unwrap(); let child = EntryFrame::new(Some(orphan.id), &message("child")).unwrap(); let broken = Session::from_frames(&stored(&[orphan, child.clone()]).1).unwrap(); assert_eq!(broken.selected_id(), Some(child.id)); let error = broken.fork_at(child.id).unwrap_err(); assert_eq!(error.code(), "ENTRY_NOT_FOUND"); // A leaf on an inactive sibling branch forks along its own chain. let root = EntryFrame::new(None, &message("root")).unwrap(); let first = EntryFrame::new(Some(root.id), &message("first")).unwrap(); let second = EntryFrame::new(Some(root.id), &message("second")).unwrap(); let siblings = Session::from_frames(&stored(&[root.clone(), first.clone(), second]).1).unwrap(); let forked = siblings.fork_at(first.id).unwrap(); assert_eq!(ids(forked.active_branch()), vec![root.id, first.id]); assert_eq!(text_of(&forked, &forked.active_branch().entries[1]), "1"); } #[test] fn loading_copies_content_bytes_without_decoding_them() { let mut scratch = Session::new(); let root = *scratch.append(&message("root")).unwrap(); let mut stored = frames(&scratch); if let Frame::Known { entry } = &mut stored[1] { entry.content = vec![0xFF, 0x00]; } let restored = Session::from_frames(&stored).unwrap(); let record = restored.active_branch().entries[0]; assert_eq!(record.kind, root.kind); assert_eq!(restored.bytes(&record), &[0xFF, 0x00]); assert_eq!( restored.content(&record).unwrap_err().code(), "ENTRY_DECODE" ); } #[test] fn content_of_a_record_whose_kind_lies_is_a_kind_mismatch() { let mut scratch = Session::new(); scratch.append(&message("root")).unwrap(); let mut stored = frames(&scratch); let lie = EntryKind::ToolCall { call_id: ToolCallId::new(), }; if let Frame::Known { entry } = &mut stored[1] { entry.kind = lie; } // Loading trusts the record; only a decode can tell. let restored = Session::from_frames(&stored).unwrap(); let record = restored.active_branch().entries[0]; assert_eq!(record.kind, lie); assert_eq!( restored.content(&record).unwrap_err().code(), "ENTRY_KIND_MISMATCH" ); } #[test] fn compaction_boundary_survives_a_frame_round_trip() { let mut session = Session::new(); session.append(&message("root")).unwrap(); let boundary = *session .append(&compaction(serde_json::json!({"summary": ""}))) .unwrap(); let restored = Session::from_frames(&frames(&session)).unwrap(); assert_eq!(restored.compaction_boundary, Some(boundary.id)); assert_eq!(restored.id, session.id); assert_eq!(restored.active_branch().id, session.header().branch_id); assert_eq!(restored, session); } } /// Laws of canonicalization, span arithmetic, and content encoding /// (`SMH-SPEC-SPEC0001`, Sessions). /// /// `serde_json` preserves insertion order but compares maps order-blind, so /// key-order laws compare serialized bytes, never values. #[cfg(test)] pub mod properties { use super::*; use proptest::collection::vec; use proptest::prelude::*; use serde_json::Value; /// Arbitrary JSON, at most three levels deep and 24 nodes large. fn json() -> impl Strategy { let leaf = prop_oneof![ Just(Value::Null), any::().prop_map(Value::from), any::().prop_map(Value::from), any::().prop_map(Value::from), (proptest::num::f64::NORMAL | proptest::num::f64::ZERO).prop_map(Value::from), "\\PC{0,8}".prop_map(Value::from), ]; leaf.prop_recursive(3, 24, 4, |inner| { prop_oneof![ vec(inner.clone(), 0..4).prop_map(Value::Array), vec(("\\PC{0,4}", inner), 0..5) .prop_map(|entries| Value::Object(entries.into_iter().collect())), ] }) } /// `value` with the keys of every object reordered as `seed` dictates. fn reordered(value: &Value, seed: u64) -> Value { match value { Value::Object(map) => { let mut entries: Vec<(String, Value)> = map .iter() .map(|(key, value)| (key.clone(), reordered(value, seed.rotate_left(7)))) .collect(); if !entries.is_empty() { let by = usize::try_from(seed % entries.len() as u64).unwrap_or(0); entries.rotate_left(by); } if seed & 1 == 1 { entries.reverse(); } Value::Object(entries.into_iter().collect()) } Value::Array(items) => Value::Array( items .iter() .map(|item| reordered(item, seed.rotate_left(7))) .collect(), ), other => other.clone(), } } /// Any message role. pub fn role() -> impl Strategy { prop_oneof![ Just(Role::User), Just(Role::Assistant), Just(Role::System), Just(Role::Tool), ] } fn block() -> impl Strategy { prop_oneof![ "\\PC{0,16}".prop_map(ContentBlock::Text), "\\PC{0,16}".prop_map(ContentBlock::Thinking), ("\\PC{0,8}", json(), any::()).prop_map(|(name, input, id)| { ContentBlock::tool_use( name, ToolArguments::from_serializable(&input).unwrap(), ToolCallId::from_u128(id), ) }), (any::(), any::(), "\\PC{0,16}").prop_map(|(id, ok, output)| { ContentBlock::tool_result(ToolCallId::from_u128(id), ok, output) }), ] } fn content() -> impl Strategy { prop_oneof![ (any::(), role(), vec(block(), 0..4), any::()).prop_map( |(id, role, blocks, timestamp_ms)| EntryContent::Message(Message { id: MessageId::from_u128(id), role, blocks, timestamp_ms, }) ), ("\\PC{0,8}", json(), any::()).prop_map(|(name, input, id)| { EntryContent::ToolCall { name, input: ToolArguments::from_serializable(&input).unwrap(), call_id: ToolCallId::from_u128(id), } }), (any::(), "\\PC{0,16}", any::()).prop_map(|(ok, output, id)| { EntryContent::ToolResult { ok, output, call_id: ToolCallId::from_u128(id), } }), ("\\PC{0,8}", json()) .prop_filter("extension metadata kind", |(kind, _)| { kind != COMPACTION_KIND && kind != PROVIDER_HTTP_KIND && kind != TOOL_MUTATION_KIND }) .prop_map(|(kind, detail)| EntryContent::Meta { kind, detail: StructuredData::from_serializable(&detail).unwrap(), }), ("\\PC{0,8}", prop::option::of(any::())).prop_map(|(summary, complete)| { let mut detail = serde_json::Map::new(); detail.insert("summary".to_string(), Value::String(summary)); if let Some(complete) = complete { detail.insert("complete".to_string(), Value::Bool(complete)); } EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: StructuredData::from_serializable(&Value::Object(detail)).unwrap(), } }), ] } /// Offsets and lengths around the 4 GiB arena cap, plus anything. fn arena_size() -> impl Strategy { let cap = u32::MAX as usize; prop_oneof![ 0..=cap, cap - 64..=cap + 64, (0..=64_usize).prop_map(move |below| cap - below), any::(), ] } proptest! { #[test] fn canonical_json_is_idempotent_and_keeps_the_value(value in json()) { let value = StructuredData::from_serializable(&value).map_err(|error| TestCaseError::fail(error.to_string()))?; let once = value.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?; let twice = once.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?; prop_assert_eq!(serde_json::to_string(&twice).unwrap(), serde_json::to_string(&once).unwrap()); prop_assert!(once.is_canonical(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?); prop_assert_eq!(once, value); } #[test] fn canonical_json_ignores_key_order(value in json(), seed in any::()) { let shuffled = reordered(&value, seed); let canonical_shuffled = StructuredData::from_serializable(&shuffled).unwrap().canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?; let canonical_value = StructuredData::from_serializable(&value).unwrap().canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?; prop_assert_eq!(serde_json::to_string(&canonical_shuffled).unwrap(), serde_json::to_string(&canonical_value).unwrap()); } #[test] fn is_canonical_holds_exactly_when_canonicalizing_changes_nothing(value in json()) { let value = StructuredData::from_serializable(&value).unwrap(); let canonical = value.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?; prop_assert_eq!( value.is_canonical(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?, serde_json::to_string(&canonical).unwrap() == serde_json::to_string(&value).unwrap() ); } #[test] fn span_at_is_exact_below_the_cap_and_an_error_above( offset in arena_size(), len in arena_size(), ) { let fits = offset .checked_add(len) .is_some_and(|end| u32::try_from(end).is_ok()); match Span::at(offset, len) { Ok(span) => { prop_assert!(fits); prop_assert_eq!(span.offset as usize, offset); prop_assert_eq!(span.len as usize, len); prop_assert_eq!(span.end(), offset + len); prop_assert_eq!(span.range().len(), len); } Err(error) => { prop_assert!(!fits); prop_assert_eq!(error.code(), "SESSION_ARENA_FULL"); } } } #[test] fn arbitrary_content_bytes_never_panic(bytes in vec(any::(), 0..512)) { let _ = EntryContentView::decode(&bytes); } #[test] fn owned_and_borrowed_encodings_are_canonical_and_decode_back(content in content()) { let owned = EntryFrame::new(None, &content) .map_err(|e| TestCaseError::fail(e.to_string()))? .content; let mut session = Session::new(); let record = *session .append(EntryContentRef::from(&content)) .map_err(|e| TestCaseError::fail(e.to_string()))?; prop_assert_eq!(session.bytes(&record), owned.as_slice()); let view = EntryContentView::decode(&owned) .map_err(|e| TestCaseError::fail(e.to_string()))?; prop_assert_eq!(view.kind(), content.kind()); prop_assert_eq!(view.to_owned().unwrap(), content.clone()); prop_assert_eq!( EntryContent::decode(&owned).map_err(|e| TestCaseError::fail(e.to_string()))?, content ); } } }