Files
memby/server/internal/credits/behaviour_test.go
T

161 lines
5.3 KiB
Go
Raw Normal View History

2026-08-15 09:23:26 +12:00
package credits
import "testing"
// The behavioural detector, which is the half of this subsystem that costs nothing at all.
// The tests that matter are the ones about refusing to answer: this can write a marker with
// no media ever being opened, so the bar for it doing so has to be real.
const episodeRuntime = 47*60*1000 + 12*1000 // 47:12, the brief's example
func stop(user string, minutes, seconds int, next bool) StopEvent {
return StopEvent{
UserKey: user,
PositionMs: int64(minutes*60+seconds) * 1000,
RuntimeMs: episodeRuntime,
NextEpisode: next,
}
}
// The worked example from the brief: three viewers leaving within seconds of each other.
func TestClusteredStopsFindTheCredits(t *testing.T) {
evidence := AnalyseStops([]StopEvent{
stop("paul", 44, 3, true),
stop("david", 44, 9, false),
stop("matt", 44, 5, true),
}, episodeRuntime)
if !evidence.Found {
t.Fatal("three agreeing viewers produced no evidence")
}
if evidence.UserCount != 3 {
t.Fatalf("UserCount = %d, want 3", evidence.UserCount)
}
// The brief's own answer is "credits ≈ 44:05" — the middle of the cluster.
if evidence.StartMs != stop("", 44, 5, false).PositionMs {
t.Fatalf("StartMs = %d, want the 44:05 median", evidence.StartMs)
}
if !evidence.StandaloneMarker() {
t.Fatal("three viewers agreeing to within six seconds should be enough on its own")
}
}
func TestStopsBeforeTheTailAreNotEvidence(t *testing.T) {
// Three people abandoning an episode twenty minutes in agree about the episode, not
// about where its credits are.
evidence := AnalyseStops([]StopEvent{
stop("paul", 20, 0, false),
stop("david", 20, 4, false),
stop("matt", 20, 2, false),
}, episodeRuntime)
if evidence.Found {
t.Fatalf("a cluster below the tail floor was treated as credits: %+v", evidence)
}
}
func TestWatchingToTheEndSaysNothing(t *testing.T) {
// Sitting through the credits stops at the end of the file, wherever they began. These
// are the majority of sessions and they carry no positional information.
evidence := AnalyseStops([]StopEvent{
stop("paul", 47, 12, false),
stop("david", 47, 10, false),
stop("matt", 47, 11, false),
}, episodeRuntime)
if evidence.Found {
t.Fatalf("stops at the very end were read as a transition: %+v", evidence)
}
}
func TestOnePersonIsNotAHousehold(t *testing.T) {
// The same viewer stopping in the same place repeatedly is one observation, not four.
evidence := AnalyseStops([]StopEvent{
stop("paul", 44, 3, false),
stop("paul", 44, 5, false),
stop("paul", 44, 4, false),
stop("paul", 44, 6, false),
}, episodeRuntime)
if evidence.Found {
t.Fatalf("one viewer produced evidence: %+v", evidence)
}
}
// Two viewers is enough only when both pressed next, which is unambiguous about why they
// left. Two plain stops might be one couple, or one person on two devices.
func TestTwoViewersNeedNextEpisodeTransitions(t *testing.T) {
plain := AnalyseStops([]StopEvent{
stop("paul", 44, 3, false),
stop("david", 44, 7, false),
}, episodeRuntime)
if !plain.Usable() {
t.Fatal("two agreeing viewers should at least be good enough to narrow a scan")
}
if plain.StandaloneMarker() {
t.Fatal("two plain stops must not be enough to write a marker on their own")
}
advanced := AnalyseStops([]StopEvent{
stop("paul", 44, 3, true),
stop("david", 44, 7, true),
}, episodeRuntime)
if !advanced.StandaloneMarker() {
t.Fatal("two viewers who both rolled into the next episode should be enough")
}
}
// The tail contains two populations: people who left at the credits, and people who left
// part way through them. The tightest agreement is the transition.
func TestScatteredStragglersDoNotMoveTheAnswer(t *testing.T) {
evidence := AnalyseStops([]StopEvent{
stop("paul", 44, 3, false),
stop("david", 44, 6, false),
stop("matt", 44, 4, false),
stop("jane", 46, 30, false), // gave up half way through the credits
}, episodeRuntime)
if !evidence.Found {
t.Fatal("expected the tight cluster to win")
}
if evidence.StartMs > stop("", 44, 10, false).PositionMs {
t.Fatalf("StartMs = %d; a straggler dragged the answer later", evidence.StartMs)
}
}
func TestNoStopsIsNoEvidence(t *testing.T) {
if evidence := AnalyseStops(nil, episodeRuntime); evidence.Found {
t.Fatal("no stops produced evidence")
}
if evidence := AnalyseStops([]StopEvent{stop("paul", 44, 0, false)}, 0); evidence.Found {
t.Fatal("a zero runtime produced evidence")
}
}
func TestTighterAgreementScoresHigher(t *testing.T) {
tight := AnalyseStops([]StopEvent{
stop("paul", 44, 3, false),
stop("david", 44, 4, false),
stop("matt", 44, 5, false),
}, episodeRuntime)
loose := AnalyseStops([]StopEvent{
stop("paul", 44, 0, false),
stop("david", 44, 14, false),
stop("matt", 44, 28, false),
}, episodeRuntime)
if BehaviourConfidence(tight) <= BehaviourConfidence(loose) {
t.Fatalf("tight agreement (%.2f) did not beat loose agreement (%.2f)",
BehaviourConfidence(tight), BehaviourConfidence(loose))
}
}
func TestBehaviourConfidenceNeverReachesCertainty(t *testing.T) {
stops := []StopEvent{}
for index := 0; index < 20; index++ {
stops = append(stops, stop("viewer"+string(rune('a'+index)), 44, 4, true))
}
evidence := AnalyseStops(stops, episodeRuntime)
if score := BehaviourConfidence(evidence); score > behaviourCeiling {
t.Fatalf("confidence %.2f exceeded the behavioural ceiling %.2f",
score, behaviourCeiling)
}
}