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>
2026-07-12 18:29:34 -04:00
|
|
|
import { describe, it, expect } from 'vitest'
|
2026-07-12 18:49:10 -04:00
|
|
|
import {
|
|
|
|
|
resolveSessionState,
|
|
|
|
|
sessionState,
|
|
|
|
|
sessionStateLabel,
|
|
|
|
|
} from '../lib/sessionState'
|
|
|
|
|
import type {
|
|
|
|
|
ContextAvailability,
|
|
|
|
|
ContextSession,
|
|
|
|
|
DatasetInfo,
|
|
|
|
|
Session,
|
|
|
|
|
WeekendContext,
|
|
|
|
|
WeekendSession,
|
|
|
|
|
} from '../types'
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
|
|
|
|
|
const NOW = new Date('2025-06-01T00:00:00Z')
|
|
|
|
|
|
|
|
|
|
function mk(
|
|
|
|
|
overrides: Partial<Session>,
|
|
|
|
|
source: WeekendSession['source'],
|
|
|
|
|
datasets: Record<string, DatasetInfo> = {},
|
|
|
|
|
): WeekendSession {
|
|
|
|
|
return {
|
|
|
|
|
session: {
|
|
|
|
|
session_key: 1,
|
|
|
|
|
session_name: 'Race',
|
|
|
|
|
session_type: 'Race',
|
|
|
|
|
meeting_key: 1,
|
|
|
|
|
date_start: '2025-05-25T13:00:00+00:00',
|
|
|
|
|
date_end: '2025-05-25T15:00:00+00:00',
|
|
|
|
|
gmt_offset: '00:00:00',
|
|
|
|
|
...overrides,
|
|
|
|
|
},
|
|
|
|
|
source,
|
|
|
|
|
datasets,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FULL: Record<string, DatasetInfo> = Object.fromEntries(
|
|
|
|
|
[
|
|
|
|
|
'meeting',
|
|
|
|
|
'session',
|
|
|
|
|
'drivers',
|
|
|
|
|
'results',
|
|
|
|
|
'starting_grid',
|
|
|
|
|
'stints',
|
|
|
|
|
'pit_stops',
|
|
|
|
|
'positions',
|
|
|
|
|
'race_control',
|
|
|
|
|
'weather',
|
|
|
|
|
'laps',
|
|
|
|
|
].map((k) => [k, { status: 'available', source: 'local', count: 1 }]),
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-12 18:49:10 -04:00
|
|
|
function availability(overrides: Partial<ContextAvailability> = {}): ContextAvailability {
|
|
|
|
|
return {
|
|
|
|
|
schedule: 'available',
|
|
|
|
|
live_transport: 'unknown',
|
|
|
|
|
live_session: 'inactive',
|
|
|
|
|
archive: 'unavailable',
|
|
|
|
|
local_analysis: 'complete',
|
|
|
|
|
freshness: 'fresh',
|
|
|
|
|
limitations: [],
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function contextSession(
|
|
|
|
|
session: Session,
|
|
|
|
|
avail: Partial<ContextAvailability> = {},
|
|
|
|
|
): ContextSession {
|
|
|
|
|
return { session, availability: availability(avail) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function context(overrides: Partial<WeekendContext> = {}): WeekendContext {
|
|
|
|
|
return {
|
|
|
|
|
temporal_state: 'post_weekend',
|
|
|
|
|
championship_round: 1,
|
|
|
|
|
total_championship_rounds: 1,
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
describe('sessionState', () => {
|
|
|
|
|
it('marks a future session as upcoming', () => {
|
|
|
|
|
const s = mk({ date_start: '2099-05-25T13:00:00+00:00', date_end: '2099-05-25T15:00:00+00:00' }, 'none')
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('upcoming')
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-12 18:49:10 -04:00
|
|
|
it('does not mark Live from schedule alone', () => {
|
|
|
|
|
const start = new Date(NOW.getTime() - 60_000).toISOString()
|
|
|
|
|
const end = new Date(NOW.getTime() + 60_000).toISOString()
|
|
|
|
|
const s = mk({ date_start: start, date_end: end }, 'none')
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('preparing')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('marks Live only when Weekend Context active identity matches', () => {
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
const start = new Date(NOW.getTime() - 60_000).toISOString()
|
|
|
|
|
const end = new Date(NOW.getTime() + 60_000).toISOString()
|
2026-07-12 18:49:10 -04:00
|
|
|
const s = mk({ session_key: 42, date_start: start, date_end: end }, 'none')
|
|
|
|
|
const ctx = context({
|
|
|
|
|
temporal_state: 'session_live',
|
|
|
|
|
active_session: contextSession(s.session, { live_session: 'active' }),
|
|
|
|
|
})
|
|
|
|
|
expect(resolveSessionState({ weekendSession: s, context: ctx, now: NOW })).toBe('live')
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('marks a finished session with full local data as ready', () => {
|
|
|
|
|
const s = mk({}, 'local', FULL)
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('ready')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('marks a finished session with no data as preparing', () => {
|
|
|
|
|
const s = mk({}, 'none', {})
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('preparing')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('marks a finished session with partial data as partial', () => {
|
|
|
|
|
const s = mk({}, 'partial', { drivers: { status: 'available', source: 'local', count: 20 } })
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('partial')
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-12 18:49:10 -04:00
|
|
|
it('marks unavailable from context availability', () => {
|
|
|
|
|
const s = mk({ session_key: 7 }, 'none')
|
|
|
|
|
const ctx = context({
|
|
|
|
|
previous_completed_session: contextSession(s.session, {
|
|
|
|
|
local_analysis: 'unavailable',
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
expect(resolveSessionState({ weekendSession: s, context: ctx, now: NOW })).toBe(
|
|
|
|
|
'unavailable',
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
it('marks a cancelled session as cancelled', () => {
|
|
|
|
|
const s = mk({}, 'cancelled')
|
|
|
|
|
expect(sessionState(s, NOW)).toBe('cancelled')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses user language labels rather than coverage counts', () => {
|
|
|
|
|
expect(sessionStateLabel('ready')).toBe('Ready')
|
|
|
|
|
expect(sessionStateLabel('upcoming')).toBe('Upcoming')
|
|
|
|
|
expect(sessionStateLabel('partial')).toBe('Partial')
|
2026-07-12 18:49:10 -04:00
|
|
|
expect(sessionStateLabel('unavailable')).toBe('Unavailable')
|
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>
2026-07-12 18:29:34 -04:00
|
|
|
})
|
|
|
|
|
})
|