package seeder_test import ( "testing" "time" "github.com/krow/krow-backend/go-api/internal/seeder" ) // The shift generator is a port of src/api/attendanceSeed.js. These tests pin // the distribution that module's own documentation describes, so a drift in the // port shows up as a failing assertion rather than as quietly different // attendance figures. func shiftsFor(anchor time.Time, email string) []map[string]any { var out []map[string]any for _, r := range seeder.BuildShifts(anchor) { if r["worker_email"] == email { out = append(out, r) } } return out } func TestShiftDistributionMatchesSource(t *testing.T) { anchor := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local) all := seeder.BuildShifts(anchor) counts := map[string]int{} for _, r := range all { counts[r["status"].(string)]++ } // Marcus alone supplies the attendance anomaly: two absences, one no-show // and three late arrivals in the recent window, plus one older late. if counts["absent"] != 2 { t.Errorf("absent = %d, want 2", counts["absent"]) } if counts["no_show"] != 1 { t.Errorf("no_show = %d, want 1", counts["no_show"]) } // Marcus i=1,5,9,26 plus Marco i=14. if counts["late"] != 5 { t.Errorf("late = %d, want 5", counts["late"]) } if counts["present"] == 0 { t.Error("no present shifts generated") } } func TestShiftRosterIsThreePeople(t *testing.T) { anchor := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local) emails := map[string]bool{} for _, r := range seeder.BuildShifts(anchor) { emails[r["worker_email"].(string)] = true } if len(emails) != 3 { t.Fatalf("roster has %d people, want 3 (one per hire)", len(emails)) } } // A missed shift is zero hours worked, not a short one — and the schema's // shift_records_absence_has_no_hours constraint depends on it. func TestAbsencesHaveNoHours(t *testing.T) { anchor := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local) for _, r := range seeder.BuildShifts(anchor) { status := r["status"].(string) if status != "absent" && status != "no_show" { continue } if r["actual_hours"].(float64) != 0 { t.Errorf("%s: %s shift has actual_hours %v", r["id"], status, r["actual_hours"]) } if r["minutes_late"].(int) != 0 { t.Errorf("%s: %s shift has minutes_late %v", r["id"], status, r["minutes_late"]) } if r["actual_start"] != nil || r["actual_end"] != nil { t.Errorf("%s: %s shift has actual timestamps", r["id"], status) } } } // Antoine's overtime climbs week on week — a trend rather than a spike. It is // the overtime anomaly the analytics are shaped to surface. func TestAntoineOvertimeClimbs(t *testing.T) { anchor := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local) shifts := shiftsFor(anchor, "antoine.dubois@email.com") if len(shifts) == 0 { t.Fatal("no shifts generated for Antoine") } // BuildShifts returns most-recent-first, so recent overtime should exceed // the overtime from the far end of the window. var recent, older float64 for i, r := range shifts { ot := r["overtime_hours"].(float64) if i < 10 { recent += ot } if i >= len(shifts)-10 { older += ot } } if recent <= older { t.Errorf("overtime is not climbing: recent 10 = %.1fh, oldest 10 = %.1fh", recent, older) } } // created_date is the instant the shift was worked. dataResolver windows every // collection on it, so a shift dated anywhere else would vanish from every // period reading. func TestShiftCreatedDateIsTheShiftInstant(t *testing.T) { anchor := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local) for _, r := range seeder.BuildShifts(anchor) { if r["created_date"] != r["scheduled_start"] { t.Fatalf("%s: created_date %v is not the scheduled start %v", r["id"], r["created_date"], r["scheduled_start"]) } } } // The window rolls forward with the anchor: shifts must stay recent relative to // whenever the seeder runs, which is the whole reason this is a port rather // than a frozen snapshot. func TestShiftWindowFollowsTheAnchor(t *testing.T) { early := seeder.BuildShifts(time.Date(2026, 3, 1, 12, 0, 0, 0, time.Local)) late := seeder.BuildShifts(time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local)) if early[0]["created_date"] == late[0]["created_date"] { t.Fatal("shift dates did not move with the anchor") } if got := late[0]["created_date"].(string)[:4]; got != "2026" { t.Errorf("most recent shift is dated %s", got) } }