agent build
This commit is contained in:
246
go-api/internal/tools/hiring_test.go
Normal file
246
go-api/internal/tools/hiring_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user