Files
krow_backend/go-api/internal/seeder/shifts_test.go
2026-08-24 13:06:29 +05:30

135 lines
4.3 KiB
Go

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)
}
}