1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
8pub enum MessageRole {
9 User,
10 Agent,
11 Tool,
12 System,
13 Other(String),
14}
15
16impl std::fmt::Display for MessageRole {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 MessageRole::User => write!(f, "User"),
20 MessageRole::Agent => write!(f, "Agent"),
21 MessageRole::Tool => write!(f, "Tool"),
22 MessageRole::System => write!(f, "System"),
23 MessageRole::Other(s) => write!(f, "{}", s),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Agent {
30 pub id: Option<i64>,
31 pub slug: String,
32 pub name: String,
33 pub version: Option<String>,
34 pub kind: AgentKind,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub enum AgentKind {
39 Cli,
40 VsCode,
41 Hybrid,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Workspace {
46 pub id: Option<i64>,
47 pub path: PathBuf,
48 pub display_name: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Conversation {
53 pub id: Option<i64>,
54 pub agent_slug: String,
55 pub workspace: Option<PathBuf>,
56 pub external_id: Option<String>,
57 pub title: Option<String>,
58 pub source_path: PathBuf,
59 pub started_at: Option<i64>,
60 pub ended_at: Option<i64>,
61 pub approx_tokens: Option<i64>,
62 pub metadata_json: serde_json::Value,
63 pub messages: Vec<Message>,
64 #[serde(default = "default_source_id")]
67 pub source_id: String,
68 #[serde(default)]
70 pub origin_host: Option<String>,
71}
72
73fn default_source_id() -> String {
74 "local".to_string()
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Message {
79 pub id: Option<i64>,
80 pub idx: i64,
81 pub role: MessageRole,
82 pub author: Option<String>,
83 pub created_at: Option<i64>,
84 pub content: String,
85 pub extra_json: serde_json::Value,
86 pub snippets: Vec<Snippet>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Snippet {
91 pub id: Option<i64>,
92 pub file_path: Option<PathBuf>,
93 pub start_line: Option<i64>,
94 pub end_line: Option<i64>,
95 pub language: Option<String>,
96 pub snippet_text: Option<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Tag {
101 pub id: Option<i64>,
102 pub name: String,
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde_json::{from_value, json, to_value};
109
110 fn message_fixture(content: impl Into<String>) -> Message {
111 Message {
112 id: None,
113 idx: 0,
114 role: MessageRole::User,
115 author: None,
116 created_at: None,
117 content: content.into(),
118 extra_json: json!(null),
119 snippets: vec![],
120 }
121 }
122
123 fn conversation_fixture(agent_slug: &str, source_path: &str) -> Conversation {
124 Conversation {
125 id: None,
126 agent_slug: agent_slug.to_string(),
127 workspace: None,
128 external_id: None,
129 title: None,
130 source_path: PathBuf::from(source_path),
131 started_at: None,
132 ended_at: None,
133 approx_tokens: None,
134 metadata_json: json!(null),
135 messages: vec![],
136 source_id: "local".to_string(),
137 origin_host: None,
138 }
139 }
140
141 #[test]
146 fn message_role_display() {
147 let cases = [
148 (MessageRole::User, "User"),
149 (MessageRole::Agent, "Agent"),
150 (MessageRole::Tool, "Tool"),
151 (MessageRole::System, "System"),
152 (MessageRole::Other("Custom".to_string()), "Custom"),
153 (MessageRole::Other("".to_string()), ""),
154 (MessageRole::Other("日本語".to_string()), "日本語"),
155 ];
156
157 for (role, expected_display) in cases {
158 let actual_display = role.to_string();
159 assert_eq!(actual_display, expected_display, "role: {role:?}");
160 }
161 }
162
163 #[test]
164 fn message_role_serde_roundtrip() {
165 let roles = vec![
166 MessageRole::User,
167 MessageRole::Agent,
168 MessageRole::Tool,
169 MessageRole::System,
170 MessageRole::Other("CustomRole".to_string()),
171 ];
172
173 for role in roles {
174 let serialized = to_value(&role).unwrap();
175 let deserialized: MessageRole = from_value(serialized).unwrap();
176 assert_eq!(role, deserialized);
177 }
178 }
179
180 #[test]
181 fn message_role_equality() {
182 assert_eq!(MessageRole::User, MessageRole::User);
183 assert_ne!(MessageRole::User, MessageRole::Agent);
184 assert_eq!(
185 MessageRole::Other("x".to_string()),
186 MessageRole::Other("x".to_string())
187 );
188 assert_ne!(
189 MessageRole::Other("x".to_string()),
190 MessageRole::Other("y".to_string())
191 );
192 }
193
194 #[test]
199 fn agent_kind_serde_roundtrip() {
200 let kinds = vec![AgentKind::Cli, AgentKind::VsCode, AgentKind::Hybrid];
201
202 for kind in kinds {
203 let serialized = to_value(&kind).unwrap();
204 let deserialized: AgentKind = from_value(serialized).unwrap();
205 assert_eq!(kind, deserialized);
206 }
207 }
208
209 #[test]
210 fn agent_kind_equality() {
211 assert_eq!(AgentKind::Cli, AgentKind::Cli);
212 assert_ne!(AgentKind::Cli, AgentKind::VsCode);
213 assert_ne!(AgentKind::VsCode, AgentKind::Hybrid);
214 }
215
216 #[test]
221 fn agent_serde_roundtrip() {
222 let agent = Agent {
223 id: Some(42),
224 slug: "claude-code".to_string(),
225 name: "Claude Code".to_string(),
226 version: Some("1.0.0".to_string()),
227 kind: AgentKind::Cli,
228 };
229
230 let json = serde_json::to_string(&agent).unwrap();
231 let deserialized: Agent = serde_json::from_str(&json).unwrap();
232
233 assert_eq!(deserialized.id, Some(42));
234 assert_eq!(deserialized.slug, "claude-code");
235 assert_eq!(deserialized.name, "Claude Code");
236 assert_eq!(deserialized.version, Some("1.0.0".to_string()));
237 assert_eq!(deserialized.kind, AgentKind::Cli);
238 }
239
240 #[test]
241 fn agent_with_none_fields() {
242 let agent = Agent {
243 id: None,
244 slug: "test".to_string(),
245 name: "Test".to_string(),
246 version: None,
247 kind: AgentKind::VsCode,
248 };
249
250 let json = serde_json::to_string(&agent).unwrap();
251 let deserialized: Agent = serde_json::from_str(&json).unwrap();
252
253 assert!(deserialized.id.is_none());
254 assert!(deserialized.version.is_none());
255 }
256
257 #[test]
262 fn workspace_serde_roundtrip() {
263 let workspace = Workspace {
264 id: Some(1),
265 path: PathBuf::from("/home/user/project"),
266 display_name: Some("My Project".to_string()),
267 };
268
269 let json = serde_json::to_string(&workspace).unwrap();
270 let deserialized: Workspace = serde_json::from_str(&json).unwrap();
271
272 assert_eq!(deserialized.id, Some(1));
273 assert_eq!(deserialized.path, PathBuf::from("/home/user/project"));
274 assert_eq!(deserialized.display_name, Some("My Project".to_string()));
275 }
276
277 #[test]
278 fn workspace_with_unicode_path() {
279 let workspace = Workspace {
280 id: None,
281 path: PathBuf::from("/home/用户/プロジェクト"),
282 display_name: Some("日本語プロジェクト".to_string()),
283 };
284
285 let json = serde_json::to_string(&workspace).unwrap();
286 let deserialized: Workspace = serde_json::from_str(&json).unwrap();
287
288 assert_eq!(deserialized.path, PathBuf::from("/home/用户/プロジェクト"));
289 assert_eq!(
290 deserialized.display_name,
291 Some("日本語プロジェクト".to_string())
292 );
293 }
294
295 #[test]
300 fn tag_serde_roundtrip() {
301 let tag = Tag {
302 id: Some(100),
303 name: "important".to_string(),
304 };
305
306 let json = serde_json::to_string(&tag).unwrap();
307 let deserialized: Tag = serde_json::from_str(&json).unwrap();
308
309 assert_eq!(deserialized.id, Some(100));
310 assert_eq!(deserialized.name, "important");
311 }
312
313 #[test]
314 fn tag_with_empty_name() {
315 let tag = Tag {
316 id: None,
317 name: "".to_string(),
318 };
319
320 let json = serde_json::to_string(&tag).unwrap();
321 let deserialized: Tag = serde_json::from_str(&json).unwrap();
322
323 assert_eq!(deserialized.name, "");
324 }
325
326 #[test]
331 fn snippet_serde_roundtrip() {
332 let snippet = Snippet {
333 id: Some(1),
334 file_path: Some(PathBuf::from("src/main.rs")),
335 start_line: Some(10),
336 end_line: Some(20),
337 language: Some("rust".to_string()),
338 snippet_text: Some("fn main() {}".to_string()),
339 };
340
341 let json = serde_json::to_string(&snippet).unwrap();
342 let deserialized: Snippet = serde_json::from_str(&json).unwrap();
343
344 assert_eq!(deserialized.id, Some(1));
345 assert_eq!(deserialized.file_path, Some(PathBuf::from("src/main.rs")));
346 assert_eq!(deserialized.start_line, Some(10));
347 assert_eq!(deserialized.end_line, Some(20));
348 assert_eq!(deserialized.language, Some("rust".to_string()));
349 assert_eq!(deserialized.snippet_text, Some("fn main() {}".to_string()));
350 }
351
352 #[test]
353 fn snippet_all_none() {
354 let snippet = Snippet {
355 id: None,
356 file_path: None,
357 start_line: None,
358 end_line: None,
359 language: None,
360 snippet_text: None,
361 };
362
363 let json = serde_json::to_string(&snippet).unwrap();
364 let deserialized: Snippet = serde_json::from_str(&json).unwrap();
365
366 assert!(deserialized.id.is_none());
367 assert!(deserialized.file_path.is_none());
368 assert!(deserialized.start_line.is_none());
369 assert!(deserialized.end_line.is_none());
370 assert!(deserialized.language.is_none());
371 assert!(deserialized.snippet_text.is_none());
372 }
373
374 #[test]
379 fn message_serde_roundtrip() {
380 let message = Message {
381 id: Some(42),
382 idx: 0,
383 role: MessageRole::User,
384 author: Some("human".to_string()),
385 created_at: Some(1700000000000),
386 content: "Hello, world!".to_string(),
387 extra_json: json!({"key": "value"}),
388 snippets: vec![],
389 };
390
391 let json = serde_json::to_string(&message).unwrap();
392 let deserialized: Message = serde_json::from_str(&json).unwrap();
393
394 assert_eq!(deserialized.id, Some(42));
395 assert_eq!(deserialized.idx, 0);
396 assert_eq!(deserialized.role, MessageRole::User);
397 assert_eq!(deserialized.author, Some("human".to_string()));
398 assert_eq!(deserialized.created_at, Some(1700000000000));
399 assert_eq!(deserialized.content, "Hello, world!");
400 assert_eq!(deserialized.extra_json, json!({"key": "value"}));
401 assert!(deserialized.snippets.is_empty());
402 }
403
404 #[test]
405 fn message_with_snippets() {
406 let snippet = Snippet {
407 id: None,
408 file_path: Some(PathBuf::from("test.rs")),
409 start_line: Some(1),
410 end_line: Some(5),
411 language: Some("rust".to_string()),
412 snippet_text: Some("code".to_string()),
413 };
414
415 let mut message = message_fixture("Here's some code");
416 message.idx = 1;
417 message.role = MessageRole::Agent;
418 message.snippets = vec![snippet];
419
420 let json = serde_json::to_string(&message).unwrap();
421 let deserialized: Message = serde_json::from_str(&json).unwrap();
422
423 assert_eq!(deserialized.snippets.len(), 1);
424 assert_eq!(deserialized.snippets[0].language, Some("rust".to_string()));
425 }
426
427 #[test]
428 fn message_with_unicode_content() {
429 let mut message = message_fixture("こんにちは世界!🌍");
430 message.author = Some("ユーザー".to_string());
431 message.extra_json = json!({"emoji": "🎉"});
432
433 let json = serde_json::to_string(&message).unwrap();
434 let deserialized: Message = serde_json::from_str(&json).unwrap();
435
436 assert_eq!(deserialized.content, "こんにちは世界!🌍");
437 assert_eq!(deserialized.author, Some("ユーザー".to_string()));
438 }
439
440 #[test]
445 fn conversation_serde_roundtrip() {
446 let conversation = Conversation {
447 id: Some(1),
448 agent_slug: "claude-code".to_string(),
449 workspace: Some(PathBuf::from("/project")),
450 external_id: Some("ext-123".to_string()),
451 title: Some("Test Conversation".to_string()),
452 source_path: PathBuf::from("/path/to/session.jsonl"),
453 started_at: Some(1700000000000),
454 ended_at: Some(1700003600000),
455 approx_tokens: Some(1000),
456 metadata_json: json!({"model": "claude-3"}),
457 messages: vec![],
458 source_id: "local".to_string(),
459 origin_host: None,
460 };
461
462 let json = serde_json::to_string(&conversation).unwrap();
463 let deserialized: Conversation = serde_json::from_str(&json).unwrap();
464
465 assert_eq!(deserialized.id, Some(1));
466 assert_eq!(deserialized.agent_slug, "claude-code");
467 assert_eq!(deserialized.workspace, Some(PathBuf::from("/project")));
468 assert_eq!(deserialized.external_id, Some("ext-123".to_string()));
469 assert_eq!(deserialized.title, Some("Test Conversation".to_string()));
470 assert_eq!(
471 deserialized.source_path,
472 PathBuf::from("/path/to/session.jsonl")
473 );
474 assert_eq!(deserialized.started_at, Some(1700000000000));
475 assert_eq!(deserialized.ended_at, Some(1700003600000));
476 assert_eq!(deserialized.approx_tokens, Some(1000));
477 assert_eq!(deserialized.source_id, "local");
478 assert!(deserialized.origin_host.is_none());
479 }
480
481 #[test]
482 fn conversation_source_id_default() {
483 let json = json!({
485 "agent_slug": "test",
486 "source_path": "/test.jsonl",
487 "metadata_json": {},
488 "messages": []
489 });
490
491 let conversation: Conversation = from_value(json).unwrap();
492 assert_eq!(conversation.source_id, "local");
493 }
494
495 #[test]
496 fn conversation_with_remote_source() {
497 let mut conversation = conversation_fixture("codex", "/remote/session.jsonl");
498 conversation.source_id = "work-laptop".to_string();
499 conversation.origin_host = Some("laptop.local".to_string());
500
501 let json = serde_json::to_string(&conversation).unwrap();
502 let deserialized: Conversation = serde_json::from_str(&json).unwrap();
503
504 assert_eq!(deserialized.source_id, "work-laptop");
505 assert_eq!(deserialized.origin_host, Some("laptop.local".to_string()));
506 }
507
508 #[test]
509 fn conversation_with_messages() {
510 let mut conversation = conversation_fixture("test", "/test.jsonl");
511 conversation.messages = vec![message_fixture("Hello")];
512
513 let json = serde_json::to_string(&conversation).unwrap();
514 let deserialized: Conversation = serde_json::from_str(&json).unwrap();
515
516 assert_eq!(deserialized.messages.len(), 1);
517 assert_eq!(deserialized.messages[0].content, "Hello");
518 }
519
520 #[test]
525 fn empty_strings_are_valid() {
526 let tag = Tag {
527 id: None,
528 name: "".to_string(),
529 };
530 let agent = Agent {
531 id: None,
532 slug: "".to_string(),
533 name: "".to_string(),
534 version: Some("".to_string()),
535 kind: AgentKind::Cli,
536 };
537
538 let tag_json = serde_json::to_string(&tag).unwrap();
540 let _: Tag = serde_json::from_str(&tag_json).unwrap();
541
542 let agent_json = serde_json::to_string(&agent).unwrap();
543 let _: Agent = serde_json::from_str(&agent_json).unwrap();
544 }
545
546 #[test]
547 fn large_content_strings() {
548 let large_content = "x".repeat(100_000);
549 let mut message = message_fixture(large_content.clone());
550 message.role = MessageRole::Agent;
551
552 let json = serde_json::to_string(&message).unwrap();
553 let deserialized: Message = serde_json::from_str(&json).unwrap();
554
555 assert_eq!(deserialized.content.len(), 100_000);
556 }
557
558 #[test]
559 fn special_characters_in_strings() {
560 let content = "Hello\nWorld\t\"quoted\"\r\nbackslash\\end";
561 let message = message_fixture(content);
562
563 let json = serde_json::to_string(&message).unwrap();
564 let deserialized: Message = serde_json::from_str(&json).unwrap();
565
566 assert_eq!(deserialized.content, content);
567 }
568
569 #[test]
570 fn negative_line_numbers() {
571 let snippet = Snippet {
573 id: Some(-1),
574 file_path: None,
575 start_line: Some(-10),
576 end_line: Some(-5),
577 language: None,
578 snippet_text: None,
579 };
580
581 let json = serde_json::to_string(&snippet).unwrap();
582 let deserialized: Snippet = serde_json::from_str(&json).unwrap();
583
584 assert_eq!(deserialized.start_line, Some(-10));
585 assert_eq!(deserialized.end_line, Some(-5));
586 }
587
588 #[test]
589 fn complex_metadata_json() {
590 let metadata = json!({
591 "nested": {
592 "array": [1, 2, 3],
593 "object": {"key": "value"},
594 "null": null,
595 "bool": true,
596 "number": 42.5
597 }
598 });
599
600 let mut conversation = conversation_fixture("test", "/test.jsonl");
601 conversation.metadata_json = metadata.clone();
602
603 let json = serde_json::to_string(&conversation).unwrap();
604 let deserialized: Conversation = serde_json::from_str(&json).unwrap();
605
606 assert_eq!(deserialized.metadata_json, metadata);
607 }
608}