first commit
This commit is contained in:
134
go-api/internal/seeder/shifts_test.go
Normal file
134
go-api/internal/seeder/shifts_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user