shadow_core/agentlog/
writer.rs1use std::io::Write;
9
10use crate::agentlog::Record;
11
12pub fn write_record<W: Write>(writer: &mut W, record: &Record) -> std::io::Result<()> {
14 serde_json::to_writer(&mut *writer, record)?;
15 writer.write_all(b"\n")?;
16 Ok(())
17}
18
19pub fn write_all<'a, W: Write, I: IntoIterator<Item = &'a Record>>(
26 writer: &mut W,
27 records: I,
28) -> std::io::Result<()> {
29 for r in records {
30 write_record(writer, r)?;
31 }
32 Ok(())
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38 use crate::agentlog::parser::parse_all;
39 use crate::agentlog::{Kind, Record};
40 use serde_json::json;
41 use std::io::Cursor;
42
43 fn sample(kind: Kind, payload: serde_json::Value) -> Record {
44 Record::new(kind, payload, "2026-04-21T10:00:00Z", None)
45 }
46
47 #[test]
48 fn writes_one_record_with_trailing_newline() {
49 let r = sample(Kind::ChatRequest, json!({"model": "a"}));
50 let mut buf = Vec::new();
51 write_record(&mut buf, &r).unwrap();
52 assert_eq!(buf.last(), Some(&b'\n'));
53 assert_eq!(buf.iter().filter(|b| **b == b'\n').count(), 1);
55 }
56
57 #[test]
58 fn roundtrips_through_parser() {
59 let records = vec![
60 sample(Kind::Metadata, json!({"sdk": {"name": "shadow"}})),
61 sample(Kind::ChatRequest, json!({"model": "a"})),
62 sample(
63 Kind::ChatResponse,
64 json!({"model": "a", "stop_reason": "end_turn"}),
65 ),
66 sample(
67 Kind::ToolCall,
68 json!({"tool_name": "search", "tool_call_id": "t1", "arguments": {}}),
69 ),
70 ];
71 let mut buf = Vec::new();
72 write_all(&mut buf, &records).unwrap();
73 let back = parse_all(Cursor::new(buf)).unwrap();
74 assert_eq!(back, records);
75 }
76
77 #[test]
78 fn roundtrip_preserves_content_id() {
79 let r = sample(Kind::ChatRequest, json!({"b": 2, "a": 1}));
82 let before = r.id.clone();
83 let mut buf = Vec::new();
84 write_record(&mut buf, &r).unwrap();
85 let back = parse_all(Cursor::new(buf)).unwrap();
86 assert_eq!(back[0].id, before);
87 assert!(back[0].verify_id());
88 }
89
90 #[test]
91 fn empty_input_writes_nothing() {
92 let mut buf = Vec::new();
93 let empty: Vec<&Record> = Vec::new();
94 write_all(&mut buf, empty).unwrap();
95 assert!(buf.is_empty());
96 }
97
98 #[test]
99 fn records_are_separated_by_exactly_one_newline() {
100 let records = vec![
101 sample(Kind::ChatRequest, json!({"n": 1})),
102 sample(Kind::ChatRequest, json!({"n": 2})),
103 ];
104 let mut buf = Vec::new();
105 write_all(&mut buf, &records).unwrap();
106 let s = std::str::from_utf8(&buf).unwrap();
107 assert_eq!(s.matches('\n').count(), 2);
109 assert!(!s.contains("\n\n"));
111 }
112}