1use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "camelCase")]
15pub enum CompressionTrigger {
16 Auto,
18 Manual,
20 InlineCompaction,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
35#[serde(tag = "phase", rename_all = "snake_case")]
36pub enum CompressionEvent {
37 Preparing {
39 session_id: u64,
41 tokens_before: usize,
43 msg_count: usize,
45 trigger: CompressionTrigger,
47 },
48 Started {
50 session_id: u64,
52 tokens_before: usize,
54 msg_count: usize,
56 trigger: CompressionTrigger,
58 },
59 Progress {
61 session_id: u64,
63 chars: usize,
65 },
66 Completed {
68 session_id: u64,
70 tokens_before: usize,
72 tokens_after: usize,
74 reduction_pct: i32,
76 msg_count_before: usize,
78 msg_count_after: usize,
80 trigger: CompressionTrigger,
82 },
83 Failed {
85 session_id: u64,
87 error: String,
89 trigger: CompressionTrigger,
91 },
92}
93
94impl CompressionEvent {
95 pub fn into_user_event(self) -> agent_base::UserEvent {
97 agent_base::UserEvent::Structured {
98 event_type: "compression".to_string(),
99 data: serde_json::to_value(self).expect("CompressionEvent is always serializable"),
100 }
101 }
102
103 pub fn from_user_event(event: &agent_base::UserEvent) -> Option<Self> {
108 match event {
109 agent_base::UserEvent::Structured { event_type, data }
110 if event_type == "compression" =>
111 {
112 serde_json::from_value(data.clone()).ok()
113 }
114 _ => None,
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
126 fn test_preparing_roundtrip() {
127 let event = CompressionEvent::Preparing {
128 session_id: 42,
129 tokens_before: 4200,
130 msg_count: 8,
131 trigger: CompressionTrigger::Auto,
132 };
133 let json = serde_json::to_string(&event).unwrap();
134 let parsed: CompressionEvent = serde_json::from_str(&json).unwrap();
135 assert_eq!(event, parsed);
136 }
137
138 #[test]
139 fn test_started_roundtrip() {
140 let event = CompressionEvent::Started {
141 session_id: 42,
142 tokens_before: 4200,
143 msg_count: 8,
144 trigger: CompressionTrigger::Manual,
145 };
146 let json = serde_json::to_string(&event).unwrap();
147 let parsed: CompressionEvent = serde_json::from_str(&json).unwrap();
148 assert_eq!(event, parsed);
149 }
150
151 #[test]
152 fn test_progress_roundtrip() {
153 let event = CompressionEvent::Progress {
154 session_id: 42,
155 chars: 755,
156 };
157 let json = serde_json::to_string(&event).unwrap();
158 let parsed: CompressionEvent = serde_json::from_str(&json).unwrap();
159 assert_eq!(event, parsed);
160 }
161
162 #[test]
163 fn test_completed_roundtrip() {
164 let event = CompressionEvent::Completed {
165 session_id: 42,
166 tokens_before: 4200,
167 tokens_after: 3800,
168 reduction_pct: 10,
169 msg_count_before: 20,
170 msg_count_after: 8,
171 trigger: CompressionTrigger::Auto,
172 };
173 let json = serde_json::to_string(&event).unwrap();
174 let parsed: CompressionEvent = serde_json::from_str(&json).unwrap();
175 assert_eq!(event, parsed);
176 }
177
178 #[test]
179 fn test_failed_roundtrip() {
180 let event = CompressionEvent::Failed {
181 session_id: 42,
182 error: "LLM timeout".to_string(),
183 trigger: CompressionTrigger::Manual,
184 };
185 let json = serde_json::to_string(&event).unwrap();
186 let parsed: CompressionEvent = serde_json::from_str(&json).unwrap();
187 assert_eq!(event, parsed);
188 }
189
190 #[test]
193 fn test_json_uses_snake_case() {
194 let event = CompressionEvent::Completed {
195 session_id: 1,
196 tokens_before: 100,
197 tokens_after: 50,
198 reduction_pct: 50,
199 msg_count_before: 10,
200 msg_count_after: 5,
201 trigger: CompressionTrigger::Auto,
202 };
203 let json = serde_json::to_value(&event).unwrap();
204
205 assert_eq!(json["phase"], "completed");
207 assert!(json.get("tokens_before").is_some(), "expected snake_case");
209 assert!(
210 json.get("tokensAfter").is_none(),
211 "camelCase must not appear"
212 );
213 assert!(json.get("msg_count_before").is_some());
214 assert!(json.get("msg_count_after").is_some());
215 assert!(json.get("reduction_pct").is_some());
216 assert!(json.get("session_id").is_some());
217 }
218
219 #[test]
220 fn test_trigger_json_values() {
221 let auto = serde_json::to_value(&CompressionTrigger::Auto).unwrap();
222 assert_eq!(auto, "auto");
223
224 let manual = serde_json::to_value(&CompressionTrigger::Manual).unwrap();
225 assert_eq!(manual, "manual");
226 }
227
228 #[test]
231 fn test_into_user_event_and_back() {
232 let event = CompressionEvent::Preparing {
233 session_id: 42,
234 tokens_before: 4200,
235 msg_count: 8,
236 trigger: CompressionTrigger::Auto,
237 };
238 let user_event = event.clone().into_user_event();
239
240 match &user_event {
242 agent_base::UserEvent::Structured { event_type, .. } => {
243 assert_eq!(event_type, "compression");
244 }
245 other => panic!("expected Structured, got {:?}", other),
246 }
247
248 let recovered = CompressionEvent::from_user_event(&user_event).unwrap();
250 assert_eq!(event, recovered);
251 }
252
253 #[test]
254 fn test_from_user_event_ignores_non_compression() {
255 let other = agent_base::UserEvent::Structured {
256 event_type: "other".to_string(),
257 data: serde_json::json!({}),
258 };
259 assert!(CompressionEvent::from_user_event(&other).is_none());
260 }
261
262 #[test]
263 fn test_from_user_event_returns_none_on_malformed_data() {
264 let bad = agent_base::UserEvent::Structured {
265 event_type: "compression".to_string(),
266 data: serde_json::json!({ "phase": "unknown_phase" }),
267 };
268 assert!(CompressionEvent::from_user_event(&bad).is_none());
269 }
270
271 #[test]
274 fn test_deserialize_progress_from_json() {
275 let json = serde_json::json!({
277 "phase": "progress",
278 "session_id": 1,
279 "chars": 42,
280 });
281 let event: CompressionEvent = serde_json::from_value(json).unwrap();
282 assert_eq!(
283 event,
284 CompressionEvent::Progress {
285 session_id: 1,
286 chars: 42
287 }
288 );
289 }
290
291 #[test]
292 fn test_deserialize_start_from_json() {
293 let json = serde_json::json!({
294 "phase": "start",
295 "tokens_before": 4200,
296 "msg_count": 8,
297 });
298 let result: Result<CompressionEvent, _> = serde_json::from_value(json);
301 assert!(result.is_err(), "\"start\" should not match \"started\"");
303 }
304
305 mod proptest_tests {
308 use super::*;
309 use proptest::prelude::*;
310
311 fn arb_trigger() -> impl Strategy<Value = CompressionTrigger> {
313 prop_oneof![
314 Just(CompressionTrigger::Auto),
315 Just(CompressionTrigger::Manual),
316 Just(CompressionTrigger::InlineCompaction),
317 ]
318 }
319
320 fn arb_event() -> impl Strategy<Value = CompressionEvent> {
322 prop_oneof![
323 (0u64..10000, 0usize..100000, 0usize..1000, arb_trigger()).prop_map(
324 |(sid, tokens, msgs, trigger)| CompressionEvent::Preparing {
325 session_id: sid,
326 tokens_before: tokens,
327 msg_count: msgs,
328 trigger,
329 }
330 ),
331 (0u64..10000, 0usize..100000, 0usize..1000, arb_trigger()).prop_map(
332 |(sid, tokens, msgs, trigger)| CompressionEvent::Started {
333 session_id: sid,
334 tokens_before: tokens,
335 msg_count: msgs,
336 trigger,
337 }
338 ),
339 (0u64..10000, 0usize..100000).prop_map(|(sid, chars)| CompressionEvent::Progress {
340 session_id: sid,
341 chars,
342 }),
343 (
344 0u64..10000,
345 0usize..100000,
346 0usize..100000,
347 -100i32..100,
348 0usize..1000,
349 0usize..1000,
350 arb_trigger()
351 )
352 .prop_map(|(sid, tb, ta, pct, mb, ma, trigger)| {
353 CompressionEvent::Completed {
354 session_id: sid,
355 tokens_before: tb,
356 tokens_after: ta,
357 reduction_pct: pct,
358 msg_count_before: mb,
359 msg_count_after: ma,
360 trigger,
361 }
362 }),
363 (0u64..10000, "[a-z ]{0,50}", arb_trigger()).prop_map(|(sid, err, trigger)| {
364 CompressionEvent::Failed {
365 session_id: sid,
366 error: err,
367 trigger,
368 }
369 }),
370 ]
371 }
372
373 proptest! {
374 #[test]
375 fn serde_roundtrip(event in arb_event()) {
376 let json = serde_json::to_value(&event).unwrap();
377 let reparsed: CompressionEvent = serde_json::from_value(json).unwrap();
378 assert_eq!(event, reparsed, "serde roundtrip failed");
379 }
380
381 #[test]
382 fn json_keys_are_snake_case(event in arb_event()) {
383 let json = serde_json::to_value(&event).unwrap();
384 if let Some(obj) = json.as_object() {
385 for key in obj.keys() {
386 assert!(!key.chars().any(|c| c.is_ascii_uppercase()),
388 "key {:?} is not snake_case in {:?}", key, json);
389 }
390 }
391 }
392
393 #[test]
394 fn phase_tag_always_present(event in arb_event()) {
395 let json = serde_json::to_value(&event).unwrap();
396 assert!(json.get("phase").is_some(),
397 "missing 'phase' tag in {:?}", json);
398 assert!(json["phase"].is_string(),
399 "'phase' should be a string in {:?}", json);
400 }
401 }
402 }
403}