Files
felhom.eu/hub/internal/intent/presence_test.go
T
admin d604e624a3 hub: box presence from the wait channel + delete a switched-off box at once (R-30, D2)
Slice 1: intent.Hub records one start/end per GET /api/v1/wait request and answers
Presence(customer, now): connected (hold open, or one started < 333 s = 243 s cadence + 90 s
grace ago), not connected since T, or unknown (hub up < 333 s; in memory only). The host page
shows "Box connection". One DEBUG line per presence change.

Slice 2 (operator ruling D2, 2026-10-08, 09 §3 decision 186): a host that is online by its report
clock but whose box has had no wait-channel connection for >= 360 s may be deleted at once after
the tick "I checked: the box is off". Presence is re-read at POST time; the tick alone, unknown
presence, a connected box or a shorter gap keep today's 409. The delete logs the operator channel
and saves one host_deleted_box_off event. RESET and the customer-delete cascade are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-08 15:17:05 +02:00

112 lines
4.1 KiB
Go

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