package store import ( "math" "testing" "time" ) func refTime() time.Time { return time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC) } func TestPresenceScoreColdBoot(t *testing.T) { // No probes at all. Product collapses to 1.0; score = 0. Fail-closed. got := PresenceScore(refTime(), nil) if math.Abs(got-0.0) > 1e-9 { t.Fatalf("cold boot: want 0, got %f", got) } if b := ResolveCold(got); b != Away { t.Fatalf("cold boot: want Away, got %s", b) } } func TestPresenceScoreAllSignalsDroppedOnNoFact(t *testing.T) { // Every probe present but LastTs nil → drops out → score 0. probes := []SignalProbe{ {Key: "desk_active", LastTs: nil}, {Key: "page_heartbeat", LastTs: nil}, {Key: "wg_handshake", LastTs: nil}, } if got := PresenceScore(refTime(), probes); got != 0.0 { t.Fatalf("all-nil probes: want 0, got %f", got) } } func TestPresenceScoreAtDeskTyping(t *testing.T) { // Fresh (<1s) desk_active + heartbeat → very high. Spec table: ~0.90. now := refTime() justNow := now.Add(-1 * time.Second) probes := []SignalProbe{ {Key: "desk_active", LastTs: &justNow}, {Key: "page_heartbeat", LastTs: &justNow}, } got := PresenceScore(now, probes) if got < 0.85 { t.Fatalf("at desk typing: want >= 0.85, got %f", got) } if b := Resolve(got, Away); b != Present { t.Fatalf("away→present at P=%f: want Present, got %s", got, b) } } func TestPresenceScoreFreshWGAloneCannotEnter(t *testing.T) { // A lone fresh wg (weight 0.40) is below ENTER (0.55). Cannot declare present. now := refTime() justNow := now.Add(-1 * time.Second) probes := []SignalProbe{{Key: "wg_handshake", LastTs: &justNow}} got := PresenceScore(now, probes) if got > PresenceEnter { t.Fatalf("fresh wg alone: want <= ENTER, got %f", got) } if b := Resolve(got, Away); b != Away { t.Fatalf("wg alone enters present: want Away, got %s (P=%f)", b, got) } } func TestPresenceDeskOnlyDecaysToAway(t *testing.T) { // Desk-only, zero input for ~9min → <0.30 → Away. Spec table row 3. now := refTime() deskThen := now.Add(-9 * time.Minute) probes := []SignalProbe{{Key: "desk_active", LastTs: &deskThen}} got := PresenceScore(now, probes) if got >= PresenceExit { t.Fatalf("9min-stale desk-only: want < EXIT(%f), got %f", PresenceExit, got) } if b := Resolve(got, Present); b != Away { t.Fatalf("present→away at P=%f (9min stale desk): want Away, got %s", got, b) } } func TestPresenceHysteresisHoldsWhileDecaying(t *testing.T) { // Already present. WG alone decayed—but a fresh-ish wg_holding(0.40) while // present must HOLD present even though it could not ENTER present. Band. now := refTime() wgThen := now.Add(-1 * time.Minute) // dt=1min; p ≈ 0.40 * exp(-1/20) ≈ 0.381 probes := []SignalProbe{{Key: "wg_handshake", LastTs: &wgThen}} got := PresenceScore(now, probes) // Sanity: above EXIT, below ENTER — sitting in the hold band. if got <= PresenceExit || got >= PresenceEnter { t.Fatalf("decaying wg in hold band: want (%f, %f), got %f", PresenceExit, PresenceEnter, got) } if b := Resolve(got, Present); b != Present { t.Fatalf("hold during decay: want Present, got %s (P=%f, last=Present)", b, got) } // But the exact same reading from Away must NOT enter. if b := Resolve(got, Away); b != Away { t.Fatalf("from Away the same P must not enter: want Away, got %s (P=%f)", b, got) } } func TestPresenceCouchPhoneOpen(t *testing.T) { // Couch, phone page open, no desk → ~0.60, just present (≥0.55). now := refTime() justNow := now.Add(-1 * time.Second) probes := []SignalProbe{{Key: "page_heartbeat", LastTs: &justNow}} got := PresenceScore(now, probes) // weight 0.60, decay≈0 → near 0.60. Should ENTER from away. if got < PresenceEnter { t.Fatalf("couch+phone: want >= ENTER(%f), got %f", PresenceEnter, got) } if b := Resolve(got, Away); b != Present { t.Fatalf("couch+phone enters: want Present, got %s (P=%f)", b, got) } } func TestPresenceUnequalWeightsSumDiminishingReturns(t *testing.T) { // Noisy-OR: stacking adds diminishing returns; never exceeds 1.0. // At t=0 all three fresh: P = 1 - (0.1)(0.4)(0.6) = 1 - 0.024 = 0.976. now := refTime() t0 := now probes := []SignalProbe{ {Key: "desk_active", LastTs: &t0}, {Key: "page_heartbeat", LastTs: &t0}, {Key: "wg_handshake", LastTs: &t0}, } got := PresenceScore(now, probes) if got >= 1.0 { t.Fatalf("stacked fresh: want < 1.0, got %f", got) } if math.Abs(got-0.976) > 1e-3 { t.Fatalf("stacked fresh: want ≈ 0.976, got %f", got) } } func TestNegativeClockSkewClampsToFresh(t *testing.T) { // last is in the future relative to now (clock skew). Should clamp dt to 0 // rather than gifting a p > weight via exp(-negative/τ) > 1. now := refTime() future := now.Add(5 * time.Minute) probes := []SignalProbe{{Key: "desk_active", LastTs: &future}} got := PresenceScore(now, probes) if got > 0.90+1e-9 { t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got) } }