package intent import ( "strings" "testing" "time" ) // fakeClock is a settable clock for the presence seam. type fakeClock struct{ t time.Time } func (c *fakeClock) now() time.Time { return c.t } // A hub that has been up long enough to judge (born well before t0). func judgingHub(t0 time.Time) (*Hub, *fakeClock) { c := &fakeClock{t: t0.Add(-time.Hour)} h := New() h.SetClock(c.now) c.t = t0 return h, c } // R-30 §4 test 1: a wait starts at t0 and ends at t0+240 s, and none follows → at t0+300 s the box is // connected (a wait started < 333 s ago), at t0+334 s it is "not connected since t0+240 s". func TestPresence_WindowsAfterOneHold(t *testing.T) { t0 := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) h, c := judgingHub(t0) h.MarkWaitStart("c1") if p := h.Presence("c1", t0.Add(100*time.Second)); p.State != PresenceConnected { t.Fatalf("during the hold: %+v, want connected", p) } c.t = t0.Add(240 * time.Second) h.MarkWaitEnd("c1") if p := h.Presence("c1", t0.Add(300*time.Second)); p.State != PresenceConnected { t.Fatalf("t0+300 s: %+v, want connected (started < 333 s ago)", p) } p := h.Presence("c1", t0.Add(334*time.Second)) if p.State != PresenceNotConnected || !p.Since.Equal(t0.Add(240*time.Second)) || p.NeverSeen { t.Fatalf("t0+334 s: %+v, want not_connected since t0+240 s", p) } if got := p.NotConnectedFor(t0.Add(600 * time.Second)); got != 360*time.Second { t.Fatalf("NotConnectedFor at t0+600 s = %s, want 6m0s", got) } } // R-30 §4 test 2: a new start at t0+242 s keeps the box connected well past t0+334 s. func TestPresence_NextHoldKeepsConnected(t *testing.T) { t0 := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) h, c := judgingHub(t0) h.MarkWaitStart("c1") c.t = t0.Add(240 * time.Second) h.MarkWaitEnd("c1") c.t = t0.Add(242 * time.Second) h.MarkWaitStart("c1") for _, at := range []time.Duration{334 * time.Second, 500 * time.Second, 574 * time.Second} { if p := h.Presence("c1", t0.Add(at)); p.State != PresenceConnected { t.Fatalf("t0+%s: %+v, want connected (a hold is open)", at, p) } } // Two overlapping requests: one ending does not close the other. h.MarkWaitStart("c1") h.MarkWaitEnd("c1") if p := h.Presence("c1", t0.Add(time.Hour)); p.State != PresenceConnected { t.Fatalf("one of two holds still open: %+v, want connected", p) } } // R-30 §4 test 3: a new Hub (a hub restart) answers unknown — never not_connected — until it has // been up for the connected window; after that, a customer never seen reads not_connected since the // hub's start, flagged NeverSeen. func TestPresence_HubRestartIsUnknown(t *testing.T) { t0 := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) c := &fakeClock{t: t0} h := New() h.SetClock(c.now) for _, at := range []time.Duration{0, 100 * time.Second, PresenceConnectedWindow - time.Second} { if p := h.Presence("c1", t0.Add(at)); p.State != PresenceUnknown { t.Fatalf("hub up %s: %+v, want unknown", at, p) } } p := h.Presence("c1", t0.Add(PresenceConnectedWindow)) if p.State != PresenceNotConnected || !p.NeverSeen || !p.Since.Equal(t0) { t.Fatalf("hub up 333 s, box never seen: %+v, want not_connected since hub start", p) } if p := h.Presence("", t0.Add(time.Hour)); p.State != PresenceUnknown { t.Fatalf("no customer: %+v, want unknown", p) } } // The window is the measured cadence plus the grace (pins the constants the delete guard leans on). func TestPresence_WindowConstants(t *testing.T) { if PresenceConnectedWindow != 333*time.Second { t.Fatalf("PresenceConnectedWindow = %s, want 5m33s (243 s + 90 s)", PresenceConnectedWindow) } } // One DEBUG line per change, none on a repeat read. func TestPresence_DebugLineOnChange(t *testing.T) { t0 := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) h, c := judgingHub(t0) var lines []string h.SetLogger(func(f string, a ...any) { lines = append(lines, f) }) h.MarkWaitStart("c1") c.t = t0.Add(240 * time.Second) h.MarkWaitEnd("c1") h.Presence("c1", t0.Add(400*time.Second)) h.Presence("c1", t0.Add(500*time.Second)) if len(lines) != 1 || !strings.Contains(lines[0], "[DEBUG] box presence") { t.Fatalf("debug lines = %q, want exactly one change line", lines) } }