mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
feat(race-hub): trustworthy defaults and fan-facing analysis hierarchy (#75)
Make bare /race-hub resolve to a completed session and never open empty post-session analysis for a future race. - Backend: add `default_analysis_session` to the Weekend context. It never resolves to a future session (picks the richest completed session, ties toward the later one; 0 when everything is upcoming). Existing `default_session_key` and deep links are unchanged. - Frontend default resolution prefers the most recently completed weekend (`pickAnalysisFocusMeeting`) and consumes `default_analysis_session`, falling back to the switcher when only upcoming sessions exist. - New `sessionState` lib maps timing + coverage to user language (upcoming/live/preparing/partial/ready/cancelled); the session rail, active sub-bar, and WeekendSwitcher now label states instead of raw x/11 counts. - Future sessions render a purpose-built PreSessionView (expected availability + countdown) instead of empty Winner/Podium/Pole/Strategy/Compare cards. - Analysis navigation regrouped into Story / Analysis / Data & Context; every existing tab is preserved. Diagnostics (renamed from Data Status) is now a secondary action and the raw dataset strip is hidden behind an explicit toggle, so operational coverage no longer precedes fan content. - Loading/error states offer Retry and a path back to Weekend. Tests: Go query tests for future-exclusion; Vitest for default selection, future pre-session, partial state, error/retry, grouped nav, and sessionState; hermetic Playwright for bare/completed/future/return-to-Weekend; new race-hub-future visual snapshots. Seed adds a far-future session inside the Monaco meeting (kept in-meeting so Command Center focus is unaffected). Note: `default_analysis_session` is an additive field on the existing `/weekend` contract (no new endpoint), per the spec's "context contract" scope. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AmanTahiliani/box-box/internal/models"
|
||||
"github.com/AmanTahiliani/box-box/internal/store"
|
||||
@@ -376,6 +377,88 @@ func TestGetWeekendWithSessions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickDefaultAnalysisSessionSkipsFuture(t *testing.T) {
|
||||
now := mustTime(t, "2025-05-24T18:00:00Z")
|
||||
sessions := []WeekendSession{
|
||||
{ // completed qualifying, partial coverage
|
||||
Session: models.Session{SessionKey: 100, DateStart: "2025-05-24T14:00:00+00:00"},
|
||||
Datasets: map[string]DatasetInfo{"results": availableLocal(1)},
|
||||
},
|
||||
{ // future race with the richest coverage — must NOT be selected
|
||||
Session: models.Session{SessionKey: 200, DateStart: "2025-05-25T13:00:00+00:00"},
|
||||
Datasets: map[string]DatasetInfo{
|
||||
"results": availableLocal(1),
|
||||
"laps": availableLocal(1),
|
||||
"stints": availableLocal(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := pickDefaultAnalysisSession(sessions, now)
|
||||
if got != 100 {
|
||||
t.Fatalf("pickDefaultAnalysisSession() = %d, want 100 (never a future session)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickDefaultAnalysisSessionAllFuture(t *testing.T) {
|
||||
now := mustTime(t, "2025-05-20T00:00:00Z")
|
||||
sessions := []WeekendSession{
|
||||
{Session: models.Session{SessionKey: 100, DateStart: "2025-05-24T14:00:00+00:00"}},
|
||||
{Session: models.Session{SessionKey: 200, DateStart: "2025-05-25T13:00:00+00:00"}},
|
||||
}
|
||||
|
||||
if got := pickDefaultAnalysisSession(sessions, now); got != 0 {
|
||||
t.Fatalf("pickDefaultAnalysisSession() = %d, want 0 (everything upcoming)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickDefaultAnalysisSessionPrefersRichestCompleted(t *testing.T) {
|
||||
now := mustTime(t, "2025-05-26T00:00:00Z")
|
||||
sessions := []WeekendSession{
|
||||
{
|
||||
Session: models.Session{SessionKey: 100, DateStart: "2025-05-24T14:00:00+00:00"},
|
||||
Datasets: map[string]DatasetInfo{"results": availableLocal(1)},
|
||||
},
|
||||
{
|
||||
Session: models.Session{SessionKey: 200, DateStart: "2025-05-25T13:00:00+00:00"},
|
||||
Datasets: map[string]DatasetInfo{
|
||||
"results": availableLocal(1),
|
||||
"laps": availableLocal(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if got := pickDefaultAnalysisSession(sessions, now); got != 200 {
|
||||
t.Fatalf("pickDefaultAnalysisSession() = %d, want 200 (richest completed)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWeekendSetsDefaultAnalysisSession(t *testing.T) {
|
||||
prev := weekendNow
|
||||
weekendNow = func() time.Time { return mustTime(t, "2025-05-26T00:00:00Z") }
|
||||
t.Cleanup(func() { weekendNow = prev })
|
||||
|
||||
svc := openTestService(t)
|
||||
seedRaceHubData(t, svc.store)
|
||||
|
||||
weekend, err := svc.GetWeekend(1229)
|
||||
if err != nil {
|
||||
t.Fatalf("GetWeekend() error = %v", err)
|
||||
}
|
||||
if weekend.DefaultAnalysisSession != 9472 {
|
||||
t.Fatalf("DefaultAnalysisSession = %d, want 9472", weekend.DefaultAnalysisSession)
|
||||
}
|
||||
}
|
||||
|
||||
func mustTime(t *testing.T, value string) time.Time {
|
||||
t.Helper()
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
t.Fatalf("parse time %q: %v", value, err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func TestGetChampionshipInputsIncludesSprintPoints(t *testing.T) {
|
||||
// Regression for #57: Race-only aggregation dropped Sprint points.
|
||||
// Setup: same meeting 1229 has Race (9472) 25pts + Sprint (9473) 8pts => total 33.
|
||||
|
||||
Reference in New Issue
Block a user