//! SMH-SPEC-SPEC0001: Sessions, typed entry content. //! //! Version 1 typed entry content. All integers are little endian, strings and //! JSON spans have u32 byte lengths; JSON alone uses CBOR. No Rust layout is persisted. use crate::cbor::{Encoder, Header, Write}; use crate::session::{COMPACTION_KIND, EntryContent, EntryContentRef, EntryKind}; use serde::ser::{SerializeSeq, SerializeStruct}; use serde::{Serialize, Serializer}; use smith::error::{Result, SmithError}; use smith::id::{MessageId, ToolCallId}; use smith::message::{ContentBlock, Message, Role}; use smith::structured::{AccessError, Kind, StructuredData, StructuredValue, ToolArguments}; fn invalid(reason: &str) -> SmithError { SmithError::Session { code: smith::error::SessionCode::EntryDecode, message: reason.into(), } } struct Cursor<'a> { bytes: &'a [u8], } impl<'a> Cursor<'a> { fn take(&mut self, count: usize) -> Result<&'a [u8]> { let (head, rest) = self .bytes .split_at_checked(count) .ok_or_else(|| invalid("truncated content span"))?; self.bytes = rest; Ok(head) } fn byte(&mut self) -> Result { Ok(self.take(1)?[0]) } fn u32(&mut self) -> Result { Ok(u32::from_le_bytes( self.take(4)? .try_into() .map_err(|_| invalid("truncated u32"))?, )) } fn i64(&mut self) -> Result { Ok(i64::from_le_bytes( self.take(8)? .try_into() .map_err(|_| invalid("truncated i64"))?, )) } fn id(&mut self) -> Result { Ok(u128::from_le_bytes( self.take(16)? .try_into() .map_err(|_| invalid("truncated ID"))?, )) } fn span(&mut self) -> Result<&'a [u8]> { let n = self.u32()? as usize; self.take(n) } fn text(&mut self) -> Result<&'a str> { std::str::from_utf8(self.span()?).map_err(|_| invalid("invalid UTF-8")) } fn boolean(&mut self) -> Result { match self.byte()? { 0 => Ok(false), 1 => Ok(true), _ => Err(invalid("invalid boolean")), } } fn json(&mut self) -> Result> { let bytes = self.span()?; crate::json_scan::scan(bytes, None)?; Ok(JsonView(bytes)) } fn end(&self) -> Result<()> { if self.bytes.is_empty() { Ok(()) } else { Err(invalid("trailing content bytes")) } } } fn put_span(out: &mut Vec, bytes: &[u8]) -> Result<()> { let len = u32::try_from(bytes.len()).map_err(|_| invalid("content span exceeds u32"))?; out.extend_from_slice(&len.to_le_bytes()); out.extend_from_slice(bytes); Ok(()) } #[derive(Clone, Copy, Debug)] struct SpanLimit; /// CBOR sink limited to one length-prefixed JSON span. struct BoundedOutput<'a> { out: &'a mut Vec, start: usize, max: usize, } impl<'a> BoundedOutput<'a> { fn new(out: &'a mut Vec, max: usize) -> Self { Self { start: out.len(), out, max: max.min(u32::MAX as usize), } } const fn len(&self) -> usize { self.out.len() - self.start } } impl Write for BoundedOutput<'_> { type Error = SpanLimit; fn write_all(&mut self, bytes: &[u8]) -> std::result::Result<(), Self::Error> { let Some(len) = self.len().checked_add(bytes.len()) else { return Err(SpanLimit); }; if len > self.max { return Err(SpanLimit); } self.out.extend_from_slice(bytes); Ok(()) } fn flush(&mut self) -> std::result::Result<(), Self::Error> { Ok(()) } } fn put_json<'a, Value: StructuredValue<'a>>(out: &mut Vec, value: Value) -> Result<()> { put_json_bounded(out, value, u32::MAX as usize) } fn put_json_bounded<'a, Value: StructuredValue<'a>>( out: &mut Vec, value: Value, max: usize, ) -> Result<()> { let start = out.len(); out.extend_from_slice(&[0; 4]); let result = { let sink = BoundedOutput::new(out, max); let mut encoder = Encoder::from(sink); encode_json(value, &mut encoder) }; if result.is_err() { out.truncate(start); return Err(invalid("content span exceeds u32")); } let len = out.len() - start - 4; assert!( len <= max, "bounded encoder wrote beyond its declared limit" ); let Ok(len) = u32::try_from(len) else { out.truncate(start); return Err(invalid("content span exceeds u32")); }; out[start..start + 4].copy_from_slice(&len.to_le_bytes()); Ok(()) } fn encode_json<'a, Value: StructuredValue<'a>>( value: Value, encoder: &mut Encoder>, ) -> std::result::Result<(), SpanLimit> { match value.kind().map_err(|_| SpanLimit)? { Kind::Null => encoder.push(Header::Simple(22)), Kind::Bool(value) => encoder.push(Header::Simple(if value { 21 } else { 20 })), Kind::Signed(value) if value < 0 => { encoder.push(Header::Negative((-1 - value).cast_unsigned())) } Kind::Signed(value) => encoder.push(Header::Positive(value.cast_unsigned())), Kind::Unsigned(value) => encoder.push(Header::Positive(value)), Kind::Float(value) => encoder.push(Header::Float(value)), Kind::String(value) => encoder.text(value, None), Kind::Array => { let elements = value.elements().map_err(|_| SpanLimit)?.ok_or(SpanLimit)?; encoder.push(Header::Array(Some(elements.len())))?; for element in elements { encode_json(element.map_err(|_| SpanLimit)?, encoder)?; } Ok(()) } Kind::Object => { let fields = value.fields().map_err(|_| SpanLimit)?.ok_or(SpanLimit)?; encoder.push(Header::Map(Some(fields.len())))?; for field in fields { let (key, value) = field.map_err(|_| SpanLimit)?; encoder.text(key, None)?; encode_json(value, encoder)?; } Ok(()) } } } pub(crate) fn encode(content: &EntryContentRef<'_>, out: &mut Vec) -> Result<()> { match *content { EntryContentRef::Message(message) => { out.push(0); out.extend_from_slice(&message.id.as_u128().to_le_bytes()); out.push(match message.role { Role::User => 0, Role::Assistant => 1, Role::System => 2, Role::Tool => 3, }); out.extend_from_slice(&message.timestamp_ms.to_le_bytes()); out.extend_from_slice( &u32::try_from(message.blocks.len()) .map_err(|_| invalid("too many blocks"))? .to_le_bytes(), ); for block in &message.blocks { match block { ContentBlock::Text(text) => { out.push(0); put_span(out, text.as_bytes())?; } ContentBlock::Thinking(text) => { out.push(1); put_span(out, text.as_bytes())?; } ContentBlock::ToolUse { name, input, call_id, } => { out.push(2); put_span(out, name.as_bytes())?; put_json(out, input.view())?; out.extend_from_slice(&call_id.as_u128().to_le_bytes()); } ContentBlock::ToolResult { call_id, ok, output, } => { out.push(3); out.extend_from_slice(&call_id.as_u128().to_le_bytes()); out.push(u8::from(*ok)); put_span(out, output.as_bytes())?; } } } } EntryContentRef::ToolCall { name, input, call_id, } => { out.push(1); put_span(out, name.as_bytes())?; put_json(out, input.view())?; out.extend_from_slice(&call_id.as_u128().to_le_bytes()); } EntryContentRef::ToolResult { ok, output, call_id, } => { out.push(2); out.push(u8::from(ok)); put_span(out, output.as_bytes())?; out.extend_from_slice(&call_id.as_u128().to_le_bytes()); } EntryContentRef::Meta { kind, detail } => { crate::metadata::validate(kind, detail.view()) .map_err(|_| invalid("invalid metadata"))?; out.push(3); put_span(out, kind.as_bytes())?; put_json(out, detail.view())?; } } Ok(()) } /// Validated CBOR JSON span, decoded only at the consumer boundary. #[derive(Clone, Copy, Debug)] pub struct JsonView<'a>(pub(crate) &'a [u8]); impl JsonView<'_> { fn owned(self) -> Result { StructuredData::from_serializable(&self).map_err(|error| invalid(&error.to_string())) } /// Whether any JSON string value contains `pattern`. /// /// # Panics /// Only if the private validated-span invariant is broken. #[must_use] pub fn contains(self, pattern: &str) -> bool { let found = crate::json_scan::scan(self.0, Some(pattern)); assert!( found.is_ok(), "validated JSON span remains canonical during string search" ); found.is_ok_and(|found| found) } } impl<'a> StructuredValue<'a> for JsonView<'a> { type Elements = crate::json_scan::Elements<'a>; type Fields = crate::json_scan::Fields<'a>; fn kind(self) -> std::result::Result, AccessError> { crate::json_scan::kind(self.0).map_err(|_| AccessError::Malformed) } fn elements(self) -> std::result::Result, AccessError> { crate::json_scan::elements(self.0).map_err(|_| AccessError::Malformed) } fn fields(self) -> std::result::Result, AccessError> { crate::json_scan::fields(self.0).map_err(|_| AccessError::Malformed) } } impl Serialize for JsonView<'_> { fn serialize(&self, serializer: S) -> std::result::Result { crate::json_scan::serialize(self.0, serializer) } } /// A validated block borrowing text directly from an entry content span. #[derive(Clone, Copy, Debug)] pub enum BlockView<'a> { /// Plain text. Text(&'a str), /// Internal reasoning. Thinking(&'a str), /// Structured tool request. ToolUse { /// Tool name. name: &'a str, /// Structured input. input: JsonView<'a>, /// Pairing ID. call_id: ToolCallId, }, /// Tool output. ToolResult { /// Pairing ID. call_id: ToolCallId, /// Success flag. ok: bool, /// Recorded output. output: &'a str, }, } impl Serialize for BlockView<'_> { fn serialize(&self, serializer: S) -> std::result::Result { #[derive(Serialize)] struct Use<'a> { name: &'a str, input: JsonView<'a>, call_id: ToolCallId, } #[derive(Serialize)] struct Result<'a> { call_id: ToolCallId, ok: bool, output: &'a str, } let mut outer = serializer.serialize_struct("ContentBlock", 2)?; match *self { Self::Text(t) => { outer.serialize_field("type", "Text")?; outer.serialize_field("data", t)?; } Self::Thinking(t) => { outer.serialize_field("type", "Thinking")?; outer.serialize_field("data", t)?; } Self::ToolUse { name, input, call_id, } => { outer.serialize_field("type", "ToolUse")?; outer.serialize_field( "data", &Use { name, input, call_id, }, )?; } Self::ToolResult { call_id, ok, output, } => { outer.serialize_field("type", "ToolResult")?; outer.serialize_field( "data", &Result { call_id, ok, output, }, )?; } } outer.end() } } fn block<'a>(cursor: &mut Cursor<'a>) -> Result> { Ok(match cursor.byte()? { 0 => BlockView::Text(cursor.text()?), 1 => BlockView::Thinking(cursor.text()?), 2 => BlockView::ToolUse { name: cursor.text()?, input: cursor.json()?, call_id: ToolCallId::from_u128(cursor.id()?), }, 3 => BlockView::ToolResult { call_id: ToolCallId::from_u128(cursor.id()?), ok: cursor.boolean()?, output: cursor.text()?, }, _ => return Err(invalid("invalid block tag")), }) } /// Iterator over blocks in a previously validated message span. #[derive(Clone, Debug)] pub struct Blocks<'a> { cursor: &'a [u8], left: u32, } impl<'a> Iterator for Blocks<'a> { type Item = BlockView<'a>; #[expect( clippy::expect_used, reason = "Blocks only holds spans fully traversed by decode" )] fn next(&mut self) -> Option { if self.left == 0 { return None; } let mut cursor = Cursor { bytes: self.cursor }; let block = block(&mut cursor).expect("validated block span"); self.cursor = cursor.bytes; self.left -= 1; Some(block) } fn size_hint(&self) -> (usize, Option) { (self.left as usize, Some(self.left as usize)) } } impl ExactSizeIterator for Blocks<'_> {} /// Validated message metadata with zero-copy block iteration. #[derive(Clone, Debug)] pub struct MessageView<'a> { /// Stable message identifier. pub id: MessageId, /// Sender role. pub role: Role, /// Timestamp in Unix milliseconds. pub timestamp_ms: i64, blocks: Blocks<'a>, } impl<'a> MessageView<'a> { /// Iterate message blocks without allocating. pub fn blocks(&self) -> Blocks<'a> { self.blocks.clone() } } impl Serialize for MessageView<'_> { fn serialize(&self, serializer: S) -> std::result::Result { struct Sequence<'a>(Blocks<'a>); impl Serialize for Sequence<'_> { fn serialize( &self, serializer: S, ) -> std::result::Result { let mut seq = serializer.serialize_seq(Some(self.0.len()))?; for block in self.0.clone() { seq.serialize_element(&block)?; } seq.end() } } let mut out = serializer.serialize_struct("Message", 4)?; out.serialize_field("id", &self.id)?; out.serialize_field("role", &self.role)?; out.serialize_field("blocks", &Sequence(self.blocks()))?; out.serialize_field("timestamp_ms", &self.timestamp_ms)?; out.end() } } /// Content decoded from bytes, distinct from the recording-time borrowed Rust view. #[derive(Clone, Debug)] pub enum EntryContentView<'a> { /// Message with block spans. Message(MessageView<'a>), /// Tool invocation. ToolCall { /// Tool name. name: &'a str, /// Structured input. input: JsonView<'a>, /// Pairing ID. call_id: ToolCallId, }, /// Tool completion. ToolResult { /// Success flag. ok: bool, /// Recorded output. output: &'a str, /// Pairing ID. call_id: ToolCallId, }, /// Metadata event. Meta { /// Metadata kind. kind: &'a str, /// Structured detail. detail: JsonView<'a>, }, } impl<'a> EntryContentView<'a> { /// Validate every tag, length, nested value and trailing byte before returning a view. /// /// # Errors /// Returns `ENTRY_DECODE` for malformed content. pub fn decode(bytes: &'a [u8]) -> Result { let mut cursor = Cursor { bytes }; let value = match cursor.byte()? { 0 => { let id = MessageId::from_u128(cursor.id()?); let role = match cursor.byte()? { 0 => Role::User, 1 => Role::Assistant, 2 => Role::System, 3 => Role::Tool, _ => return Err(invalid("invalid message role")), }; let timestamp_ms = cursor.i64()?; let count = cursor.u32()?; let blocks = Blocks { cursor: cursor.bytes, left: count, }; for _ in 0..count { block(&mut cursor)?; } Self::Message(MessageView { id, role, timestamp_ms, blocks, }) } 1 => Self::ToolCall { name: cursor.text()?, input: cursor.json()?, call_id: ToolCallId::from_u128(cursor.id()?), }, 2 => Self::ToolResult { ok: cursor.boolean()?, output: cursor.text()?, call_id: ToolCallId::from_u128(cursor.id()?), }, 3 => { let kind = cursor.text()?; let detail = cursor.json()?; crate::metadata::validate(kind, detail).map_err(|_| invalid("invalid metadata"))?; Self::Meta { kind, detail } } _ => return Err(invalid("invalid content tag")), }; cursor.end()?; Ok(value) } /// Traversal metadata derived from validated content. #[must_use] pub fn kind(&self) -> EntryKind { match self { Self::Message(m) => EntryKind::Message { id: m.id, role: m.role, }, Self::ToolCall { call_id, .. } => EntryKind::ToolCall { call_id: *call_id }, Self::ToolResult { call_id, ok, .. } => EntryKind::ToolResult { call_id: *call_id, ok: *ok, }, Self::Meta { kind, .. } => EntryKind::Meta { compaction: *kind == COMPACTION_KIND, }, } } /// Match against recorded string values (not keys). #[must_use] pub fn contains(&self, pattern: &str) -> bool { match self { Self::Message(m) => m.blocks().any(|b| match b { BlockView::Text(s) | BlockView::Thinking(s) => s.contains(pattern), BlockView::ToolUse { name, input, .. } => { name.contains(pattern) || input.contains(pattern) } BlockView::ToolResult { output, .. } => output.contains(pattern), }), Self::ToolCall { name, input, .. } => name.contains(pattern) || input.contains(pattern), Self::ToolResult { output, .. } => output.contains(pattern), Self::Meta { kind, detail } => kind.contains(pattern) || detail.contains(pattern), } } /// Materialize content for agent context, replay or API consumers. /// /// # Errors /// Returns `ENTRY_DECODE` if structured data cannot be materialized. pub fn to_owned(&self) -> Result { Ok(match self { Self::Message(m) => { let mut blocks = Vec::with_capacity(m.blocks.len()); for block in m.blocks() { blocks.push(match block { BlockView::Text(text) => ContentBlock::Text(text.to_owned()), BlockView::Thinking(text) => ContentBlock::Thinking(text.to_owned()), BlockView::ToolUse { name, input, call_id, } => ContentBlock::ToolUse { name: name.to_owned(), input: ToolArguments::from_data(input.owned()?), call_id, }, BlockView::ToolResult { call_id, ok, output, } => ContentBlock::ToolResult { call_id, ok, output: output.to_owned(), }, }); } EntryContent::Message(Message { id: m.id, role: m.role, timestamp_ms: m.timestamp_ms, blocks, }) } Self::ToolCall { name, input, call_id, } => EntryContent::ToolCall { name: (*name).to_owned(), input: ToolArguments::from_data(input.owned()?), call_id: *call_id, }, Self::ToolResult { ok, output, call_id, } => EntryContent::ToolResult { ok: *ok, output: (*output).to_owned(), call_id: *call_id, }, Self::Meta { kind, detail } => EntryContent::Meta { kind: (*kind).to_owned(), detail: detail.owned()?, }, }) } } impl Serialize for EntryContentView<'_> { fn serialize(&self, serializer: S) -> std::result::Result { #[derive(Serialize)] struct Invocation<'a> { name: &'a str, input: JsonView<'a>, call_id: ToolCallId, } #[derive(Serialize)] struct Result<'a> { ok: bool, output: &'a str, call_id: ToolCallId, } #[derive(Serialize)] struct Meta<'a> { kind: &'a str, detail: JsonView<'a>, } let mut out = serializer.serialize_struct("EntryContent", 2)?; match self { Self::Message(m) => { out.serialize_field("kind", "Message")?; out.serialize_field("data", m)?; } Self::ToolCall { name, input, call_id, } => { out.serialize_field("kind", "ToolCall")?; out.serialize_field( "data", &Invocation { name, input: *input, call_id: *call_id, }, )?; } Self::ToolResult { ok, output, call_id, } => { out.serialize_field("kind", "ToolResult")?; out.serialize_field( "data", &Result { ok: *ok, output, call_id: *call_id, }, )?; } Self::Meta { kind, detail } => { out.serialize_field("kind", "Meta")?; out.serialize_field( "data", &Meta { kind, detail: *detail, }, )?; } } out.end() } } #[cfg(test)] mod tests { //! SMH-SPEC-SPEC0001: Sessions, typed content and bounded access. use super::*; use crate::session::EntryFrame; use proptest::prelude::*; use serde::Serialize; use smith_alloc::{Counting, Scope, measure}; use std::sync::Mutex; #[global_allocator] static ALLOC: Counting = Counting; static MEASURING: Mutex<()> = Mutex::new(()); #[test] fn materialized_message_reserves_exact_validated_block_count() { let mut message = Message::with_text(Role::User, "0"); for block in 1..7 { message.add_block(ContentBlock::Text(block.to_string())); } let mut bytes = Vec::new(); encode(&EntryContentRef::Message(&message), &mut bytes).unwrap(); let EntryContent::Message(owned) = EntryContentView::decode(&bytes) .unwrap() .to_owned() .unwrap() else { panic!("message kind must survive materialization"); }; assert_eq!(owned, message); assert_eq!(owned.blocks.len(), 7); assert_eq!(owned.blocks.capacity(), 7); } #[test] fn borrowed_arguments_validate_and_traverse_without_allocations() { let arguments = ToolArguments::from_json( r#"{"path":"notes.txt","limit":18446744073709551615,"items":[null,true,-0.0]}"#, ) .unwrap() .canonicalized(64) .unwrap(); let schema = smith::ToolSchema::object( &[("path", "string"), ("limit", "integer"), ("items", "array")], &["path"], ); let mut bytes = Vec::new(); encode( &EntryContentRef::ToolCall { name: "read", input: &arguments, call_id: ToolCallId::from_u128(1), }, &mut bytes, ) .unwrap(); let EntryContentView::ToolCall { input, .. } = EntryContentView::decode(&bytes).unwrap() else { panic!("tool call must retain argument view"); }; let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let ((), cost) = measure(Scope::Session, || { schema.validate(input).unwrap(); assert_eq!(input.fields().unwrap().unwrap().len(), 3); let Kind::String(path) = input.field("path").unwrap().unwrap().kind().unwrap() else { panic!("path must remain a string"); }; assert_eq!(path, "notes.txt"); let resident = (bytes.as_ptr() as usize)..(bytes.as_ptr() as usize + bytes.len()); assert!(resident.contains(&(path.as_ptr() as usize))); assert_eq!( input.field("limit").unwrap().unwrap().kind().unwrap(), Kind::Unsigned(u64::MAX) ); let mut items = input .field("items") .unwrap() .unwrap() .elements() .unwrap() .unwrap(); assert_eq!(items.len(), 3); assert_eq!(items.next().unwrap().unwrap().kind().unwrap(), Kind::Null); assert_eq!(items.len(), 2); assert_eq!( items.next().unwrap().unwrap().kind().unwrap(), Kind::Bool(true) ); let Kind::Float(zero) = items.next().unwrap().unwrap().kind().unwrap() else { panic!("negative zero must remain a float"); }; assert_eq!(zero.to_bits(), (-0.0_f64).to_bits()); assert_eq!(items.len(), 0); assert!(items.next().is_none()); }); assert_eq!(cost.allocations, 0); assert_eq!(cost.bytes, 0); assert_eq!(cost.peak, 0); assert_eq!(std::mem::size_of::>(), 16); assert_eq!(std::mem::size_of::>(), 24); assert_eq!(std::mem::size_of::>(), 40); } #[test] fn json_encoding_uses_existing_output_capacity() { let value = StructuredData::from_serializable(&serde_json::json!({"b": [1, 2, 3], "aa": "value"})) .unwrap(); let mut bytes = Vec::with_capacity(128); let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (result, cost) = measure(Scope::Session, || put_json(&mut bytes, value.view())); result.unwrap(); assert_eq!(cost.allocations, 0); assert_eq!(cost.bytes, 0); } #[test] fn bounded_output_writes_short_and_exact_spans() { let mut bytes = vec![9]; { let mut sink = BoundedOutput::new(&mut bytes, 3); sink.write_all(&[1]).unwrap(); sink.write_all(&[2, 3]).unwrap(); assert_eq!(sink.len(), 3); } assert_eq!(bytes, [9, 1, 2, 3]); } #[test] fn bounded_output_rejects_over_limit_without_appending() { let mut bytes = vec![9]; { let mut sink = BoundedOutput::new(&mut bytes, 2); assert!(sink.write_all(&[1, 2, 3]).is_err()); assert_eq!(sink.len(), 0); } assert_eq!(bytes, [9]); } #[test] fn json_encoding_rolls_back_on_span_limit() { let mut bytes = vec![9]; let value = StructuredData::from_json("\"x\"").unwrap(); assert!(put_json_bounded(&mut bytes, value.view(), 1).is_err()); assert_eq!(bytes, [9]); } #[test] fn structured_value_wire_bytes_match_v4_golden_contract() { let value = StructuredData::from_serializable(&serde_json::json!([ -1, 0, 23, 24, 255, 256, 65_535, 65_536, i64::MIN, u64::MAX, -0.0, 1.5, 1.1, {"b": 1, "aa": 2}, ])) .unwrap(); let mut bytes = Vec::new(); put_json(&mut bytes, value.view()).unwrap(); assert_eq!( bytes, [ 60, 0, 0, 0, 0x8e, 0x20, 0x00, 0x17, 0x18, 0x18, 0x18, 0xff, 0x19, 0x01, 0x00, 0x19, 0xff, 0xff, 0x1a, 0x00, 0x01, 0x00, 0x00, 0x3b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x80, 0x00, 0xf9, 0x3e, 0x00, 0xfb, 0x3f, 0xf1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, 0xa2, 0x61, b'b', 0x01, 0x62, b'a', b'a', 0x02, ] ); } proptest! { #[test] fn encoded_json_scalars_round_trip_with_numeric_kind(value in prop_oneof![ any::().prop_map(serde_json::Value::Bool), any::().prop_map(serde_json::Value::from), any::().prop_map(serde_json::Value::from), any::().prop_filter_map("finite JSON number", serde_json::Number::from_f64) .prop_map(serde_json::Value::Number), ".{0,64}".prop_map(serde_json::Value::String), ]) { let mut bytes = Vec::new(); let expected = value.clone(); let value = StructuredData::from_serializable(&value).unwrap(); put_json(&mut bytes, value.view()).unwrap(); let encoded = &bytes[4..]; prop_assert!(crate::json_scan::scan(encoded, None).is_ok()); prop_assert_eq!( crate::json_scan::serialize(encoded, serde_json::value::Serializer).unwrap(), expected, ); } } #[test] fn rejects_duplicate_json_keys_before_search_or_materialization() { let mut bytes = vec![3, 1, 0, 0, 0, b'k', 7, 0, 0, 0]; bytes.extend_from_slice(&[0xa2, 0x61, b'a', 1, 0x61, b'a', 2]); assert_eq!( EntryContentView::decode(&bytes).unwrap_err().code(), "ENTRY_DECODE" ); } #[test] fn borrowed_known_metadata_validation_allocates_nothing() { let tool = crate::metadata::encode(&crate::metadata::ToolMutationMetadata::new( ToolCallId::from_u128(1), smith::error::MutationState::Applied, )) .unwrap(); let compaction = crate::metadata::encode(&crate::metadata::CompactionMetadata { summary: "summary".to_string(), complete: Some(true), }) .unwrap(); let provider = StructuredData::from_json( r#"{"phase":"outcome","status":0,"response_bytes":0,"outcome":"completed"}"#, ) .unwrap(); for (kind, detail) in [ (COMPACTION_KIND, compaction), (crate::session::TOOL_MUTATION_KIND, tool), (crate::session::PROVIDER_HTTP_KIND, provider), ] { let bytes = EntryFrame::new( None, &EntryContent::Meta { kind: kind.to_string(), detail, }, ) .unwrap() .content; let EntryContentView::Meta { kind, detail } = EntryContentView::decode(&bytes).unwrap() else { panic!("metadata must retain a borrowed view"); }; let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (result, cost) = measure(Scope::Session, || crate::metadata::validate(kind, detail)); assert!(result.is_ok()); assert_eq!(cost.allocations, 0, "{kind} validation allocated"); assert_eq!(cost.bytes, 0, "{kind} validation allocated bytes"); } } #[test] fn reserved_metadata_rejects_malformed_owned_and_borrowed_shapes() { let malformed = StructuredData::from_json(r#"{"summary":false}"#).unwrap(); assert!( EntryFrame::new( None, &EntryContent::Meta { kind: COMPACTION_KIND.to_string(), detail: malformed, }, ) .is_err() ); let bytes = [ 3, 10, 0, 0, 0, b'c', b'o', b'm', b'p', b'a', b'c', b't', b'i', b'o', b'n', 10, 0, 0, 0, 0xa1, 0x67, b's', b'u', b'm', b'm', b'a', b'r', b'y', 0xf4, ]; assert!(EntryContentView::decode(&bytes).is_err()); } #[test] fn json_view_serialization_streams_without_materializing_json() { let detail = StructuredData::from_serializable(&serde_json::Value::Array(vec![ serde_json::Value::from(-1), serde_json::Value::from(u64::MAX), serde_json::Value::Number(serde_json::Number::from_f64(-0.0).unwrap()), serde_json::Value::Number(serde_json::Number::from_f64(1.1).unwrap()), ])) .unwrap(); let expected = serde_json::to_value(&detail).unwrap(); let bytes = EntryFrame::new( None, &EntryContent::Meta { kind: "event".into(), detail, }, ) .unwrap() .content; let EntryContentView::Meta { detail, .. } = EntryContentView::decode(&bytes).unwrap() else { unreachable!(); }; let mut output = Vec::with_capacity(128); let _serial = MEASURING .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let (result, cost) = measure(Scope::Session, || { let mut serializer = serde_json::Serializer::new(&mut output); detail.serialize(&mut serializer) }); result.unwrap(); assert_eq!( cost.allocations, 0, "streaming JSON allocated {} times", cost.allocations ); let values: serde_json::Value = serde_json::from_slice(&output).unwrap(); let values = values.as_array().unwrap(); assert_eq!(values[0].as_i64(), Some(-1)); assert_eq!(values[1].as_u64(), Some(u64::MAX)); assert!(values[2].as_f64().unwrap().is_sign_negative()); assert_eq!(values[3].as_f64(), Some(1.1)); let EntryContent::Meta { detail, .. } = EntryContentView::decode(&bytes) .unwrap() .to_owned() .unwrap() else { unreachable!(); }; let detail = serde_json::to_value(&detail).unwrap(); assert_eq!(detail[0].as_i64(), Some(-1)); assert_eq!(detail[1].as_u64(), Some(u64::MAX)); assert!(detail[2].as_f64().unwrap().is_sign_negative()); assert_eq!(detail, expected); } }