d52f60c54e
- 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>
146 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|