diff --git a/TASKS-backend-progress-tracking.md b/TASKS-backend-progress-tracking.md index 7b6e80c..94839a9 100644 --- a/TASKS-backend-progress-tracking.md +++ b/TASKS-backend-progress-tracking.md @@ -473,27 +473,42 @@ func (s *ScannerIntegrationTestSuite) TearDownSuite() { func (s *ScannerIntegrationTestSuite) TestScanProgress_TracksStatistics() { token := s.setup.Token - // Create test library with folder - libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Scan Test Library", false) - - // Start scan - url := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) - req, _ := http.NewRequest("POST", url, nil) + // Create test library + libraryID := s.setup.CreateLibrary(s.T(), "Scan Test Library", "ebooks") + + // Add folder to library + folderURL := fmt.Sprintf("%s/api/libraries/%s/folders", s.setup.Server.URL, libraryID) + folderReq := map[string]interface{}{ + "folder_path": "/app/uploads", + } + folderBody, _ := json.Marshal(folderReq) + req, _ := http.NewRequest("POST", folderURL, bytes.NewBuffer(folderBody)) + req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(s.T(), err) - require.Equal(s.T(), http.StatusAccepted, resp.StatusCode) + resp.Body.Close() + require.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Folder creation should succeed") + + // Start scan + scanURL := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) + scanReq, _ := http.NewRequest("POST", scanURL, nil) + scanReq.Header.Set("Authorization", "Bearer "+token) + + scanResp, err := client.Do(scanReq) + require.NoError(s.T(), err) + require.Equal(s.T(), http.StatusAccepted, scanResp.StatusCode) var scanResponse map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&scanResponse) + err = json.NewDecoder(scanResp.Body).Decode(&scanResponse) require.NoError(s.T(), err) - resp.Body.Close() + scanResp.Body.Close() jobID, ok := scanResponse["job_id"].(string) - require.True(t, ok, "job_id should be string") - require.NotEmpty(t, jobID, "job_id should not be empty") + require.True(s.T(), ok, "job_id should be string") + require.NotEmpty(s.T(), jobID, "job_id should not be empty") // Poll for progress updates var lastProgress float64 @@ -521,19 +536,19 @@ func (s *ScannerIntegrationTestSuite) TestScanProgress_TracksStatistics() { // Track progress with safe type assertions progressFloat, ok := status["progress"].(float64) - require.True(t, ok, "progress should be float64") + require.True(s.T(), ok, "progress should be float64") progress := progressFloat filesScannedFloat, ok := status["files_scanned"].(float64) - require.True(t, ok, "files_scanned should be float64") + require.True(s.T(), ok, "files_scanned should be float64") filesScanned := int(filesScannedFloat) newItemsFloat, ok := status["new_items"].(float64) - require.True(t, ok, "new_items should be float64") + require.True(s.T(), ok, "new_items should be float64") newItems := int(newItemsFloat) errorsFloat, ok := status["errors"].(float64) - require.True(t, ok, "errors should be float64") + require.True(s.T(), ok, "errors should be float64") errors := int(errorsFloat) // Progress should be non-decreasing @@ -562,12 +577,16 @@ func (s *ScannerIntegrationTestSuite) TestScanProgress_TracksStatistics() { func (s *ScannerIntegrationTestSuite) TestScanProgress_BatchingWorks() { token := s.setup.Token - // Create library - libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Batch Test Library", false) + // Create library with folder + libraryID := s.setup.CreateLibrary(s.T(), "Batch Test Library", "ebooks") - // Start scan - url := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) - req, _ := http.NewRequest("POST", url, nil) + folderURL := fmt.Sprintf("%s/api/libraries/%s/folders", s.setup.Server.URL, libraryID) + folderReq := map[string]interface{}{ + "folder_path": "/app/uploads", + } + folderBody, _ := json.Marshal(folderReq) + req, _ := http.NewRequest("POST", folderURL, bytes.NewBuffer(folderBody)) + req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} @@ -575,12 +594,21 @@ func (s *ScannerIntegrationTestSuite) TestScanProgress_BatchingWorks() { require.NoError(s.T(), err) resp.Body.Close() + // Start scan + scanURL := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) + scanReq, _ := http.NewRequest("POST", scanURL, nil) + scanReq.Header.Set("Authorization", "Bearer "+token) + + scanResp, err := client.Do(scanReq) + require.NoError(s.T(), err) + scanResp.Body.Close() + var scanResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&scanResponse) + json.NewDecoder(scanResp.Body).Decode(&scanResponse) jobID, ok := scanResponse["job_id"].(string) - require.True(t, ok, "job_id should be string") - require.NotEmpty(t, jobID, "job_id should not be empty") + require.True(s.T(), ok, "job_id should be string") + require.NotEmpty(s.T(), jobID, "job_id should not be empty") // Poll and verify we don't get updates on EVERY file updateCount := 0 @@ -601,7 +629,7 @@ func (s *ScannerIntegrationTestSuite) TestScanProgress_BatchingWorks() { statusResp.Body.Close() filesScannedFloat, ok := status["files_scanned"].(float64) - require.True(t, ok, "files_scanned should be float64") + require.True(s.T(), ok, "files_scanned should be float64") filesScanned := int(filesScannedFloat) // Only count as update if files_scanned changed @@ -618,7 +646,7 @@ func (s *ScannerIntegrationTestSuite) TestScanProgress_BatchingWorks() { // With batching every 10 files, we should have FEWER updates than files // This is a weak assertion, but verifies batching is working // Threshold of 50 assumes test library has < 500 files - adjust based on actual test data - assert.Less(t, updateCount, 50) + assert.Less(s.T(), updateCount, 50) } func TestScannerIntegrationTestSuite(t *testing.T) { @@ -632,28 +660,90 @@ func TestScannerIntegrationTestSuite(t *testing.T) { **Add test for new JobResult fields:** ```go -func TestWorker_ProcessScanJob_ReturnsStats(t *testing.T) { - // Test that processScanJob returns proper stat structure +func TestWorker_JobResult_HasStatsFields(t *testing.T) { + // Test that JobResult properly stores scan statistics + worker := NewWorker(1) + defer worker.Shutdown() + + jobID := "test-job-stats" + + // Simulate job completion with stats + worker.mu.Lock() + worker.results[jobID] = &JobResult{ + JobID: jobID, + Status: JobStatusCompleted, + Progress: 1.0, + FilesScanned: 42, + NewItems: 5, + Errors: 1, + } + worker.mu.Unlock() + + // Verify stats are retrievable + result, exists := worker.GetJobStatus(jobID) + require.True(t, exists, "Job result should exist") + require.NotNil(t, result, "Result should not be nil") + + assert.Equal(t, jobID, result.JobID) + assert.Equal(t, JobStatusCompleted, result.Status) + assert.Equal(t, 1.0, result.Progress) + assert.Equal(t, 42, result.FilesScanned, "FilesScanned should be 42") + assert.Equal(t, 5, result.NewItems, "NewItems should be 5") + assert.Equal(t, 1, result.Errors, "Errors should be 1") +} + +func TestWorker_ProgressCallback_UpdatesJobResult(t *testing.T) { + // Test that progress callback updates JobResult in real-time worker := NewWorker(1) defer worker.Shutdown() job := &Job{ - ID: "test-job-stats", - Type: JobTypeScan, - Params: map[string]interface{}{ - "library_id": "test-lib", - "folders": []string{"/test"}, - "admin_id": "test-admin", - "db": nil, // Will fail but we can test return structure - }, - Status: JobStatusPending, + ID: "test-progress", + Type: JobTypeScan, + Status: JobStatusInProgress, + Context: context.Background(), } - result, err := worker.processScanJob(job) + // Set up progress callback + job.ProgressCallback = func(progress float64, filesScanned, newItems, errors int) { + worker.mu.Lock() + defer worker.mu.Unlock() + + if result, exists := worker.results[job.ID]; exists { + result.Progress = progress + result.FilesScanned = filesScanned + result.NewItems = newItems + result.Errors = errors + } + } - // Should fail (nil db), but we can test the error handling - assert.Error(t, err) - assert.Nil(t, result) + // Initialize result + worker.mu.Lock() + worker.results[job.ID] = &JobResult{ + JobID: job.ID, + Status: JobStatusInProgress, + } + worker.mu.Unlock() + + // Simulate progress updates + job.UpdateProgress(0.5, 10, 2, 0) + + result, exists := worker.GetJobStatus(job.ID) + require.True(t, exists) + assert.Equal(t, 0.5, result.Progress) + assert.Equal(t, 10, result.FilesScanned) + assert.Equal(t, 2, result.NewItems) + assert.Equal(t, 0, result.Errors) + + // Simulate completion + job.UpdateProgress(1.0, 20, 5, 1) + + result, exists = worker.GetJobStatus(job.ID) + require.True(t, exists) + assert.Equal(t, 1.0, result.Progress) + assert.Equal(t, 20, result.FilesScanned) + assert.Equal(t, 5, result.NewItems) + assert.Equal(t, 1, result.Errors) } ``` @@ -664,7 +754,11 @@ func TestWorker_ProcessScanJob_ReturnsStats(t *testing.T) { After implementation: - [ ] Unit tests pass: `go test ./internal/services/...` + - [ ] TestWorker_JobResult_HasStatsFields verifies stats are stored + - [ ] TestWorker_ProgressCallback_UpdatesJobResult verifies real-time updates - [ ] Integration tests pass: `go test ./cmd/server/tests/...` + - [ ] TestScanProgress_TracksStatistics verifies all new fields exist and increment + - [ ] TestScanProgress_BatchingWorks verifies batching reduces update frequency - [ ] Scan a library with multiple files - [ ] Poll `/api/scanner/status/{jobId}` during scan - [ ] Verify `progress` increases from 0% to 100% gradually @@ -686,6 +780,7 @@ After implementation: - **Memory:** Stats tracking uses 3 int fields (24 bytes) per MediaScanner instance - **Backwards Compatibility:** Frontend already uses `|| 0` fallbacks, so safe to deploy - **Testing:** Integration tests use `setupTestServer()` from `test_helpers.go` +- **Database Pool Configuration:** `setupTestServer()` already sets `max_conns=1` (test_helpers.go:428), preventing connection pool exhaustion during test runs ---