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
This commit is contained in:
@@ -48,6 +48,11 @@ func (h *Handler) handleWait(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// R-30: box presence — recorded ONCE per request (intent.Wait below runs once per heartbeat
|
||||
// window), and ended on every exit path.
|
||||
h.intentHub.MarkWaitStart(customerID)
|
||||
defer h.intentHub.MarkWaitEnd(customerID)
|
||||
|
||||
var lastSeen uint64
|
||||
if s := r.URL.Query().Get("gen"); s != "" {
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err == nil {
|
||||
|
||||
@@ -181,3 +181,30 @@ func TestWait_CustomerIsolation(t *testing.T) {
|
||||
t.Fatalf("A should time out on its own gen 0 (isolation); body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// R-30 slice 1: each wait REQUEST marks one start and one end on the intent hub's presence — open
|
||||
// while held, ended on return. Without the marks the box reads "never seen"; without the end mark it
|
||||
// would read connected forever.
|
||||
func TestWait_MarksBoxPresence(t *testing.T) {
|
||||
h, st, hub := newWaitHandler(t)
|
||||
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c", RetrievalPassword: "pw", APIKey: "CKEY", ConfigJSON: "{}"}); err != nil {
|
||||
t.Fatalf("SaveCustomerConfig: %v", err)
|
||||
}
|
||||
t0 := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
clock := t0.Add(-time.Hour)
|
||||
hub.SetClock(func() time.Time { return clock }) // born an hour before t0
|
||||
clock = t0
|
||||
shrinkWaitTiming(t, 2*time.Second, 500*time.Millisecond)
|
||||
|
||||
ch := doWaitAsync(h, "CKEY", "0")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if p := hub.Presence("c", t0.Add(time.Hour)); p.State != intent.PresenceConnected {
|
||||
t.Fatalf("while the wait is held: %+v, want connected (open hold)", p)
|
||||
}
|
||||
hub.Bump("c")
|
||||
recvRR(t, ch, time.Second)
|
||||
p := hub.Presence("c", t0.Add(intent.PresenceConnectedWindow+time.Second))
|
||||
if p.State != intent.PresenceNotConnected || p.NeverSeen || !p.Since.Equal(t0) {
|
||||
t.Fatalf("after the wait returned: %+v, want not_connected since t0 (start and end both marked)", p)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user