conversations_test.go

  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}