d604e624a3
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
112 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|