Files
Maven/internal/store/presence_test.go
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00

146 lines
4.8 KiB
Go

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)
}
}