package tools_test import ( "context" "encoding/json" "fmt" "testing" "github.com/krow/krow-backend/go-api/internal/authctx" "github.com/krow/krow-backend/go-api/internal/testutil" "github.com/krow/krow-backend/go-api/internal/tools" ) // An ai_score of 0 is the absence of a score, not a score of zero. The product // states this rule in candidateIntelligence.js: "null rather than zeros ... so // an unscreened candidate shows '—' instead of a confident-looking 0". // // candidates_quality shipped without a test and broke the rule three ways: it // counted every unscored candidate as weak, dragged the average down with their // zeros, and offered no way to tell how many the average was over. On the real // corpus that reported "16 weak, averaging 28" where the truth was "1 weak, 9 // scored, averaging 76" — a number that would push a manager to reject a pool // that is in fact strong. func TestCandidateQualityTreatsAnAbsentScoreAsAbsentNotAsZero(t *testing.T) { h := testutil.New(t) f := seedFunnel(t, h, "quality-zero") // Dana: ai_screened, scored 88 reg := liveRegistry(t, h) ctx := context.Background() // Three more, sharing Dana's org and posting. for _, c := range []struct { name, email, status string score int }{ {"Unscored Applicant", "u1-quality@example.test", "applied", 0}, {"Advanced Unscored", "u2-quality@example.test", "interview", 0}, {"Genuinely Weak", "u3-quality@example.test", "ai_screened", 30}, } { if _, err := h.Pool.Exec(ctx, ` INSERT INTO job_applications (org_id, job_posting_id, applicant_name, email, status, ai_score) SELECT org_id, job_posting_id, $2, $3, $4::application_status, $5 FROM job_applications WHERE id = $1::uuid`, f.appID, c.name, c.email, c.status, c.score); err != nil { t.Fatalf("seed %s: %v", c.name, err) } } res := reg.Dispatch(ctx, tools.Context{Principal: f.admin, RunID: "run_q1"}, "candidates_quality", json.RawMessage(`{}`)) if res.Error != nil { t.Fatalf("candidates_quality failed: %+v", res.Error) } var got struct { Applications int `json:"applications"` Strong int `json:"strong"` Weak int `json:"weak"` Unscreened int `json:"unscreened"` Scored int `json:"scored"` Average int `json:"averageMatchScore"` Basis int `json:"averageMatchScoreBasis"` } body, _ := json.Marshal(res.Data) if err := json.Unmarshal(body, &got); err != nil { t.Fatalf("decode %s: %v", body, err) } // Only the 30 is weak. The two zeros are unscored, not bad. for _, c := range []struct { field string got int want int }{ {"applications", got.Applications, 4}, {"strong", got.Strong, 1}, // 88 {"weak", got.Weak, 1}, // 30 only — not the two zeros {"unscreened", got.Unscreened, 1}, // the one at 'applied' {"scored", got.Scored, 2}, // 88 and 30 {"averageMatchScore", got.Average, 59}, // (88+30)/2, not (88+30+0+0)/4 {"averageMatchScoreBasis", got.Basis, 2}, } { if c.got != c.want { t.Errorf("%s = %d, want %d — full payload: %s", c.field, c.got, c.want, body) } } } // seedRatedWorkers creates an org with workers at the given krow scores. A score // of 0 is how the product records "not yet rated" — worker_profiles.krow_score is // NOT NULL, so there is no null to distinguish it, which is exactly the trap. func seedRatedWorkers(t *testing.T, h *testutil.Harness, slug string, workers []struct { name string krow int rated float64 }) (string, authctx.Identity) { t.Helper() ctx := context.Background() org := freshOrg(t, h, slug) boss := fmt.Sprintf("boss-%s@example.test", slug) admin := authctx.Identity{ UserID: seedUser(t, h, org, boss, "admin"), OrgID: org, Role: "admin", Email: boss, } for i, w := range workers { if _, err := h.Pool.Exec(ctx, ` INSERT INTO worker_profiles (org_id, full_name, email, krow_score, reliability_score, attendance_score, performance_score, client_rating) VALUES ($1::uuid, $2, $3, $4, $4, $4, $4, $5)`, org, w.name, fmt.Sprintf("w%d-%s@example.test", i, slug), w.krow, w.rated); err != nil { t.Fatalf("seed worker %s: %v", w.name, err) } } return org, admin } // A krow_score of 0 means "Not yet scored" — the product says so in as many // words (dataResolver.js) and its lowest band starts above 0 (TalentPool.jsx). // Reading it as a score of zero made hires_performance report the four unrated // workers as the four *weakest* performers by name, and dragged every average // down with them: a workforce rated 4.7 of 5 was reported at 1.6. func TestPerformanceNeverNamesAnUnratedWorkerAsWeakest(t *testing.T) { h := testutil.New(t) org, admin := seedRatedWorkers(t, h, "perf-unrated", []struct { name string krow int rated float64 }{ {"Scored High", 90, 5}, {"Scored Low", 50, 4}, {"Never Rated", 0, 0}, {"Also Never Rated", 0, 0}, }) _ = org reg := liveRegistry(t, h) res := reg.Dispatch(context.Background(), tools.Context{Principal: admin, RunID: "run_p1"}, "hires_performance", json.RawMessage(`{}`)) if res.Error != nil { t.Fatalf("hires_performance failed: %+v", res.Error) } var got struct { Workers int `json:"workers"` Scored int `json:"scored"` Unscored int `json:"unscored"` AvgKrow int `json:"averageKrowScore"` AvgRated float64 `json:"averageClientRating"` Weakest []struct { Name string `json:"name"` } `json:"weakest"` Strongest []struct { Name string `json:"name"` } `json:"strongest"` } body, _ := json.Marshal(res.Data) if err := json.Unmarshal(body, &got); err != nil { t.Fatalf("decode %s: %v", body, err) } // The headline the operator acts on. for _, c := range []struct { field string got int want int }{ {"workers", got.Workers, 4}, {"scored", got.Scored, 2}, {"unscored", got.Unscored, 2}, {"averageKrowScore", got.AvgKrow, 70}, // (90+50)/2, not (90+50+0+0)/4 = 35 } { if c.got != c.want { t.Errorf("%s = %d, want %d — payload: %s", c.field, c.got, c.want, body) } } if got.AvgRated != 4.5 { // (5+4)/2, not (5+4+0+0)/4 = 2.25 t.Errorf("averageClientRating = %v, want 4.5 — payload: %s", got.AvgRated, body) } // The part that names real people. for _, w := range append(append([]struct { Name string `json:"name"` }{}, got.Weakest...), got.Strongest...) { if w.Name == "Never Rated" || w.Name == "Also Never Rated" { t.Errorf("%q has no rating but was ranked by score — payload: %s", w.Name, body) } } if len(got.Weakest) == 0 || got.Weakest[0].Name != "Scored Low" { t.Errorf("weakest should start at the lowest *rated* worker — payload: %s", body) } } // TalentPool.jsx puts the lowest band at (krow_score || 0) > 0 && < 60, so an // unrated worker is counted separately rather than as the least ready. func TestTalentPoolCountsUnratedSeparatelyFromLowScoring(t *testing.T) { h := testutil.New(t) _, admin := seedRatedWorkers(t, h, "pool-bands", []struct { name string krow int rated float64 }{ {"Ready", 90, 5}, {"Developing", 70, 4}, {"Early", 30, 3}, {"Unrated", 0, 0}, }) reg := liveRegistry(t, h) res := reg.Dispatch(context.Background(), tools.Context{Principal: admin, RunID: "run_p2"}, "talent_pool", json.RawMessage(`{}`)) if res.Error != nil { t.Fatalf("talent_pool failed: %+v", res.Error) } var got struct { Workers int `json:"workers"` JobReady int `json:"jobReady"` Developing int `json:"developing"` Early int `json:"early"` Unscored int `json:"unscored"` } body, _ := json.Marshal(res.Data) if err := json.Unmarshal(body, &got); err != nil { t.Fatalf("decode %s: %v", body, err) } for _, c := range []struct { field string got int want int }{ {"workers", got.Workers, 4}, {"jobReady", got.JobReady, 1}, {"developing", got.Developing, 1}, {"early", got.Early, 1}, // the 30 only — not the unrated worker {"unscored", got.Unscored, 1}, } { if c.got != c.want { t.Errorf("%s = %d, want %d — payload: %s", c.field, c.got, c.want, body) } } }