package seeder_test import ( "testing" "time" "github.com/krow/krow-backend/go-api/internal/seeder" ) func activityFixture() []map[string]any { // The authored shape: a burst, then a gap, then a later cluster. return []map[string]any{ {"id": "act_1", "event_type": "create_position", "created_date": "2026-07-18T09:00:00.000Z"}, {"id": "act_2", "event_type": "apply_job", "created_date": "2026-07-24T09:00:00.000Z"}, {"id": "act_3", "event_type": "hire_candidate", "created_date": "2026-07-25T09:00:00.000Z"}, {"id": "act_4", "event_type": "login", "created_date": "2026-08-06T09:00:00.000Z"}, } } func mustTime(t *testing.T, rec map[string]any) time.Time { t.Helper() at, err := time.Parse(time.RFC3339, rec["created_date"].(string)) if err != nil { t.Fatalf("created_date %v does not parse: %v", rec["created_date"], err) } return at } // The newest event lands on now, so the demo always has something recent. func TestRebaseActivityAnchorsTheNewestEventToNow(t *testing.T) { now := time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC) out := seeder.RebaseToNow(activityFixture(), "created_date", now) if len(out) != 4 { t.Fatalf("got %d records, want 4", len(out)) } newest := mustTime(t, out[3]) if !newest.Equal(now) { t.Errorf("newest event at %s, want %s", newest, now) } for _, rec := range out { if at := mustTime(t, rec); at.After(now) { t.Errorf("%v is in the future at %s", rec["id"], at) } } } // Every gap is preserved. The shape of this data — three hires on one day, a // quiet fortnight before it — is what every reader of it is looking at. func TestRebaseActivityPreservesTheGaps(t *testing.T) { in := activityFixture() now := time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC) out := seeder.RebaseToNow(in, "created_date", now) for i := 1; i < len(in); i++ { before := mustTime(t, in[i]).Sub(mustTime(t, in[i-1])) after := mustTime(t, out[i]).Sub(mustTime(t, out[i-1])) if before != after { t.Errorf("gap %d changed from %s to %s", i, before, after) } } } // The whole point: events land inside the windows the activity tools ask about. func TestRebaseActivityLandsInsideTheReportingWindows(t *testing.T) { now := time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC) out := seeder.RebaseToNow(activityFixture(), "created_date", now) var last7, last30 int for _, rec := range out { age := now.Sub(mustTime(t, rec)) if age < 7*24*time.Hour { last7++ } if age < 30*24*time.Hour { last30++ } } if last7 == 0 { t.Error("no events in the last 7 days — the demo still looks dead, " + "which is the condition this function exists to prevent") } if last30 != 4 { t.Errorf("%d of 4 events in the last 30 days, want all of them", last30) } } // Seeding twice must not compound the shift. func TestRebaseActivityIsIdempotent(t *testing.T) { now := time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC) first := seeder.RebaseToNow(activityFixture(), "created_date", now) second := seeder.RebaseToNow(activityFixture(), "created_date", now) for i := range first { if first[i]["created_date"] != second[i]["created_date"] { t.Errorf("record %d differs between runs: %v vs %v", i, first[i]["created_date"], second[i]["created_date"]) } } } // The fixture is read once and may be seeded into several organizations, so // rebasing must not rewrite it in place. func TestRebaseActivityDoesNotMutateTheFixture(t *testing.T) { in := activityFixture() original := in[0]["created_date"] seeder.RebaseToNow(in, "created_date", time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC)) if in[0]["created_date"] != original { t.Errorf("the fixture was rewritten in place: %v became %v", original, in[0]["created_date"]) } } func TestRebaseActivityHandlesNothingToDo(t *testing.T) { now := time.Now() if got := seeder.RebaseToNow(nil, "created_date", now); got != nil { t.Errorf("nil in, %v out", got) } if got := seeder.RebaseToNow([]map[string]any{}, "created_date", now); len(got) != 0 { t.Errorf("empty in, %d out", len(got)) } // Unparseable dates are left alone rather than guessed at. junk := []map[string]any{{"id": "x", "created_date": "not a date"}} out := seeder.RebaseToNow(junk, "created_date", now) if out[0]["created_date"] != "not a date" { t.Errorf("an unreadable date was rewritten to %v", out[0]["created_date"]) } } // The gap WITHIN a record matters as much as the gaps between records. // buildHires subtracts an application's created_date from its updated_date to // get time-to-hire, so shifting one and not the other invents a hire that took // three weeks or minus one. The seeder's TestSeedPreservesSourceValues caught // this the first time; this pins it where the shifting happens. func TestRebaseToNowShiftsEveryTimestampTogether(t *testing.T) { in := []map[string]any{ { "id": "app_1", "created_date": "2026-07-20T09:00:00.000Z", "updated_date": "2026-07-25T09:00:00.000Z", // five days later "status": "hired", "ai_score": 91, }, { "id": "app_2", "created_date": "2026-08-06T09:00:00.000Z", "updated_date": "2026-08-06T09:00:00.000Z", "status": "applied", }, } now := time.Date(2027, 3, 14, 12, 0, 0, 0, time.UTC) out := seeder.RebaseToNow(in, "created_date", now) created := mustTime(t, out[0]) updated, err := time.Parse(time.RFC3339, out[0]["updated_date"].(string)) if err != nil { t.Fatalf("updated_date did not survive as a timestamp: %v", out[0]["updated_date"]) } if gap := updated.Sub(created); gap != 5*24*time.Hour { t.Errorf("time-to-hire became %s, want 120h — the two timestamps moved "+ "by different amounts", gap) } // Non-date fields are untouched. if out[0]["status"] != "hired" || out[0]["ai_score"] != 91 { t.Errorf("a non-date field was rewritten: %+v", out[0]) } // And a record whose two dates were equal still has them equal. if out[1]["created_date"] != out[1]["updated_date"] { t.Errorf("equal timestamps diverged: %v vs %v", out[1]["created_date"], out[1]["updated_date"]) } }