mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
fix(live): keep red-flagged sessions active
This commit is contained in:
@@ -153,6 +153,29 @@ describe('LiveTimingPage archive mode', () => {
|
|||||||
expect(screen.queryByRole('button', { name: /view last session/i })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /view last session/i })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders the live session for a red-flag paused snapshot instead of the inactive empty state', async () => {
|
||||||
|
renderPage({
|
||||||
|
is_live: true,
|
||||||
|
data: {
|
||||||
|
...archivedSnapshot,
|
||||||
|
SessionStatus: 'Inactive',
|
||||||
|
TrackStatus: '2',
|
||||||
|
Clock: '00:03:27',
|
||||||
|
Session: {
|
||||||
|
MeetingName: 'Belgian Grand Prix',
|
||||||
|
CircuitName: 'Spa-Francorchamps',
|
||||||
|
SessionType: 'Practice',
|
||||||
|
SessionName: 'Practice 2',
|
||||||
|
Path: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await screen.findByText('Timing Tower')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('live-empty')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('live-archive-strip')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('temporarily omits the track map while live GPS is unavailable', async () => {
|
it('temporarily omits the track map while live GPS is unavailable', async () => {
|
||||||
renderPage({
|
renderPage({
|
||||||
is_live: true,
|
is_live: true,
|
||||||
|
|||||||
@@ -307,6 +307,28 @@ func TestSessionStatusIsActive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionStatusIsTerminal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"Finished", true},
|
||||||
|
{"Finalised", true},
|
||||||
|
{"Finalized", true},
|
||||||
|
{"Ends", true},
|
||||||
|
{"Aborted", true},
|
||||||
|
{"Started", false},
|
||||||
|
{"Resumed", false},
|
||||||
|
{"Inactive", false},
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := live.SessionStatusIsTerminal(tt.status); got != tt.want {
|
||||||
|
t.Errorf("SessionStatusIsTerminal(%q) = %v, want %v", tt.status, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessTopicRaceControlMessages(t *testing.T) {
|
func TestProcessTopicRaceControlMessages(t *testing.T) {
|
||||||
state := live.NewState()
|
state := live.NewState()
|
||||||
data := json.RawMessage(`{
|
data := json.RawMessage(`{
|
||||||
|
|||||||
@@ -186,6 +186,19 @@ func SessionStatusIsActive(status string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionStatusIsTerminal reports whether a raw F1 live timing SessionStatus
|
||||||
|
// value represents a session that has ended and will not resume. A temporarily
|
||||||
|
// inactive session (e.g. a red-flag pause reported as "Inactive") is neither
|
||||||
|
// active nor terminal.
|
||||||
|
func SessionStatusIsTerminal(status string) bool {
|
||||||
|
switch normalizeSessionStatus(status) {
|
||||||
|
case "finished", "finalised", "finalized", "ends", "aborted":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeSessionStatus(status string) string {
|
func normalizeSessionStatus(status string) string {
|
||||||
out := make([]rune, 0, len(status))
|
out := make([]rune, 0, len(status))
|
||||||
for _, r := range status {
|
for _, r := range status {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (h *SSEHub) applySnapshot(data live.LiveStreamData, now time.Time) liveStat
|
|||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
if live.SessionStatusIsActive(data.SessionStatus) {
|
if snapshotIsLive(data) {
|
||||||
h.isLive = true
|
h.isLive = true
|
||||||
h.activeSnapshot = &data
|
h.activeSnapshot = &data
|
||||||
if data.PositionUpdated && len(data.Positions) > 0 {
|
if data.PositionUpdated && len(data.Positions) > 0 {
|
||||||
@@ -208,6 +208,40 @@ func (h *SSEHub) stateLocked() liveStatePayload {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// snapshotIsLive reports whether the current live-timing snapshot represents an
|
||||||
|
// ongoing session that should be surfaced as live.
|
||||||
|
//
|
||||||
|
// An actively running session (Started/Resumed) is always live. A non-terminal
|
||||||
|
// but temporarily inactive session — e.g. a red-flag pause where SessionStatus
|
||||||
|
// drops to "Inactive" while the session is still in progress — is also live when
|
||||||
|
// the snapshot itself carries live evidence: a session clock with time remaining
|
||||||
|
// plus session metadata. This keeps the paused track state visible instead of
|
||||||
|
// collapsing to the inactive empty state.
|
||||||
|
//
|
||||||
|
// Terminal sessions (Finished/Finalised/Ends/Aborted) are never live. A generic
|
||||||
|
// inactive/no-session stale snapshot is not live either: the predicate keys off
|
||||||
|
// the current snapshot's clock and session, so old metadata alone is not enough.
|
||||||
|
func snapshotIsLive(data live.LiveStreamData) bool {
|
||||||
|
if live.SessionStatusIsActive(data.SessionStatus) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if live.SessionStatusIsTerminal(data.SessionStatus) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return clockHasTimeRemaining(data.Clock) && data.Session.SessionName != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// clockHasTimeRemaining reports whether an "HH:MM:SS" session clock has any time
|
||||||
|
// left. Empty or all-zero clocks (a spent or absent session) return false.
|
||||||
|
func clockHasTimeRemaining(clock string) bool {
|
||||||
|
for _, r := range clock {
|
||||||
|
if r >= '1' && r <= '9' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func hasLiveSnapshotData(data live.LiveStreamData) bool {
|
func hasLiveSnapshotData(data live.LiveStreamData) bool {
|
||||||
return len(data.Drivers) > 0 ||
|
return len(data.Drivers) > 0 ||
|
||||||
len(data.DriverInfo) > 0 ||
|
len(data.DriverInfo) > 0 ||
|
||||||
|
|||||||
@@ -51,6 +51,101 @@ func TestSSEHubArchivesTerminalSessionSnapshot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSnapshotIsLive(t *testing.T) {
|
||||||
|
redFlag := live.LiveStreamData{
|
||||||
|
SessionStatus: "Inactive",
|
||||||
|
TrackStatus: "2",
|
||||||
|
Clock: "00:03:27",
|
||||||
|
Session: live.LiveSessionMeta{MeetingName: "Belgian Grand Prix", SessionName: "Practice 2", SessionType: "Practice"},
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data live.LiveStreamData
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"started", live.LiveStreamData{SessionStatus: "Started"}, true},
|
||||||
|
{"resumed", live.LiveStreamData{SessionStatus: "Resumed"}, true},
|
||||||
|
{"red flag inactive with clock and session", redFlag, true},
|
||||||
|
{"terminal finished with clock", func() live.LiveStreamData {
|
||||||
|
d := redFlag
|
||||||
|
d.SessionStatus = "Finished"
|
||||||
|
return d
|
||||||
|
}(), false},
|
||||||
|
{"inactive spent clock", func() live.LiveStreamData {
|
||||||
|
d := redFlag
|
||||||
|
d.Clock = "00:00:00"
|
||||||
|
return d
|
||||||
|
}(), false},
|
||||||
|
{"inactive no clock", func() live.LiveStreamData {
|
||||||
|
d := redFlag
|
||||||
|
d.Clock = ""
|
||||||
|
return d
|
||||||
|
}(), false},
|
||||||
|
{"inactive no session metadata", func() live.LiveStreamData {
|
||||||
|
d := redFlag
|
||||||
|
d.Session = live.LiveSessionMeta{}
|
||||||
|
return d
|
||||||
|
}(), false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := snapshotIsLive(tt.data); got != tt.want {
|
||||||
|
t.Errorf("%s: snapshotIsLive = %v, want %v", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSEHubKeepsRedFlagSessionLive(t *testing.T) {
|
||||||
|
hub := newSSEHub()
|
||||||
|
now := time.Date(2026, 7, 26, 14, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
redFlag := live.LiveStreamData{
|
||||||
|
SessionStatus: "Inactive",
|
||||||
|
TrackStatus: "2",
|
||||||
|
Clock: "00:03:27",
|
||||||
|
Session: live.LiveSessionMeta{MeetingName: "Belgian Grand Prix", SessionName: "Practice 2", SessionType: "Practice"},
|
||||||
|
SnapshotUpdated: true,
|
||||||
|
}
|
||||||
|
state := hub.applySnapshot(redFlag, now)
|
||||||
|
|
||||||
|
if !state.IsLive {
|
||||||
|
t.Fatal("red-flag paused session should report is_live=true")
|
||||||
|
}
|
||||||
|
if state.Data == nil {
|
||||||
|
t.Fatal("red-flag paused session must still send the active snapshot")
|
||||||
|
}
|
||||||
|
if state.Data.TrackStatus != "2" {
|
||||||
|
t.Fatalf("track status = %q, want the paused state \"2\"", state.Data.TrackStatus)
|
||||||
|
}
|
||||||
|
if state.LastSnapshot != nil {
|
||||||
|
t.Fatalf("red-flag session should not be archived: %+v", state.LastSnapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSEHubStaleInactiveSnapshotNotLive(t *testing.T) {
|
||||||
|
hub := newSSEHub()
|
||||||
|
now := time.Date(2026, 7, 26, 14, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// Inactive snapshot with session metadata but no remaining clock: this is a
|
||||||
|
// stale/no-live-evidence snapshot and must not be surfaced as live.
|
||||||
|
stale := live.LiveStreamData{
|
||||||
|
SessionStatus: "Inactive",
|
||||||
|
Clock: "00:00:00",
|
||||||
|
Session: live.LiveSessionMeta{MeetingName: "Belgian Grand Prix", SessionName: "Practice 2"},
|
||||||
|
SnapshotUpdated: true,
|
||||||
|
}
|
||||||
|
state := hub.applySnapshot(stale, now)
|
||||||
|
|
||||||
|
if state.IsLive {
|
||||||
|
t.Fatal("stale inactive snapshot should report is_live=false")
|
||||||
|
}
|
||||||
|
if state.Data != nil {
|
||||||
|
t.Fatalf("stale inactive snapshot leaked into active data: %+v", state.Data)
|
||||||
|
}
|
||||||
|
if state.LastSnapshot == nil {
|
||||||
|
t.Fatal("stale inactive snapshot with metadata should be archived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleLiveStateKeepsArchiveOutOfActiveData(t *testing.T) {
|
func TestHandleLiveStateKeepsArchiveOutOfActiveData(t *testing.T) {
|
||||||
hub := newSSEHub()
|
hub := newSSEHub()
|
||||||
now := time.Date(2026, 7, 4, 14, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 7, 4, 14, 0, 0, 0, time.UTC)
|
||||||
|
|||||||
Reference in New Issue
Block a user