package db import ( "os" "path/filepath" "strings" "testing" ) func TestVectorSimilarityDeduplication(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_dedup.db") database, err := InitDB(dbPath) if err != nil { t.Fatalf("failed to init db: %v", err) } defer func() { _ = database.Close() }() contentA := "Senior Software Engineer - Go Distributed Systems. Experience with microservices, gRPC, and SQLite database pipelines." contentSimilar := "Senior Software Engineer - Go Distributed Systems. Experience with microservices, gRPC, and SQLite database pipelines." contentDifferent := "Marketing Specialist - Social Media Campaign Manager. Experience with SEO, Google Ads, and brand strategy." // 1. Insert job A jobA, inserted1, err := database.InsertRawJob("canonical-1", nil, "Software Engineer", "Google", "Seattle", "https://google.com/job/1", contentA) if err != nil || !inserted1 { t.Fatalf("expected job A to be inserted: err=%v, inserted=%v", err, inserted1) } // 2. Insert job with identical/99%+ similar content -> should be deduplicated! _, inserted2, err := database.InsertRawJob("canonical-2-different-id", nil, "Learn more about Software Engineer", "Google", "Seattle", "https://google.com/job/1-dup", contentSimilar) if err != nil { t.Fatalf("unexpected error: %v", err) } if inserted2 { t.Errorf("expected 99%% similar content job to be deduplicated (inserted=false), got inserted=true") } // 3. Insert job with distinct content -> should insert normally! _, inserted3, err := database.InsertRawJob("canonical-3", nil, "Marketing Specialist", "Google", "Seattle", "https://google.com/job/3", contentDifferent) if err != nil || !inserted3 { t.Fatalf("expected distinct job to insert normally: err=%v, inserted=%v", err, inserted3) } // Verify only 2 raw jobs exist in DB allJobs, err := database.getAllRawJobsForDeduplication() if err != nil { t.Fatalf("getAllRawJobsForDeduplication failed: %v", err) } if len(allJobs) != 2 { t.Errorf("expected 2 raw jobs in DB after deduplication, got %d", len(allJobs)) } if allJobs[0].ID != jobA.ID { t.Errorf("first job mismatch") } } func TestDeleteTargetCascade(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_delete_target.db") database, err := InitDB(dbPath) if err != nil { t.Fatalf("failed to init db: %v", err) } defer func() { _ = database.Close() }() // 1. Create two search targets target1, err := database.CreateTarget("Target 1", "https://example.com/target1", "google_jobs") if err != nil { t.Fatalf("failed to create target 1: %v", err) } target2, err := database.CreateTarget("Target 2", "https://example.com/target2", "apple") if err != nil { t.Fatalf("failed to create target 2: %v", err) } // 2. Insert raw jobs for target 1 and target 2 job1, _, err := database.InsertRawJob("canon-t1-j1", &target1.ID, "Job 1", "Google", "Seattle", "https://google.com/1", "Software Engineer Go") if err != nil { t.Fatalf("failed to insert job 1: %v", err) } job2, _, err := database.InsertRawJob("canon-t1-j2", &target1.ID, "Job 2", "Google", "Seattle", "https://google.com/2", "Software Engineer Python") if err != nil { t.Fatalf("failed to insert job 2: %v", err) } job3, _, err := database.InsertRawJob("canon-t2-j1", &target2.ID, "Job 3", "Apple", "Seattle", "https://apple.com/1", "Software Engineer iOS") if err != nil { t.Fatalf("failed to insert job 3: %v", err) } // 3. Create analyses and applications for target 1 jobs and target 2 jobs _ = database.CreateJobAnalysis(job1.ID, 85.0, true, "DONE") _ = database.CreateJobAnalysis(job2.ID, 40.0, false, "DONE") _ = database.CreateJobAnalysis(job3.ID, 90.0, true, "DONE") _, _ = database.CreateOrUpdateApplication(job1.ID, "APPLIED", "Sent resume") _, _ = database.CreateOrUpdateApplication(job3.ID, "SAVED", "Review later") // 4. Enqueue tasks _ = database.EnqueueTask("DISCOVER", map[string]interface{}{"target_id": target1.ID, "target_url": target1.TargetURL}) _ = database.EnqueueTask("DISCOVER", map[string]interface{}{"target_id": target2.ID, "target_url": target2.TargetURL}) _ = database.EnqueueTask("PROCESS", map[string]interface{}{"raw_job_id": job1.ID}) _ = database.EnqueueTask("PROCESS", map[string]interface{}{"raw_job_id": job3.ID}) // 5. Delete target 1 deletedTarget, deletedJobsCount, err := database.DeleteTarget(target1.ID) if err != nil { t.Fatalf("DeleteTarget failed: %v", err) } if deletedTarget.ID != target1.ID { t.Errorf("expected deleted target ID %d, got %d", target1.ID, deletedTarget.ID) } if deletedJobsCount != 2 { t.Errorf("expected 2 deleted jobs, got %d", deletedJobsCount) } // 6. Verify Target 1 is gone, Target 2 remains allTargets, err := database.GetAllTargets() if err != nil { t.Fatalf("GetAllTargets failed: %v", err) } if len(allTargets) != 1 || allTargets[0].ID != target2.ID { t.Errorf("expected 1 remaining target (target 2), got %v", allTargets) } // 7. Verify Target 1's jobs are gone, Target 2's job remains jobsWithAnalysis, err := database.GetJobsWithAnalysis() if err != nil { t.Fatalf("GetJobsWithAnalysis failed: %v", err) } if len(jobsWithAnalysis) != 1 || jobsWithAnalysis[0].ID != job3.ID { t.Errorf("expected only job 3 to remain, got %v", jobsWithAnalysis) } // 8. Verify applications for target 1 are gone, target 2 application remains apps, err := database.GetApplications() if err != nil { t.Fatalf("GetApplications failed: %v", err) } if len(apps) != 1 || apps[0].ID != job3.ID { t.Errorf("expected only job 3 application to remain, got %v", apps) } } func TestRemoveTargetFromYAML(t *testing.T) { tempDir := t.TempDir() yamlPath := filepath.Join(tempDir, "targets.yaml") initialYAML := `targets: - name: "Google Seattle" target_url: "https://google.com/jobs" platform: "google_jobs" is_active: true - name: "Apple Seattle" target_url: "https://apple.com/jobs" platform: "apple" is_active: true ` if err := os.WriteFile(yamlPath, []byte(initialYAML), 0644); err != nil { t.Fatalf("failed to write initial yaml: %v", err) } // Remove Google Seattle if err := RemoveTargetFromYAML(yamlPath, "https://google.com/jobs"); err != nil { t.Fatalf("RemoveTargetFromYAML failed: %v", err) } // Read back and verify data, err := os.ReadFile(yamlPath) if err != nil { t.Fatalf("failed to read yaml back: %v", err) } content := string(data) if strings.Contains(content, "https://google.com/jobs") { t.Errorf("expected google target to be removed, but found in file:\n%s", content) } if !strings.Contains(content, "https://apple.com/jobs") { t.Errorf("expected apple target to remain, but not found in file:\n%s", content) } } func TestTouchJobLastSeenAndMaintenance(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_maintenance.db") database, err := InitDB(dbPath) if err != nil { t.Fatalf("failed to init db: %v", err) } defer func() { _ = database.Close() }() // Insert jobs job1, _, err := database.InsertRawJob("canon-1", nil, "Job 1", "Google", "Seattle", "https://google.com/1", "Software Engineer Go") if err != nil || job1.ID == 0 { t.Fatalf("failed to insert job 1: %v", err) } job2, _, err := database.InsertRawJob("canon-2", nil, "Job 2", "Apple", "Seattle", "https://apple.com/2", "Software Engineer Swift") if err != nil { t.Fatalf("failed to insert job 2: %v", err) } job3, _, err := database.InsertRawJob("canon-3", nil, "Job 3", "Meta", "Seattle", "https://meta.com/3", "Software Engineer React") if err != nil { t.Fatalf("failed to insert job 3: %v", err) } // 1. Touch last seen for canon-1 if err := database.TouchJobLastSeen("canon-1"); err != nil { t.Fatalf("TouchJobLastSeen failed: %v", err) } // Make job2 and job3 artificially stale (20 days and 40 days old) _, err = database.Exec(`UPDATE raw_jobs SET last_seen_at = datetime('now', '-20 days') WHERE id = ?`, job2.ID) if err != nil { t.Fatalf("failed to update last_seen_at: %v", err) } _, err = database.Exec(`UPDATE raw_jobs SET last_seen_at = datetime('now', '-40 days') WHERE id = ?`, job3.ID) if err != nil { t.Fatalf("failed to update last_seen_at: %v", err) } // Protect job3 in active pipeline (APPLIED) _, _ = database.CreateOrUpdateApplication(job3.ID, "APPLIED", "Applied to Meta") // 2. Enqueue expire tasks (> 14 days) with pipeline protection queuedExpireCount, err := database.EnqueueExpireTasks(14, true) if err != nil { t.Fatalf("EnqueueExpireTasks failed: %v", err) } // job2 is stale and unprotected -> should be queued // job3 is stale but protected (APPLIED) -> should NOT be queued // job1 is fresh -> should NOT be queued if queuedExpireCount != 1 { t.Errorf("expected 1 job queued for expire, got %d", queuedExpireCount) } // 3. Directly expire job2 if err := database.ExpireJob(job2.ID, "Test expiration"); err != nil { t.Fatalf("ExpireJob failed: %v", err) } // Verify job2 status in DB j2Detail, err := database.GetJobByID(job2.ID) if err != nil { t.Fatalf("GetJobByID failed: %v", err) } if j2Detail.App == nil || j2Detail.App.Status != "EXPIRED" { t.Errorf("expected job2 to have EXPIRED application, got %v", j2Detail.App) } // Verify job3 still APPLIED j3Detail, err := database.GetJobByID(job3.ID) if err != nil { t.Fatalf("GetJobByID failed: %v", err) } if j3Detail.App == nil || j3Detail.App.Status != "APPLIED" { t.Errorf("expected job3 to remain APPLIED, got %v", j3Detail.App) } // 4. Stats check stats, err := database.GetJobMaintenanceStats() if err != nil { t.Fatalf("GetJobMaintenanceStats failed: %v", err) } if stats["total_jobs"] != 3 { t.Errorf("expected total_jobs=3, got %d", stats["total_jobs"]) } if stats["active_7d"] != 1 { t.Errorf("expected active_7d=1, got %d", stats["active_7d"]) } if stats["stale_7d"] != 2 { t.Errorf("expected stale_7d=2, got %d", stats["stale_7d"]) } if stats["expired_count"] != 1 { t.Errorf("expected expired_count=1, got %d", stats["expired_count"]) } } func TestDevDBMigration(t *testing.T) { devDBPath := "../../data/db/dev/hireme.db" if _, err := os.Stat(devDBPath); os.IsNotExist(err) { return } database, err := InitDB(devDBPath) if err != nil { t.Fatalf("InitDB dev db failed: %v", err) } defer func() { _ = database.Close() }() jobs, err := database.GetJobsWithAnalysis() if err != nil { t.Fatalf("GetJobsWithAnalysis on dev db failed: %v", err) } t.Logf("Successfully verified %d jobs from dev db", len(jobs)) }