1use chrono::{DateTime, TimeZone, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use std::path::PathBuf;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Project {
23 pub id: String,
24 pub worktree: PathBuf,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub vcs: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub name: Option<String>,
29 pub time_created: i64,
30 pub time_updated: i64,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub time_initialized: Option<i64>,
33 #[serde(default)]
35 pub sandboxes: Vec<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Session {
43 pub id: String,
44 pub project_id: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub workspace_id: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub parent_id: Option<String>,
49 pub slug: String,
50 pub directory: PathBuf,
51 pub title: String,
52 pub version: String,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub share_url: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub summary_additions: Option<i64>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub summary_deletions: Option<i64>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub summary_files: Option<i64>,
61 pub time_created: i64,
62 pub time_updated: i64,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub time_compacting: Option<i64>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub time_archived: Option<i64>,
67
68 #[serde(default)]
71 pub messages: Vec<Message>,
72}
73
74impl Session {
75 pub fn started_at(&self) -> Option<DateTime<Utc>> {
76 Utc.timestamp_millis_opt(self.time_created).single()
77 }
78 pub fn last_activity(&self) -> Option<DateTime<Utc>> {
79 Utc.timestamp_millis_opt(self.time_updated).single()
80 }
81
82 pub fn first_user_text(&self) -> Option<String> {
85 for msg in &self.messages {
86 if let MessageData::User(_) = &msg.data {
87 for part in &msg.parts {
88 if let PartData::Text(t) = &part.data
89 && !t.text.is_empty()
90 {
91 return Some(t.text.clone());
92 }
93 }
94 }
95 }
96 None
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SessionMetadata {
103 pub id: String,
104 pub project_id: String,
105 pub directory: PathBuf,
106 pub title: String,
107 pub version: String,
108 pub started_at: Option<DateTime<Utc>>,
109 pub last_activity: Option<DateTime<Utc>>,
110 pub message_count: usize,
111 pub first_user_message: Option<String>,
112 pub summary_additions: Option<i64>,
113 pub summary_deletions: Option<i64>,
114 pub summary_files: Option<i64>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Message {
122 pub id: String,
123 pub session_id: String,
124 pub time_created: i64,
125 pub time_updated: i64,
126 pub data: MessageData,
127 #[serde(default)]
128 pub parts: Vec<Part>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(tag = "role", rename_all = "lowercase")]
134pub enum MessageData {
135 User(UserMessage),
136 Assistant(AssistantMessage),
137 #[serde(other)]
139 Other,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct UserMessage {
144 pub time: MessageTime,
145 #[serde(default)]
146 pub agent: String,
147 pub model: ModelRef,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub format: Option<Value>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub summary: Option<UserSummary>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub system: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub tools: Option<HashMap<String, bool>>,
156 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
157 pub extra: HashMap<String, Value>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct UserSummary {
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub title: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub body: Option<String>,
166 #[serde(default)]
167 pub diffs: Vec<Value>,
168 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
169 pub extra: HashMap<String, Value>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct AssistantMessage {
174 #[serde(rename = "parentID")]
175 pub parent_id: String,
176 pub time: MessageTime,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub error: Option<Value>,
179 #[serde(default)]
180 pub agent: String,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub mode: Option<String>,
184 #[serde(rename = "modelID", default)]
185 pub model_id: String,
186 #[serde(rename = "providerID", default)]
187 pub provider_id: String,
188 pub path: MessagePath,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub summary: Option<bool>,
191 #[serde(default)]
192 pub cost: f64,
193 #[serde(default)]
194 pub tokens: Tokens,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub structured: Option<Value>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub variant: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub finish: Option<String>,
201 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
202 pub extra: HashMap<String, Value>,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct MessageTime {
207 pub created: i64,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub completed: Option<i64>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ModelRef {
214 #[serde(rename = "providerID")]
215 pub provider_id: String,
216 #[serde(rename = "modelID")]
217 pub model_id: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub variant: Option<String>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct MessagePath {
224 pub cwd: PathBuf,
225 pub root: PathBuf,
226}
227
228#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct Tokens {
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub total: Option<u64>,
232 #[serde(default)]
233 pub input: u64,
234 #[serde(default)]
235 pub output: u64,
236 #[serde(default)]
237 pub reasoning: u64,
238 #[serde(default)]
239 pub cache: TokenCache,
240}
241
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
243pub struct TokenCache {
244 #[serde(default)]
245 pub read: u64,
246 #[serde(default)]
247 pub write: u64,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct Part {
255 pub id: String,
256 pub message_id: String,
257 pub session_id: String,
258 pub time_created: i64,
259 pub time_updated: i64,
260 pub data: PartData,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(tag = "type", rename_all = "kebab-case")]
269pub enum PartData {
270 Text(TextPart),
271 Reasoning(ReasoningPart),
272 Tool(ToolPart),
273 File(FilePart),
274 Agent(AgentPart),
275 Subtask(SubtaskPart),
276 Retry(RetryPart),
277 Compaction(CompactionPart),
278 StepStart(StepStartPart),
279 StepFinish(StepFinishPart),
280 Snapshot(SnapshotPart),
281 Patch(PatchPart),
282 #[serde(other)]
286 Unknown,
287}
288
289impl PartData {
290 pub fn kind(&self) -> &'static str {
292 match self {
293 PartData::Text(_) => "text",
294 PartData::Reasoning(_) => "reasoning",
295 PartData::Tool(_) => "tool",
296 PartData::File(_) => "file",
297 PartData::Agent(_) => "agent",
298 PartData::Subtask(_) => "subtask",
299 PartData::Retry(_) => "retry",
300 PartData::Compaction(_) => "compaction",
301 PartData::StepStart(_) => "step-start",
302 PartData::StepFinish(_) => "step-finish",
303 PartData::Snapshot(_) => "snapshot",
304 PartData::Patch(_) => "patch",
305 PartData::Unknown => "unknown",
306 }
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct TextPart {
312 #[serde(default)]
313 pub text: String,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub synthetic: Option<bool>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub ignored: Option<bool>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub time: Option<TimeRange>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub metadata: Option<Value>,
322 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
323 pub extra: HashMap<String, Value>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ReasoningPart {
328 #[serde(default)]
329 pub text: String,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub time: Option<TimeRange>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub metadata: Option<Value>,
334 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
335 pub extra: HashMap<String, Value>,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339pub struct TimeRange {
340 pub start: i64,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub end: Option<i64>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct ToolPart {
347 pub tool: String,
348 #[serde(rename = "callID")]
349 pub call_id: String,
350 pub state: ToolState,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub metadata: Option<Value>,
353 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
354 pub extra: HashMap<String, Value>,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(tag = "status", rename_all = "lowercase")]
360pub enum ToolState {
361 Pending(ToolStatePending),
362 Running(ToolStateRunning),
363 Completed(ToolStateCompleted),
364 Error(ToolStateError),
365 #[serde(other)]
368 Unknown,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ToolStatePending {
373 #[serde(default)]
374 pub input: Value,
375 #[serde(default)]
376 pub raw: String,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct ToolStateRunning {
381 #[serde(default)]
382 pub input: Value,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub title: Option<String>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub metadata: Option<Value>,
387 pub time: ToolStartTime,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ToolStartTime {
392 pub start: i64,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct ToolStateCompleted {
397 #[serde(default)]
398 pub input: Value,
399 #[serde(default)]
400 pub output: String,
401 #[serde(default)]
402 pub title: String,
403 #[serde(default)]
404 pub metadata: Value,
405 pub time: ToolRunTime,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub attachments: Option<Vec<Value>>,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct ToolRunTime {
412 pub start: i64,
413 pub end: i64,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub compacted: Option<i64>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ToolStateError {
420 #[serde(default)]
421 pub input: Value,
422 #[serde(default)]
423 pub error: String,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub metadata: Option<Value>,
426 pub time: ToolRunTime,
427}
428
429impl ToolState {
430 pub fn input(&self) -> Option<&Value> {
432 match self {
433 ToolState::Pending(p) => Some(&p.input),
434 ToolState::Running(r) => Some(&r.input),
435 ToolState::Completed(c) => Some(&c.input),
436 ToolState::Error(e) => Some(&e.input),
437 ToolState::Unknown => None,
438 }
439 }
440
441 pub fn output(&self) -> Option<&str> {
442 match self {
443 ToolState::Completed(c) => Some(&c.output),
444 _ => None,
445 }
446 }
447
448 pub fn error(&self) -> Option<&str> {
449 match self {
450 ToolState::Error(e) => Some(&e.error),
451 _ => None,
452 }
453 }
454
455 pub fn is_error(&self) -> bool {
456 matches!(self, ToolState::Error(_))
457 }
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct FilePart {
462 pub mime: String,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub filename: Option<String>,
465 pub url: String,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub source: Option<Value>,
468 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
469 pub extra: HashMap<String, Value>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct AgentPart {
474 pub name: String,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub source: Option<Value>,
477 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
478 pub extra: HashMap<String, Value>,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct SubtaskPart {
483 pub prompt: String,
484 pub description: String,
485 pub agent: String,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub model: Option<ModelRef>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub command: Option<String>,
490 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
491 pub extra: HashMap<String, Value>,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct RetryPart {
496 pub attempt: u32,
497 pub error: Value,
498 pub time: RetryTime,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct RetryTime {
503 pub created: i64,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct CompactionPart {
508 pub auto: bool,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub overflow: Option<bool>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub tail_start_id: Option<String>,
513 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
514 pub extra: HashMap<String, Value>,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct StepStartPart {
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub snapshot: Option<String>,
521 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
522 pub extra: HashMap<String, Value>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct StepFinishPart {
527 pub reason: String,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub snapshot: Option<String>,
530 #[serde(default)]
531 pub cost: f64,
532 #[serde(default)]
533 pub tokens: Tokens,
534 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
535 pub extra: HashMap<String, Value>,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct SnapshotPart {
540 pub snapshot: String,
541 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
542 pub extra: HashMap<String, Value>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct PatchPart {
547 pub hash: String,
548 #[serde(default)]
549 pub files: Vec<String>,
550 #[serde(flatten, skip_serializing_if = "HashMap::is_empty", default)]
551 pub extra: HashMap<String, Value>,
552}
553
554#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn parse_user_message() {
562 let raw = r#"{"role":"user","time":{"created":1776792838512},"agent":"build","model":{"providerID":"opencode","modelID":"big-pickle"},"summary":{"diffs":[]}}"#;
563 let md: MessageData = serde_json::from_str(raw).unwrap();
564 match md {
565 MessageData::User(u) => {
566 assert_eq!(u.agent, "build");
567 assert_eq!(u.model.provider_id, "opencode");
568 assert_eq!(u.model.model_id, "big-pickle");
569 assert_eq!(u.time.created, 1776792838512);
570 }
571 _ => panic!("expected User"),
572 }
573 }
574
575 #[test]
576 fn parse_assistant_message() {
577 let raw = r#"{"parentID":"msg_x","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/p","root":"/p"},"cost":0.0,"tokens":{"total":10,"input":5,"output":3,"reasoning":2,"cache":{"read":1,"write":0}},"modelID":"m","providerID":"p","time":{"created":1,"completed":2},"finish":"stop"}"#;
578 let md: MessageData = serde_json::from_str(raw).unwrap();
579 match md {
580 MessageData::Assistant(a) => {
581 assert_eq!(a.parent_id, "msg_x");
582 assert_eq!(a.model_id, "m");
583 assert_eq!(a.provider_id, "p");
584 assert_eq!(a.finish.as_deref(), Some("stop"));
585 assert_eq!(a.tokens.input, 5);
586 assert_eq!(a.tokens.cache.read, 1);
587 }
588 _ => panic!("expected Assistant"),
589 }
590 }
591
592 #[test]
593 fn unknown_role_roundtrips_to_other() {
594 let raw = r#"{"role":"system","text":"hi"}"#;
595 let md: MessageData = serde_json::from_str(raw).unwrap();
596 assert!(matches!(md, MessageData::Other));
597 }
598
599 #[test]
600 fn parse_text_part() {
601 let raw = r#"{"type":"text","text":"hello"}"#;
602 let pd: PartData = serde_json::from_str(raw).unwrap();
603 match pd {
604 PartData::Text(t) => assert_eq!(t.text, "hello"),
605 _ => panic!("expected Text"),
606 }
607 }
608
609 #[test]
610 fn parse_reasoning_part() {
611 let raw = r#"{"type":"reasoning","text":"thinking","time":{"start":1,"end":2},"metadata":{"anthropic":{"signature":"abc"}}}"#;
612 let pd: PartData = serde_json::from_str(raw).unwrap();
613 match pd {
614 PartData::Reasoning(r) => {
615 assert_eq!(r.text, "thinking");
616 assert_eq!(r.time.as_ref().unwrap().start, 1);
617 assert_eq!(r.time.as_ref().unwrap().end, Some(2));
618 }
619 _ => panic!("expected Reasoning"),
620 }
621 }
622
623 #[test]
624 fn parse_tool_part_completed() {
625 let raw = r#"{"type":"tool","tool":"bash","callID":"c1","state":{"status":"completed","input":{"command":"ls"},"output":"a\nb\n","title":"List","metadata":{"exit":0},"time":{"start":1,"end":2}}}"#;
626 let pd: PartData = serde_json::from_str(raw).unwrap();
627 match pd {
628 PartData::Tool(t) => {
629 assert_eq!(t.tool, "bash");
630 assert_eq!(t.call_id, "c1");
631 match &t.state {
632 ToolState::Completed(c) => {
633 assert_eq!(c.output, "a\nb\n");
634 assert_eq!(c.title, "List");
635 assert_eq!(c.metadata["exit"], 0);
636 }
637 _ => panic!("expected Completed"),
638 }
639 }
640 _ => panic!("expected Tool"),
641 }
642 }
643
644 #[test]
645 fn parse_tool_part_error() {
646 let raw = r#"{"type":"tool","tool":"bash","callID":"c","state":{"status":"error","input":{"command":"false"},"error":"exit 1","time":{"start":1,"end":2}}}"#;
647 let pd: PartData = serde_json::from_str(raw).unwrap();
648 match pd {
649 PartData::Tool(t) => match &t.state {
650 ToolState::Error(e) => {
651 assert_eq!(e.error, "exit 1");
652 }
653 _ => panic!("expected Error"),
654 },
655 _ => panic!("expected Tool"),
656 }
657 }
658
659 #[test]
660 fn parse_step_parts() {
661 let raw_start = r#"{"type":"step-start","snapshot":"abc"}"#;
662 let pd: PartData = serde_json::from_str(raw_start).unwrap();
663 assert!(matches!(pd, PartData::StepStart(_)));
664
665 let raw_finish = r#"{"type":"step-finish","reason":"stop","snapshot":"abc","tokens":{"input":1,"output":2,"reasoning":0,"cache":{"read":0,"write":0}},"cost":0.001}"#;
666 let pd: PartData = serde_json::from_str(raw_finish).unwrap();
667 match pd {
668 PartData::StepFinish(s) => {
669 assert_eq!(s.reason, "stop");
670 assert_eq!(s.snapshot.as_deref(), Some("abc"));
671 }
672 _ => panic!("expected StepFinish"),
673 }
674 }
675
676 #[test]
677 fn parse_unknown_part_type() {
678 let raw = r#"{"type":"future-thing","foo":"bar"}"#;
679 let pd: PartData = serde_json::from_str(raw).unwrap();
680 assert!(matches!(pd, PartData::Unknown));
681 assert_eq!(pd.kind(), "unknown");
682 }
683
684 #[test]
685 fn parse_compaction_and_retry() {
686 let c: PartData =
687 serde_json::from_str(r#"{"type":"compaction","auto":true,"overflow":false}"#).unwrap();
688 assert!(matches!(c, PartData::Compaction(_)));
689
690 let r: PartData = serde_json::from_str(
691 r#"{"type":"retry","attempt":1,"error":{"message":"nope"},"time":{"created":1}}"#,
692 )
693 .unwrap();
694 assert!(matches!(r, PartData::Retry(_)));
695 }
696
697 #[test]
698 fn tokens_roundtrip() {
699 let raw = r#"{"total":10,"input":5,"output":3,"reasoning":2,"cache":{"read":1,"write":0}}"#;
700 let t: Tokens = serde_json::from_str(raw).unwrap();
701 let back = serde_json::to_value(&t).unwrap();
702 let orig: serde_json::Value = serde_json::from_str(raw).unwrap();
703 assert_eq!(orig, back);
704 }
705
706 #[test]
707 fn message_extras_survive() {
708 let raw = r#"{"role":"user","time":{"created":1},"agent":"build","model":{"providerID":"p","modelID":"m"},"future_field":"kept"}"#;
709 let md: MessageData = serde_json::from_str(raw).unwrap();
710 match md {
711 MessageData::User(u) => {
712 assert_eq!(
713 u.extra.get("future_field"),
714 Some(&serde_json::json!("kept"))
715 );
716 }
717 _ => panic!("expected User"),
718 }
719 }
720}