Files
santifer--career-ops/dashboard/internal/data/career_test.go
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

362 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package data
import (
"os"
"path/filepath"
"strings"
"testing"
)
// Regression for #1180: a status word appearing as a substring of an earlier
// cell (Company "Applied Materials" contains "Applied") must not be rewritten;
// only the Status column changes.
func TestUpdateApplicationStatusOnlyRewritesStatusColumn(t *testing.T) {
tempDir := t.TempDir()
dataDir := filepath.Join(tempDir, "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("failed to create data dir: %v", err)
}
applications := `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 7 | 2026-06-23 | Applied Materials | Staff Android Engineer | 4.2/5 | Applied | ✅ | [7](reports/007.md) | substring trap |
`
path := filepath.Join(dataDir, "applications.md")
if err := os.WriteFile(path, []byte(applications), 0o644); err != nil {
t.Fatalf("failed to write tracker: %v", err)
}
apps := ParseApplications(tempDir)
if len(apps) != 1 {
t.Fatalf("expected 1 parsed application, got %d", len(apps))
}
if err := UpdateApplicationStatus(tempDir, apps[0], "Interview"); err != nil {
t.Fatalf("UpdateApplicationStatus: %v", err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
out := string(got)
if !strings.Contains(out, "| Applied Materials |") {
t.Errorf("Company cell was corrupted, file now:\n%s", out)
}
if !strings.Contains(out, "| Interview |") {
t.Errorf("Status cell was not updated to Interview, file now:\n%s", out)
}
if strings.Contains(out, "Interview Materials") {
t.Errorf("status word was replaced inside the Company cell, file now:\n%s", out)
}
reparsed := ParseApplications(tempDir)
if reparsed[0].Company != "Applied Materials" {
t.Errorf("company = %q, want \"Applied Materials\"", reparsed[0].Company)
}
if reparsed[0].Status != "Interview" {
t.Errorf("status = %q, want \"Interview\"", reparsed[0].Status)
}
}
func TestParseApplicationsUsesTrackerNumberColumn(t *testing.T) {
tempDir := t.TempDir()
dataDir := filepath.Join(tempDir, "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("failed to create data dir: %v", err)
}
applications := `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 140 | 2026-04-16 | Arize AI | AI Engineer, Instrumentation | 4.7/5 | Evaluated | ✅ | [140](reports/140-arize-ai-engineer-instrumentation-2026-04-16.md) | Strong fit |
| 143 | 2026-04-16 | Arize AI | AI Sales Engineer, US | 4.1/5 | Evaluated | ❌ | [143](reports/143-arize-ai-sales-engineer-us-2026-04-16.md) | Good fit |
`
applicationsPath := filepath.Join(dataDir, "applications.md")
if err := os.WriteFile(applicationsPath, []byte(applications), 0o644); err != nil {
t.Fatalf("failed to write applications tracker: %v", err)
}
apps := ParseApplications(tempDir)
if len(apps) != 2 {
t.Fatalf("expected 2 parsed applications, got %d", len(apps))
}
if apps[0].Number != 140 {
t.Fatalf("expected first application number to be 140, got %d", apps[0].Number)
}
if apps[1].Number != 143 {
t.Fatalf("expected second application number to be 143, got %d", apps[1].Number)
}
if apps[0].ReportNumber != "140" || apps[1].ReportNumber != "143" {
t.Fatalf("expected report numbers to stay aligned with tracker IDs, got %q and %q", apps[0].ReportNumber, apps[1].ReportNumber)
}
}
func TestParseApplicationsResolvesTrackerRelativeReportLinks(t *testing.T) {
tempDir := t.TempDir()
dataDir := filepath.Join(tempDir, "data")
reportsDir := filepath.Join(tempDir, "reports")
for _, dir := range []string{dataDir, reportsDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create dir %s: %v", dir, err)
}
}
// Tracker links are written relative to the tracker file itself
// (merge-tracker.mjs normalization): ../reports/... when the tracker
// lives under data/. Legacy trackers may still carry root-relative
// links; both must resolve to the same on-disk report.
applications := `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 1 | 2026-06-03 | Acme | Engineer | 4.0/5 | Evaluated | ✅ | [1](../reports/001-acme-2026-06-03.md) | Tracker-relative link |
| 2 | 2026-06-03 | Legacy Co | Engineer | 3.0/5 | Evaluated | ❌ | [2](reports/002-legacy-2026-06-03.md) | Legacy root-relative link |
`
if err := os.WriteFile(filepath.Join(dataDir, "applications.md"), []byte(applications), 0o644); err != nil {
t.Fatalf("failed to write applications tracker: %v", err)
}
for _, name := range []string{"001-acme-2026-06-03.md", "002-legacy-2026-06-03.md"} {
if err := os.WriteFile(filepath.Join(reportsDir, name), []byte("# Report\n"), 0o644); err != nil {
t.Fatalf("failed to write report %s: %v", name, err)
}
}
apps := ParseApplications(tempDir)
if len(apps) != 2 {
t.Fatalf("expected 2 parsed applications, got %d", len(apps))
}
wantFirst := filepath.Join("reports", "001-acme-2026-06-03.md")
if apps[0].ReportPath != wantFirst {
t.Fatalf("expected tracker-relative link to resolve to %q, got %q", wantFirst, apps[0].ReportPath)
}
wantSecond := filepath.Join("reports", "002-legacy-2026-06-03.md")
if apps[1].ReportPath != wantSecond {
t.Fatalf("expected legacy root-relative link to resolve to %q, got %q", wantSecond, apps[1].ReportPath)
}
// Every consumer joins ReportPath against careerOpsPath — both rows
// must point at files that exist.
for i, app := range apps {
if _, err := os.Stat(filepath.Join(tempDir, app.ReportPath)); err != nil {
t.Fatalf("row %d: resolved report path %q does not exist: %v", i, app.ReportPath, err)
}
}
}
// writeTracker writes applications.md under data/ and returns the temp root and
// the tracker path.
func writeTracker(t *testing.T, body string) (string, string) {
t.Helper()
tempDir := t.TempDir()
dataDir := filepath.Join(tempDir, "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
path := filepath.Join(dataDir, "applications.md")
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write tracker: %v", err)
}
return tempDir, path
}
const insertedColumnTracker = `# Applications Tracker
| # | Date | Company | Role | Location | Score | Status | PDF | Report | Notes |
|---|------|---------|------|----------|-------|--------|-----|--------|-------|
| 1 | 2026-06-01 | Acme | VP Marketing | Remote | 4.5/5 | Applied | ✅ | [1](reports/001.md) | hot lead |
`
// A tracker with a Location column inserted before Score (the customized layout
// the Node tracker tooling supports since #954) must not desync the Go reader.
// Without header-aware mapping, Status reads the Score cell and the report link
// reads the PDF cell, so ReportNumber comes back empty.
func TestParseApplicationsMapsColumnsByHeader(t *testing.T) {
tempDir, _ := writeTracker(t, insertedColumnTracker)
apps := ParseApplications(tempDir)
if len(apps) != 1 {
t.Fatalf("expected 1 application, got %d", len(apps))
}
a := apps[0]
if a.Company != "Acme" {
t.Errorf("Company = %q, want \"Acme\"", a.Company)
}
if a.Role != "VP Marketing" {
t.Errorf("Role = %q, want \"VP Marketing\"", a.Role)
}
if a.Status != "Applied" {
t.Errorf("Status = %q, want \"Applied\"", a.Status)
}
if a.ScoreRaw != "4.5/5" {
t.Errorf("ScoreRaw = %q, want \"4.5/5\"", a.ScoreRaw)
}
if !a.HasPDF {
t.Errorf("HasPDF = false, want true")
}
if a.ReportNumber != "1" {
t.Errorf("ReportNumber = %q, want \"1\"", a.ReportNumber)
}
}
// End-to-end status update on the inserted-column layout: parse, update, and
// re-parse. Only the Status cell may change; every other cell stays intact.
func TestUpdateApplicationStatusInsertedColumn(t *testing.T) {
tempDir, path := writeTracker(t, insertedColumnTracker)
apps := ParseApplications(tempDir)
if len(apps) != 1 {
t.Fatalf("expected 1 application, got %d", len(apps))
}
if err := UpdateApplicationStatus(tempDir, apps[0], "Interview"); err != nil {
t.Fatalf("UpdateApplicationStatus: %v", err)
}
out, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read back: %v", err)
}
got := string(out)
if !strings.Contains(got, "| Interview |") {
t.Errorf("Status cell not updated to Interview, file now:\n%s", got)
}
if strings.Count(got, "Interview") != 1 {
t.Errorf("write touched an unintended cell; %d occurrences of Interview:\n%s", strings.Count(got, "Interview"), got)
}
for _, cell := range []string{"| Acme |", "| VP Marketing |", "| Remote |", "| 4.5/5 |", "| ✅ |"} {
if !strings.Contains(got, cell) {
t.Errorf("expected intact cell %q missing after write:\n%s", cell, got)
}
}
reparsed := ParseApplications(tempDir)
if reparsed[0].Status != "Interview" {
t.Errorf("reparsed Status = %q, want \"Interview\"", reparsed[0].Status)
}
if reparsed[0].ScoreRaw != "4.5/5" {
t.Errorf("reparsed ScoreRaw = %q, want \"4.5/5\"", reparsed[0].ScoreRaw)
}
}
// resolveTrackerColumns detects the header layout, and falls back to the legacy
// fixed layout when no recognizable header row is present.
func TestResolveTrackerColumns(t *testing.T) {
header := strings.Split(insertedColumnTracker, "\n")
cols := resolveTrackerColumns(header)
if cols["status"] != 6 {
t.Errorf("status index = %d, want 6 (inserted Location column)", cols["status"])
}
if cols["score"] != 5 {
t.Errorf("score index = %d, want 5", cols["score"])
}
headerless := []string{"| 1 | 2026-06-01 | Acme | VP Marketing | 4.5/5 | Applied | ✅ | [1](reports/001.md) | note |"}
fallback := resolveTrackerColumns(headerless)
if fallback["status"] != 5 {
t.Errorf("fallback status index = %d, want 5 (legacy layout)", fallback["status"])
}
}
// A duplicated header name resolves to its LAST occurrence, matching
// detectColumns in tracker-parse.mjs — the JS and Go readers must map an
// identical header row identically.
func TestResolveTrackerColumnsDuplicateHeaderLastWins(t *testing.T) {
dup := strings.Split(`| # | Notes | Company | Role | Score | Status | PDF | Report | Notes |
|---|-------|---------|------|-------|--------|-----|--------|-------|
| 1 | stray | Acme | Engineer | 4.0/5 | Applied | ✅ | — | real note |`, "\n")
cols := resolveTrackerColumns(dup)
// Verify that the last "Notes" column wins
if cols["notes"] != 8 {
t.Fatalf("notes index = %d, expected 8 (tracker-parse.mjs parity)", cols["notes"])
}
}
// A Via column (intermediary channel, #1596) between Company and Role maps by
// header name; later columns keep their correct indices.
func TestResolveTrackerColumnsVia(t *testing.T) {
viaTracker := strings.Split(`| # | Date | Company | Via | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|-----|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-05 | ? | Hays | Data Engineer | 4.2/5 | Applied | ✅ | — | fintech, Leeds |`, "\n")
cols := resolveTrackerColumns(viaTracker)
if cols["via"] != 3 {
t.Errorf("via index = %d, want 3", cols["via"])
}
if cols["role"] != 4 {
t.Errorf("role index = %d, want 4 (shifted by Via column)", cols["role"])
}
if cols["status"] != 6 {
t.Errorf("status index = %d, want 6", cols["status"])
}
}
// TestNormalizeStatus verifies that localized string variants map to the canonical English form.
func TestNormalizeStatus(t *testing.T) {
tests := []struct {
input string
want string
}{
// Turkish status strings
{"değerlendirildi", "evaluated"},
{"Değerlendirildi", "evaluated"},
{"DEĞERLENDİRİLDİ", "evaluated"},
{"başvuruldu", "applied"},
{"Başvuruldu", "applied"},
{"BAŞVURULDU", "applied"},
{"yanıt verildi", "responded"},
{"yanıt_verildi", "responded"},
{"YANIT VERİLDİ", "responded"},
{"mülakat", "interview"},
{"Mülakat", "interview"},
{"MÜLAKAT", "interview"},
{"teklif", "offer"},
{"Teklif", "offer"},
{"TEKLİF", "offer"},
{"reddedildi", "rejected"},
{"Reddedildi", "rejected"},
{"REDDEDİLDİ", "rejected"},
{"iptal edildi", "discarded"},
{"iptal_edildi", "discarded"},
{"İPTAL EDİLDİ", "discarded"},
{"uygun değil", "skip"},
{"uygun_değil", "skip"},
{"UYGUN DEĞİL", "skip"},
// No-diacritic variants
{"degerlendirildi", "evaluated"},
{"DEGERLENDIRILDI", "evaluated"},
{"basvuruldu", "applied"},
{"BASVURULDU", "applied"},
{"yanit verildi", "responded"},
{"mulakat", "interview"},
{"uygun degil", "skip"},
// English status strings
{"Evaluated", "evaluated"},
{"Applied", "applied"},
{"Responded", "responded"},
{"Interview", "interview"},
{"Offer", "offer"},
{"Rejected", "rejected"},
{"Discarded", "discarded"},
{"SKIP", "skip"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
if got := NormalizeStatus(tt.input); got != tt.want {
t.Errorf("NormalizeStatus(%q) = %q; want %q", tt.input, got, tt.want)
}
})
}
}