Files
silo-server/internal/playback/session_paused_grace_test.go
ee31a1f0e2 feat(playback): formalize resumable direct streams and stall observability (#464)
* feat(playback): formalize resumable direct streams and stall observability

Implements #443: strong stat-based ETag + If-Range on original-file direct
play (via http.ServeContent), stream-end outcome classification in
RollingDeadlineWriter (stalled_reap vs client_gone vs completed) with a
structured log event and Prometheus counters, the direct_stream_resume_v1
protocol-v3 capability, and a contract doc. Progressive remux is explicitly
excluded from the resume contract.

Code written by OpenAI Codex CLI (gpt-5.6-sol) from a Claude-authored spec;
reviewed and verified by Claude.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): harden direct stream resume contract

* test(playback): cover resume platform contracts

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:22:12 -04:00

107 lines
3.5 KiB
Go

package playback
import (
"fmt"
"testing"
"time"
)
// TestPausedSessionSurvivesIntentionalPause covers issue #243 symptom (a):
// a paused session whose client stops reporting progress (backgrounded tab,
// slept device, tvOS pause) must not be reaped after a few minutes — reaping
// kills the ffmpeg transcode and there is no revival path, so pressing Play
// after a >5 minute pause freezes the client. An intentional pause must
// survive well beyond the old 2-minute grace; truly abandoned sessions are
// still reaped once the (now longer) paused grace elapses.
func TestPausedSessionSurvivesIntentionalPause(t *testing.T) {
m := NewSessionManager(0, 0)
session, err := m.StartSession(1, "profile-1", 100, PlayTranscode, false)
if err != nil {
t.Fatalf("StartSession: %v", err)
}
if err := m.UpdateProgress(session.ID, 42, true); err != nil {
t.Fatalf("UpdateProgress(paused): %v", err)
}
setLastActivity := func(age time.Duration) {
m.mu.Lock()
s := m.sessions[session.ID]
s.LastActivityAt = time.Now().Add(-age)
s.UpdatedAt = s.LastActivityAt
m.mu.Unlock()
}
// Paused for 10 minutes: must survive.
setLastActivity(10 * time.Minute)
m.CleanStale()
if _, err := m.GetSession(session.ID); err != nil {
t.Fatalf("session reaped after 10-minute pause; paused grace must allow intentional pauses (err: %v)", err)
}
// Abandoned well past the paused grace: must still be reaped.
setLastActivity(DefaultPausedSessionGrace + time.Minute)
m.CleanStale()
if _, err := m.GetSession(session.ID); err == nil {
t.Fatal("session survived past the paused grace; abandoned sessions must still be reaped")
}
}
func TestSequentialRangedTransportsSurviveIdleAndPausedGrace(t *testing.T) {
m := NewSessionManager(0, 0)
session, err := m.StartSession(1, "profile-1", 100, PlayDirect, false)
if err != nil {
t.Fatalf("StartSession: %v", err)
}
setLastActivity := func(age time.Duration) {
t.Helper()
m.mu.Lock()
s := m.sessions[session.ID]
s.LastActivityAt = time.Now().Add(-age)
s.UpdatedAt = s.LastActivityAt
m.mu.Unlock()
}
assertPresent := func(stage string) {
t.Helper()
if _, err := m.GetSession(session.ID); err != nil {
t.Fatalf("%s: session was cleaned: %v", stage, err)
}
}
const activeGrace = 2 * time.Minute
for cycle := 1; cycle <= 3; cycle++ {
setLastActivity(activeGrace / 2)
m.CleanInactive(activeGrace, DefaultPausedSessionGrace)
assertPresent(fmt.Sprintf("idle gap before transport %d", cycle))
if err := m.BeginTransport(session.ID); err != nil {
t.Fatalf("BeginTransport(%d): %v", cycle, err)
}
setLastActivity(activeGrace + time.Minute)
m.CleanInactive(activeGrace, DefaultPausedSessionGrace)
assertPresent(fmt.Sprintf("active transport %d", cycle))
if err := m.EndTransport(session.ID); err != nil {
t.Fatalf("EndTransport(%d): %v", cycle, err)
}
}
if err := m.UpdateProgress(session.ID, 42, true); err != nil {
t.Fatalf("UpdateProgress(paused): %v", err)
}
setLastActivity(DefaultPausedSessionGrace - time.Minute)
m.CleanInactive(activeGrace, DefaultPausedSessionGrace)
assertPresent("late paused idle gap")
if err := m.BeginTransport(session.ID); err != nil {
t.Fatalf("BeginTransport(late range): %v", err)
}
setLastActivity(DefaultPausedSessionGrace + time.Minute)
m.CleanInactive(activeGrace, DefaultPausedSessionGrace)
assertPresent("late ranged transport active")
if err := m.EndTransport(session.ID); err != nil {
t.Fatalf("EndTransport(late range): %v", err)
}
assertPresent("completed ranged transport sequence")
}