135 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|