1package db
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "shelley.exe.dev/db/generated"
10)
11
12func TestConversationService_Create(t *testing.T) {
13 db := setupTestDB(t)
14 defer db.Close()
15
16 // Using db directly instead of service
17 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
18 defer cancel()
19
20 tests := []struct {
21 name string
22 slug *string
23 }{
24 {
25 name: "with slug",
26 slug: stringPtr("test-conversation"),
27 },
28 {
29 name: "without slug",
30 slug: nil,
31 },
32 }
33
34 for _, tt := range tests {
35 t.Run(tt.name, func(t *testing.T) {
36 conv, err := db.CreateConversation(ctx, tt.slug, true, nil, nil)
37 if err != nil {
38 t.Errorf("Create() error = %v", err)
39 return
40 }
41
42 if conv.ConversationID == "" {
43 t.Error("Expected non-empty conversation ID")
44 }
45
46 if tt.slug != nil {
47 if conv.Slug == nil || *conv.Slug != *tt.slug {
48 t.Errorf("Expected slug %v, got %v", tt.slug, conv.Slug)
49 }
50 } else {
51 if conv.Slug != nil {
52 t.Errorf("Expected nil slug, got %v", conv.Slug)
53 }
54 }
55
56 if conv.CreatedAt.IsZero() {
57 t.Error("Expected non-zero created_at time")
58 }
59
60 if conv.UpdatedAt.IsZero() {
61 t.Error("Expected non-zero updated_at time")
62 }
63 })
64 }
65}
66
67func TestConversationService_GetByID(t *testing.T) {
68 db := setupTestDB(t)
69 defer db.Close()
70
71 // Using db directly instead of service
72 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
73 defer cancel()
74
75 // Create a test conversation
76 created, err := db.CreateConversation(ctx, stringPtr("test-conversation"), true, nil, nil)
77 if err != nil {
78 t.Fatalf("Failed to create test conversation: %v", err)
79 }
80
81 // Test getting existing conversation
82 conv, err := db.GetConversationByID(ctx, created.ConversationID)
83 if err != nil {
84 t.Errorf("GetByID() error = %v", err)
85 return
86 }
87
88 if conv.ConversationID != created.ConversationID {
89 t.Errorf("Expected conversation ID %s, got %s", created.ConversationID, conv.ConversationID)
90 }
91
92 // Test getting non-existent conversation
93 _, err = db.GetConversationByID(ctx, "non-existent")
94 if err == nil {
95 t.Error("Expected error for non-existent conversation")
96 }
97 if !strings.Contains(err.Error(), "not found") {
98 t.Errorf("Expected 'not found' in error message, got: %v", err)
99 }
100}
101
102func TestConversationService_GetBySlug(t *testing.T) {
103 db := setupTestDB(t)
104 defer db.Close()
105
106 // Using db directly instead of service
107 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
108 defer cancel()
109
110 // Create a test conversation with slug
111 created, err := db.CreateConversation(ctx, stringPtr("test-slug"), true, nil, nil)
112 if err != nil {
113 t.Fatalf("Failed to create test conversation: %v", err)
114 }
115
116 // Test getting by existing slug
117 conv, err := db.GetConversationBySlug(ctx, "test-slug")
118 if err != nil {
119 t.Errorf("GetBySlug() error = %v", err)
120 return
121 }
122
123 if conv.ConversationID != created.ConversationID {
124 t.Errorf("Expected conversation ID %s, got %s", created.ConversationID, conv.ConversationID)
125 }
126
127 // Test getting by non-existent slug
128 _, err = db.GetConversationBySlug(ctx, "non-existent-slug")
129 if err == nil {
130 t.Error("Expected error for non-existent slug")
131 }
132 if !strings.Contains(err.Error(), "not found") {
133 t.Errorf("Expected 'not found' in error message, got: %v", err)
134 }
135}
136
137func TestConversationService_UpdateSlug(t *testing.T) {
138 db := setupTestDB(t)
139 defer db.Close()
140
141 // Using db directly instead of service
142 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
143 defer cancel()
144
145 // Create a test conversation
146 created, err := db.CreateConversation(ctx, nil, true, nil, nil)
147 if err != nil {
148 t.Fatalf("Failed to create test conversation: %v", err)
149 }
150
151 // Update the slug
152 newSlug := "updated-slug"
153 updated, err := db.UpdateConversationSlug(ctx, created.ConversationID, newSlug)
154 if err != nil {
155 t.Errorf("UpdateSlug() error = %v", err)
156 return
157 }
158
159 if updated.Slug == nil || *updated.Slug != newSlug {
160 t.Errorf("Expected slug %s, got %v", newSlug, updated.Slug)
161 }
162
163 // Note: SQLite CURRENT_TIMESTAMP has second precision, so we check >= instead of >
164 if updated.UpdatedAt.Before(created.UpdatedAt) {
165 t.Errorf("Expected updated_at %v to be >= created updated_at %v", updated.UpdatedAt, created.UpdatedAt)
166 }
167}
168
169func TestConversationService_List(t *testing.T) {
170 db := setupTestDB(t)
171 defer db.Close()
172
173 // Using db directly instead of service
174 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
175 defer cancel()
176
177 // Create multiple test conversations
178 for i := 0; i < 5; i++ {
179 slug := stringPtr("conversation-" + string(rune('a'+i)))
180 _, err := db.CreateConversation(ctx, slug, true, nil, nil)
181 if err != nil {
182 t.Fatalf("Failed to create test conversation %d: %v", i, err)
183 }
184 }
185
186 // Test listing with pagination
187 conversations, err := db.ListConversations(ctx, 3, 0)
188 if err != nil {
189 t.Errorf("List() error = %v", err)
190 return
191 }
192
193 if len(conversations) != 3 {
194 t.Errorf("Expected 3 conversations, got %d", len(conversations))
195 }
196
197 // The query orders by updated_at DESC, but without sleeps all timestamps
198 // may be identical, so we just verify we got the expected count
199}
200
201func TestConversationService_Search(t *testing.T) {
202 db := setupTestDB(t)
203 defer db.Close()
204
205 // Using db directly instead of service
206 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
207 defer cancel()
208
209 // Create test conversations with different slugs
210 testCases := []string{"project-alpha", "project-beta", "work-task", "personal-note"}
211 for _, slug := range testCases {
212 _, err := db.CreateConversation(ctx, stringPtr(slug), true, nil, nil)
213 if err != nil {
214 t.Fatalf("Failed to create test conversation with slug %s: %v", slug, err)
215 }
216 }
217
218 // Search for "project" should return 2 conversations
219 results, err := db.SearchConversations(ctx, "project", 10, 0)
220 if err != nil {
221 t.Errorf("Search() error = %v", err)
222 return
223 }
224
225 if len(results) != 2 {
226 t.Errorf("Expected 2 search results, got %d", len(results))
227 }
228
229 // Verify the results contain "project"
230 for _, conv := range results {
231 if conv.Slug == nil || !strings.Contains(*conv.Slug, "project") {
232 t.Errorf("Expected conversation slug to contain 'project', got %v", conv.Slug)
233 }
234 }
235}
236
237func TestConversationService_Touch(t *testing.T) {
238 db := setupTestDB(t)
239 defer db.Close()
240
241 // Using db directly instead of service
242 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
243 defer cancel()
244
245 // Create a test conversation
246 created, err := db.CreateConversation(ctx, stringPtr("test-conversation"), true, nil, nil)
247 if err != nil {
248 t.Fatalf("Failed to create test conversation: %v", err)
249 }
250
251 // Touch the conversation
252 err = db.QueriesTx(ctx, func(q *generated.Queries) error {
253 return q.UpdateConversationTimestamp(ctx, created.ConversationID)
254 })
255 if err != nil {
256 t.Errorf("Touch() error = %v", err)
257 return
258 }
259
260 // Verify updated_at was changed
261 updated, err := db.GetConversationByID(ctx, created.ConversationID)
262 if err != nil {
263 t.Fatalf("Failed to get conversation after touch: %v", err)
264 }
265
266 // Note: SQLite CURRENT_TIMESTAMP has second precision, so we check >= instead of >
267 if updated.UpdatedAt.Before(created.UpdatedAt) {
268 t.Errorf("Expected updated_at %v to be >= created updated_at %v", updated.UpdatedAt, created.UpdatedAt)
269 }
270}
271
272func TestConversationService_Delete(t *testing.T) {
273 db := setupTestDB(t)
274 defer db.Close()
275
276 // Using db directly instead of service
277 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
278 defer cancel()
279
280 // Create a test conversation
281 created, err := db.CreateConversation(ctx, stringPtr("test-conversation"), true, nil, nil)
282 if err != nil {
283 t.Fatalf("Failed to create test conversation: %v", err)
284 }
285
286 // Delete the conversation
287 err = db.QueriesTx(ctx, func(q *generated.Queries) error {
288 return q.DeleteConversation(ctx, created.ConversationID)
289 })
290 if err != nil {
291 t.Errorf("Delete() error = %v", err)
292 return
293 }
294
295 // Verify it's gone
296 _, err = db.GetConversationByID(ctx, created.ConversationID)
297 if err == nil {
298 t.Error("Expected error when getting deleted conversation")
299 }
300}
301
302func TestConversationService_Count(t *testing.T) {
303 db := setupTestDB(t)
304 defer db.Close()
305
306 // Using db directly instead of service
307 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
308 defer cancel()
309
310 // Initial count should be 0
311 var count int64
312 err := db.Queries(ctx, func(q *generated.Queries) error {
313 var err error
314 count, err = q.CountConversations(ctx)
315 return err
316 })
317 if err != nil {
318 t.Errorf("Count() error = %v", err)
319 return
320 }
321 if count != 0 {
322 t.Errorf("Expected initial count 0, got %d", count)
323 }
324
325 // Create test conversations
326 for i := 0; i < 3; i++ {
327 _, err := db.CreateConversation(ctx, stringPtr("conversation-"+string(rune('a'+i))), true, nil, nil)
328 if err != nil {
329 t.Fatalf("Failed to create test conversation %d: %v", i, err)
330 }
331 }
332
333 // Count should now be 3
334 err = db.Queries(ctx, func(q *generated.Queries) error {
335 var err error
336 count, err = q.CountConversations(ctx)
337 return err
338 })
339 if err != nil {
340 t.Errorf("Count() error = %v", err)
341 return
342 }
343 if count != 3 {
344 t.Errorf("Expected count 3, got %d", count)
345 }
346}
347
348func TestConversationService_MultipleNullSlugs(t *testing.T) {
349 db := setupTestDB(t)
350 defer db.Close()
351
352 // Using db directly instead of service
353 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
354 defer cancel()
355
356 // Create multiple conversations with null slugs - this should not fail
357 conv1, err := db.CreateConversation(ctx, nil, true, nil, nil)
358 if err != nil {
359 t.Errorf("Create() first conversation error = %v", err)
360 return
361 }
362
363 conv2, err := db.CreateConversation(ctx, nil, true, nil, nil)
364 if err != nil {
365 t.Errorf("Create() second conversation error = %v", err)
366 return
367 }
368
369 // Both should have null slugs
370 if conv1.Slug != nil {
371 t.Errorf("Expected first conversation slug to be nil, got %v", conv1.Slug)
372 }
373 if conv2.Slug != nil {
374 t.Errorf("Expected second conversation slug to be nil, got %v", conv2.Slug)
375 }
376
377 // They should have different IDs
378 if conv1.ConversationID == conv2.ConversationID {
379 t.Error("Expected different conversation IDs")
380 }
381}
382
383func TestConversationService_SlugUniquenessWhenNotNull(t *testing.T) {
384 db := setupTestDB(t)
385 defer db.Close()
386
387 // Using db directly instead of service
388 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
389 defer cancel()
390
391 // Create first conversation with a slug
392 _, err := db.CreateConversation(ctx, stringPtr("unique-slug"), true, nil, nil)
393 if err != nil {
394 t.Errorf("Create() first conversation error = %v", err)
395 return
396 }
397
398 // Try to create second conversation with the same slug - this should fail
399 _, err = db.CreateConversation(ctx, stringPtr("unique-slug"), true, nil, nil)
400 if err == nil {
401 t.Error("Expected error when creating conversation with duplicate slug")
402 return
403 }
404
405 // Verify the error is related to uniqueness constraint
406 if !strings.Contains(err.Error(), "UNIQUE constraint failed") {
407 t.Errorf("Expected UNIQUE constraint error, got: %v", err)
408 }
409}
410
411func TestConversationService_ArchiveUnarchive(t *testing.T) {
412 db := setupTestDB(t)
413 defer db.Close()
414
415 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
416 defer cancel()
417
418 // Create a test conversation
419 conv, err := db.CreateConversation(ctx, stringPtr("test-conversation"), true, nil, nil)
420 if err != nil {
421 t.Fatalf("Failed to create test conversation: %v", err)
422 }
423
424 // Test ArchiveConversation
425 archivedConv, err := db.ArchiveConversation(ctx, conv.ConversationID)
426 if err != nil {
427 t.Errorf("ArchiveConversation() error = %v", err)
428 }
429
430 if !archivedConv.Archived {
431 t.Error("Expected conversation to be archived")
432 }
433
434 // Test UnarchiveConversation
435 unarchivedConv, err := db.UnarchiveConversation(ctx, conv.ConversationID)
436 if err != nil {
437 t.Errorf("UnarchiveConversation() error = %v", err)
438 }
439
440 if unarchivedConv.Archived {
441 t.Error("Expected conversation to be unarchived")
442 }
443}
444
445func TestConversationService_ListArchivedConversations(t *testing.T) {
446 db := setupTestDB(t)
447 defer db.Close()
448
449 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
450 defer cancel()
451
452 // Create test conversations
453 conv1, err := db.CreateConversation(ctx, stringPtr("test-conversation-1"), true, nil, nil)
454 if err != nil {
455 t.Fatalf("Failed to create test conversation 1: %v", err)
456 }
457
458 conv2, err := db.CreateConversation(ctx, stringPtr("test-conversation-2"), true, nil, nil)
459 if err != nil {
460 t.Fatalf("Failed to create test conversation 2: %v", err)
461 }
462
463 // Archive both conversations
464 _, err = db.ArchiveConversation(ctx, conv1.ConversationID)
465 if err != nil {
466 t.Fatalf("Failed to archive conversation 1: %v", err)
467 }
468
469 _, err = db.ArchiveConversation(ctx, conv2.ConversationID)
470 if err != nil {
471 t.Fatalf("Failed to archive conversation 2: %v", err)
472 }
473
474 // Test ListArchivedConversations
475 conversations, err := db.ListArchivedConversations(ctx, 10, 0)
476 if err != nil {
477 t.Errorf("ListArchivedConversations() error = %v", err)
478 }
479
480 if len(conversations) != 2 {
481 t.Errorf("Expected 2 archived conversations, got %d", len(conversations))
482 }
483
484 // Check that all returned conversations are archived
485 for _, conv := range conversations {
486 if !conv.Archived {
487 t.Error("Expected all conversations to be archived")
488 break
489 }
490 }
491}
492
493func TestConversationService_SearchArchivedConversations(t *testing.T) {
494 db := setupTestDB(t)
495 defer db.Close()
496
497 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
498 defer cancel()
499
500 // Create test conversations
501 conv1, err := db.CreateConversation(ctx, stringPtr("test-conversation-search-1"), true, nil, nil)
502 if err != nil {
503 t.Fatalf("Failed to create test conversation 1: %v", err)
504 }
505
506 conv2, err := db.CreateConversation(ctx, stringPtr("another-conversation"), true, nil, nil)
507 if err != nil {
508 t.Fatalf("Failed to create test conversation 2: %v", err)
509 }
510
511 // Archive both conversations
512 _, err = db.ArchiveConversation(ctx, conv1.ConversationID)
513 if err != nil {
514 t.Fatalf("Failed to archive conversation 1: %v", err)
515 }
516
517 _, err = db.ArchiveConversation(ctx, conv2.ConversationID)
518 if err != nil {
519 t.Fatalf("Failed to archive conversation 2: %v", err)
520 }
521
522 // Test SearchArchivedConversations
523 conversations, err := db.SearchArchivedConversations(ctx, "test-conversation", 10, 0)
524 if err != nil {
525 t.Errorf("SearchArchivedConversations() error = %v", err)
526 }
527
528 if len(conversations) != 1 {
529 t.Errorf("Expected 1 archived conversation matching search, got %d", len(conversations))
530 }
531
532 if len(conversations) > 0 && conversations[0].Slug == nil {
533 t.Error("Expected conversation to have a slug")
534 } else if len(conversations) > 0 && !strings.Contains(*conversations[0].Slug, "test-conversation") {
535 t.Errorf("Expected conversation slug to contain 'test-conversation', got %s", *conversations[0].Slug)
536 }
537}
538
539func TestConversationService_DeleteConversation(t *testing.T) {
540 db := setupTestDB(t)
541 defer db.Close()
542
543 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
544 defer cancel()
545
546 // Create a test conversation
547 conv, err := db.CreateConversation(ctx, stringPtr("test-conversation-to-delete"), true, nil, nil)
548 if err != nil {
549 t.Fatalf("Failed to create test conversation: %v", err)
550 }
551
552 // Add a message to the conversation
553 _, err = db.CreateMessage(ctx, CreateMessageParams{
554 ConversationID: conv.ConversationID,
555 Type: MessageTypeUser,
556 LLMData: map[string]string{"text": "test message"},
557 })
558 if err != nil {
559 t.Fatalf("Failed to create test message: %v", err)
560 }
561
562 // Test DeleteConversation
563 err = db.DeleteConversation(ctx, conv.ConversationID)
564 if err != nil {
565 t.Errorf("DeleteConversation() error = %v", err)
566 }
567
568 // Verify conversation is deleted
569 _, err = db.GetConversationByID(ctx, conv.ConversationID)
570 if err == nil {
571 t.Error("Expected error when getting deleted conversation, got none")
572 }
573}
574
575func TestConversationService_UpdateConversationCwd(t *testing.T) {
576 db := setupTestDB(t)
577 defer db.Close()
578
579 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
580 defer cancel()
581
582 // Create a test conversation
583 conv, err := db.CreateConversation(ctx, stringPtr("test-conversation-cwd"), true, nil, nil)
584 if err != nil {
585 t.Fatalf("Failed to create test conversation: %v", err)
586 }
587
588 // Test UpdateConversationCwd
589 newCwd := "/test/new/working/directory"
590 err = db.UpdateConversationCwd(ctx, conv.ConversationID, newCwd)
591 if err != nil {
592 t.Errorf("UpdateConversationCwd() error = %v", err)
593 }
594
595 // Verify the cwd was updated
596 updatedConv, err := db.GetConversationByID(ctx, conv.ConversationID)
597 if err != nil {
598 t.Fatalf("Failed to get updated conversation: %v", err)
599 }
600
601 if updatedConv.Cwd == nil {
602 t.Error("Expected conversation to have a cwd")
603 } else if *updatedConv.Cwd != newCwd {
604 t.Errorf("Expected cwd %s, got %s", newCwd, *updatedConv.Cwd)
605 }
606}