287 lines
9.8 KiB
Go
287 lines
9.8 KiB
Go
package owliver
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/krow/krow-backend/go-api/internal/domain"
|
||
)
|
||
|
||
// Highlights — what is worth asking when nothing has been typed.
|
||
//
|
||
// Everything here is a pure function of a Context written out by hand, so the
|
||
// assertions are about the ranking itself rather than about a fixture. The
|
||
// database read that produces a real Context is exercised over HTTP, in
|
||
// internal/httpserver.
|
||
|
||
// ids is the intents a result names, in order.
|
||
func ids(list []Suggestion) []string {
|
||
out := make([]string, len(list))
|
||
for i, s := range list {
|
||
out[i] = s.Intent
|
||
}
|
||
return out
|
||
}
|
||
|
||
// has reports whether an intent was offered.
|
||
func has(list []Suggestion, intent string) bool {
|
||
for _, s := range list {
|
||
if s.Intent == intent {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// A workspace with nothing in it is asked nothing.
|
||
//
|
||
// The alternative — three questions about empty sets — is the padding this
|
||
// whole path is supposed to refuse. "You have no positions, would you like to
|
||
// know which position has the strongest pipeline?" is worse than silence.
|
||
func TestHighlightsOfAnEmptyOrganizationAreEmpty(t *testing.T) {
|
||
for _, page := range []string{"positions", "candidates", "control-center", "analytics",
|
||
"talent-pool", "hired-history", "krow-forge", "activity"} {
|
||
if got := Highlights(page, Context{}, domain.RoleAdmin); len(got) != 0 {
|
||
t.Errorf("%s offered %v for an empty organization", page, ids(got))
|
||
}
|
||
}
|
||
}
|
||
|
||
// The counts decide, and the loudest one leads.
|
||
func TestHighlightsRankByTheData(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
page string
|
||
ctx Context
|
||
wantTop string
|
||
}{
|
||
{
|
||
name: "unfinished drafts dominate a quiet workspace",
|
||
page: "positions",
|
||
ctx: Context{DraftPositions: 4, ActivePositions: 1},
|
||
wantTop: "position-drafts",
|
||
},
|
||
{
|
||
name: "a role nobody applied to outranks the drafts",
|
||
page: "positions",
|
||
ctx: Context{DraftPositions: 1, ActivePositions: 3, StarvedPositions: 2},
|
||
wantTop: "positions-attention",
|
||
},
|
||
{
|
||
// The tier, not the pile. This is the case weight × count gets
|
||
// wrong: forty applications outnumber one abandoned role, and the
|
||
// abandoned role is still the finding.
|
||
name: "one starved role outranks a large pile of everything else",
|
||
page: "positions",
|
||
ctx: Context{ActivePositions: 9, StarvedPositions: 1, Applications: 40, Unscreened: 22},
|
||
wantTop: "positions-attention",
|
||
},
|
||
{
|
||
// And the thing that just happened is heard, however small.
|
||
name: "a single unfinished draft is heard over a busy funnel",
|
||
page: "positions",
|
||
ctx: Context{ActivePositions: 9, DraftPositions: 1, Applications: 40, Unscreened: 22},
|
||
wantTop: "position-drafts",
|
||
},
|
||
{
|
||
name: "unscored candidates are the screening gap",
|
||
page: "candidates",
|
||
ctx: Context{Applications: 12, Unscreened: 9},
|
||
wantTop: "screening-gaps",
|
||
},
|
||
{
|
||
name: "a flagged interview is a risk before it is anything else",
|
||
page: "candidates",
|
||
ctx: Context{Applications: 12, Unscreened: 2, FlaggedRisks: 4},
|
||
wantTop: "candidate-risk",
|
||
},
|
||
{
|
||
name: "unverified profiles are the talent pool's gap",
|
||
page: "talent-pool",
|
||
ctx: Context{Profiles: 20, UnverifiedProfiles: 8},
|
||
wantTop: "talent-verification",
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
got := Highlights(c.page, c.ctx, domain.RoleAdmin)
|
||
if len(got) == 0 {
|
||
t.Fatalf("%s offered nothing", c.page)
|
||
}
|
||
if got[0].Intent != c.wantTop {
|
||
t.Fatalf("led with %q, want %q (whole list %v)", got[0].Intent, c.wantTop, ids(got))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// A comparison needs two things to compare.
|
||
//
|
||
// "Which position has the strongest pipeline?" is not a question about a
|
||
// workspace holding one position: the answer is a table of one row, which is
|
||
// the same padding as a question about an empty set.
|
||
func TestHighlightsDoNotOfferAComparisonOfOne(t *testing.T) {
|
||
one := Highlights("positions", Context{ActivePositions: 1, Applications: 3}, domain.RoleAdmin)
|
||
if has(one, "position-strength") {
|
||
t.Fatalf("offered a pipeline comparison across one position: %v", ids(one))
|
||
}
|
||
|
||
two := Highlights("positions", Context{ActivePositions: 2, Applications: 3}, domain.RoleAdmin)
|
||
if !has(two, "position-strength") {
|
||
t.Fatalf("two positions is a comparison and was not offered: %v", ids(two))
|
||
}
|
||
}
|
||
|
||
// Never more than three, and never the same reading twice.
|
||
func TestHighlightsAreCappedAndDistinct(t *testing.T) {
|
||
loud := Context{
|
||
ActivePositions: 9, DraftPositions: 7, StarvedPositions: 5, UnderfilledActive: 6,
|
||
Applications: 40, Unscreened: 22, Shortlisted: 9, Interviewing: 6, Hired: 11,
|
||
FlaggedRisks: 4, Staff: 30, Profiles: 60, UnverifiedProfiles: 25, Courses: 14,
|
||
ActivityEvents: 900,
|
||
}
|
||
|
||
for page := range catalogue {
|
||
got := Highlights(page, loud, domain.RoleAdmin)
|
||
if len(got) > MaxSuggestions {
|
||
t.Errorf("%s returned %d, the cap is %d", page, len(got), MaxSuggestions)
|
||
}
|
||
seen := map[string]bool{}
|
||
for _, s := range got {
|
||
if seen[s.Intent] {
|
||
t.Errorf("%s repeated %q", page, s.Intent)
|
||
}
|
||
seen[s.Intent] = true
|
||
if s.Text == "" || s.Intent == "" {
|
||
t.Errorf("%s returned an incomplete suggestion: %+v", page, s)
|
||
}
|
||
// Nothing was typed, so nothing asked for a rendering.
|
||
if s.Capability != "" {
|
||
t.Errorf("%s carried a shape nobody asked for: %+v", page, s)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Permission is decided before relevance, exactly as it is in Suggest.
|
||
//
|
||
// A count cannot promote a reading the caller may not perform: talent's rows
|
||
// are narrowed by the policy table, so an org-wide figure is not theirs to be
|
||
// told even as a ranking input.
|
||
func TestHighlightsRefuseWhatARoleCannotRead(t *testing.T) {
|
||
loud := Context{ActivePositions: 9, DraftPositions: 7, Applications: 40, Unscreened: 22,
|
||
Staff: 30, Profiles: 60}
|
||
|
||
for _, page := range []string{"positions", "candidates", "hired-history", "control-center"} {
|
||
if got := Highlights(page, loud, domain.RoleTalent); len(got) != 0 {
|
||
t.Errorf("%s offered talent %v", page, ids(got))
|
||
}
|
||
if got := Highlights(page, loud, domain.RoleAdmin); len(got) == 0 {
|
||
t.Errorf("%s offered an admin nothing — the assertion above proves nothing", page)
|
||
}
|
||
}
|
||
}
|
||
|
||
// An unrecognised role is offered nothing, as everywhere else.
|
||
func TestHighlightsDenyAnUnknownRole(t *testing.T) {
|
||
loud := Context{ActivePositions: 9, Applications: 40, Unscreened: 22}
|
||
for _, role := range []domain.Role{"", "root", "superuser", "Admin"} {
|
||
if got := Highlights("positions", loud, role); len(got) != 0 {
|
||
t.Errorf("role %q was offered %v", role, ids(got))
|
||
}
|
||
}
|
||
}
|
||
|
||
// A page the catalogue does not hold answers with an empty slice, not nil.
|
||
func TestHighlightsOfAnUnknownPageAreAnEmptySlice(t *testing.T) {
|
||
loud := Context{ActivePositions: 9, Applications: 40}
|
||
for _, page := range []string{"", "nowhere", "POSITIONS", "settings"} {
|
||
got := Highlights(page, loud, domain.RoleAdmin)
|
||
if got == nil {
|
||
t.Fatalf("page %q returned nil, which a client cannot range over", page)
|
||
}
|
||
if len(got) != 0 {
|
||
t.Errorf("page %q returned %v", page, ids(got))
|
||
}
|
||
}
|
||
}
|
||
|
||
// The same state always produces the same three in the same order.
|
||
func TestHighlightsAreDeterministic(t *testing.T) {
|
||
ctx := Context{ActivePositions: 5, DraftPositions: 3, StarvedPositions: 2,
|
||
Applications: 20, Unscreened: 7, Shortlisted: 4}
|
||
|
||
first := ids(Highlights("positions", ctx, domain.RoleAdmin))
|
||
for range 25 {
|
||
again := ids(Highlights("positions", ctx, domain.RoleAdmin))
|
||
if len(again) != len(first) {
|
||
t.Fatalf("got %v, first run was %v", again, first)
|
||
}
|
||
for i := range first {
|
||
if again[i] != first[i] {
|
||
t.Fatalf("got %v, first run was %v", again, first)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Every declared Signal names an intent the catalogue actually holds, and is
|
||
// monotonic: more of the thing it counts can never make the reading less
|
||
// relevant. A signal that fell as its subject grew would be a sign error, and
|
||
// the symptom would be a suggestion that disappears exactly when it matters.
|
||
func TestSignalsAreMonotonic(t *testing.T) {
|
||
small := Context{
|
||
ActivePositions: 2, DraftPositions: 1, StarvedPositions: 1, UnderfilledActive: 1,
|
||
Applications: 5, Unscreened: 2, Shortlisted: 1, Interviewing: 1, Hired: 1,
|
||
FlaggedRisks: 1, Staff: 2, Profiles: 3, UnverifiedProfiles: 1, Courses: 1,
|
||
ActivityEvents: 10,
|
||
}
|
||
large := Context{
|
||
ActivePositions: 20, DraftPositions: 10, StarvedPositions: 10, UnderfilledActive: 10,
|
||
Applications: 50, Unscreened: 20, Shortlisted: 10, Interviewing: 10, Hired: 10,
|
||
FlaggedRisks: 10, Staff: 20, Profiles: 30, UnverifiedProfiles: 10, Courses: 10,
|
||
ActivityEvents: 100,
|
||
}
|
||
|
||
for page, intents := range catalogue {
|
||
for _, intent := range intents {
|
||
if intent.Signal == nil {
|
||
continue
|
||
}
|
||
lo, hi := intent.Signal(small), intent.Signal(large)
|
||
if lo < 0 || hi < 0 {
|
||
t.Errorf("%s/%s: a negative signal (%d, %d)", page, intent.ID, lo, hi)
|
||
}
|
||
if hi < lo {
|
||
t.Errorf("%s/%s: signal fell as the workspace grew (%d → %d)", page, intent.ID, lo, hi)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// A tier decides before a count does, everywhere.
|
||
//
|
||
// Stated as a property rather than as a list of pairs, because it is the whole
|
||
// design: a signal is "how much does this matter when it is happening at all",
|
||
// and a count that could climb into the tier above would make a large
|
||
// organization's biggest pile outrank its most urgent finding.
|
||
func TestATierAlwaysOutranksACount(t *testing.T) {
|
||
for tier := 1; tier <= 9; tier++ {
|
||
lowestAbove := when(1, tier+1)
|
||
highestWithin := when(maxTiebreak*10, tier) // deliberately over the cap
|
||
if highestWithin >= lowestAbove {
|
||
t.Fatalf("tier %d saturates into tier %d: %d >= %d",
|
||
tier, tier+1, highestWithin, lowestAbove)
|
||
}
|
||
}
|
||
|
||
// Nothing happening scores nothing, whatever the tier claims.
|
||
for tier := 1; tier <= 9; tier++ {
|
||
for _, count := range []int{0, -1, -1000} {
|
||
if got := when(count, tier); got != 0 {
|
||
t.Fatalf("when(%d, %d) = %d, want 0", count, tier, got)
|
||
}
|
||
}
|
||
}
|
||
}
|