package extraction import ( "os" "path/filepath" "testing" "time" ) func tempQueue(t *testing.T) *Queue { t.Helper() dir := t.TempDir() q, err := NewQueue(dir, 3) if err != nil { t.Fatalf("NewQueue: %v", err) } return q } func TestEnqueue_CreatesStatusFile(t *testing.T) { q := tempQueue(t) err := q.Enqueue("att-123", "attachments/ch/att-123_doc.pdf", "application/pdf", "doc.pdf") if err != nil { t.Fatalf("Enqueue: %v", err) } item, err := q.GetStatus("att-123") if err != nil { t.Fatalf("GetStatus: %v", err) } if item == nil { t.Fatal("expected non-nil item") } if item.Status != StatusPending { t.Errorf("status = %q, want %q", item.Status, StatusPending) } if item.AttachmentID != "att-123" { t.Errorf("attachment_id = %q, want att-123", item.AttachmentID) } if item.ContentType != "application/pdf" { t.Errorf("content_type = %q, want application/pdf", item.ContentType) } } func TestGetStatus_NotQueued(t *testing.T) { q := tempQueue(t) item, err := q.GetStatus("nonexistent") if err != nil { t.Fatalf("GetStatus: %v", err) } if item != nil { t.Error("expected nil for non-queued item") } } func TestMarkComplete(t *testing.T) { q := tempQueue(t) q.Enqueue("att-456", "key", "application/pdf", "test.pdf") err := q.MarkComplete("att-456", "processing/att-456/extracted.txt") if err != nil { t.Fatalf("MarkComplete: %v", err) } item, _ := q.GetStatus("att-456") if item.Status != StatusComplete { t.Errorf("status = %q, want %q", item.Status, StatusComplete) } if item.OutputKey != "processing/att-456/extracted.txt" { t.Errorf("output_key = %q", item.OutputKey) } if item.CompletedAt == "" { t.Error("completed_at should be set") } } func TestMarkFailed(t *testing.T) { q := tempQueue(t) q.Enqueue("att-789", "key", "application/pdf", "test.pdf") err := q.MarkFailed("att-789", "libreoffice crashed") if err != nil { t.Fatalf("MarkFailed: %v", err) } item, _ := q.GetStatus("att-789") if item.Status != StatusFailed { t.Errorf("status = %q, want %q", item.Status, StatusFailed) } if item.Error != "libreoffice crashed" { t.Errorf("error = %q", item.Error) } } func TestCleanup_RemovesDirectory(t *testing.T) { q := tempQueue(t) q.Enqueue("att-cleanup", "key", "application/pdf", "test.pdf") // Verify directory exists dir := filepath.Join(q.storagePath, "processing", "att-cleanup") if _, err := os.Stat(dir); err != nil { t.Fatal("expected processing dir to exist before cleanup") } if err := q.Cleanup("att-cleanup"); err != nil { t.Fatalf("Cleanup: %v", err) } if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Error("expected processing dir to be removed after cleanup") } } func TestListPending(t *testing.T) { q := tempQueue(t) q.Enqueue("att-a", "key-a", "application/pdf", "a.pdf") q.Enqueue("att-b", "key-b", "application/pdf", "b.pdf") q.MarkComplete("att-b", "out") pending, err := q.ListPending() if err != nil { t.Fatalf("ListPending: %v", err) } if len(pending) != 1 { t.Fatalf("expected 1 pending, got %d", len(pending)) } if pending[0].AttachmentID != "att-a" { t.Errorf("pending[0].AttachmentID = %q, want att-a", pending[0].AttachmentID) } } func TestListAll(t *testing.T) { q := tempQueue(t) q.Enqueue("att-1", "key-1", "application/pdf", "1.pdf") q.Enqueue("att-2", "key-2", "application/pdf", "2.pdf") q.Enqueue("att-3", "key-3", "application/pdf", "3.pdf") all, err := q.ListAll() if err != nil { t.Fatalf("ListAll: %v", err) } if len(all) != 3 { t.Errorf("expected 3 items, got %d", len(all)) } } func TestRecoverStale(t *testing.T) { q := tempQueue(t) // Create item and manually set it to "processing" with old timestamp q.Enqueue("att-stale", "key", "application/pdf", "stale.pdf") item, _ := q.GetStatus("att-stale") item.Status = StatusProcessing item.StartedAt = time.Now().Add(-2 * time.Hour).UTC().Format(time.RFC3339) q.mu.Lock() q.writeStatus("att-stale", item) q.mu.Unlock() // Recover items stale for > 1 hour recovered, err := q.RecoverStale(1 * time.Hour) if err != nil { t.Fatalf("RecoverStale: %v", err) } if recovered != 1 { t.Errorf("recovered = %d, want 1", recovered) } // Verify it's back to pending item, _ = q.GetStatus("att-stale") if item.Status != StatusPending { t.Errorf("status = %q, want %q", item.Status, StatusPending) } } func TestRecoverStale_SkipsRecent(t *testing.T) { q := tempQueue(t) // Create item "processing" but recent (not stale) q.Enqueue("att-recent", "key", "application/pdf", "recent.pdf") item, _ := q.GetStatus("att-recent") item.Status = StatusProcessing item.StartedAt = time.Now().Add(-5 * time.Minute).UTC().Format(time.RFC3339) q.mu.Lock() q.writeStatus("att-recent", item) q.mu.Unlock() recovered, err := q.RecoverStale(1 * time.Hour) if err != nil { t.Fatalf("RecoverStale: %v", err) } if recovered != 0 { t.Errorf("recovered = %d, want 0 (item is recent)", recovered) } } func TestIsExtractable(t *testing.T) { tests := []struct { contentType string want bool }{ {"application/pdf", true}, {"application/vnd.openxmlformats-officedocument.wordprocessingml.document", true}, {"image/png", false}, {"text/plain", false}, {"image/jpeg", false}, {"application/rtf", true}, } for _, tc := range tests { got := IsExtractable(tc.contentType) if got != tc.want { t.Errorf("IsExtractable(%q) = %v, want %v", tc.contentType, got, tc.want) } } } func TestAtomicWrite(t *testing.T) { q := tempQueue(t) // Enqueue and verify the file is valid JSON q.Enqueue("att-atomic", "key", "application/pdf", "test.pdf") statusPath := filepath.Join(q.storagePath, "processing", "att-atomic", "status.json") data, err := os.ReadFile(statusPath) if err != nil { t.Fatalf("ReadFile: %v", err) } // Verify no .tmp file left behind tmpPath := statusPath + ".tmp" if _, err := os.Stat(tmpPath); !os.IsNotExist(err) { t.Error("temp file should not exist after atomic write") } if len(data) == 0 { t.Error("status.json should not be empty") } }