package main import ( "testing" "time" "github.com/novox/mesh-controller/internal/inventory" ) // windowOnABus is the merge window's clock on a real bus, and the batches its watcher heard expire. func hears(t *testing.T, heard <-chan string, within time.Duration) (string, bool) { t.Helper() select { case b := <-heard: return b, true case <-time.After(within): return "", false } } // A batch's quiet key expires when no merge came for the window, and the watcher names the batch (issue 973, // novox/hq ADR 0407 decision 1). // A merge into the batch makes the quiet key again, so the window restarts: it does not expire at the first // deadline, and the batch's most closes it all the same. // A batch cut is forgotten: its keys are taken away by a delete, which is no expiry and wakes nobody. // The deadlines a batch's keys are made with: what is left of its window and of its most, at least a second; // none for a batch whose window is closed or that is no batch. func TestTheDeadlinesOfABatchsKeys(t *testing.T) { now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) batch := func(closes, most time.Duration, closed bool) inventory.Plan { return inventory.Plan{State: inventory.PlanAssembling, Delivery: &inventory.PlanDelivery{Batch: &inventory.PlanBatch{ ClosesAt: now.Add(closes), AtMost: now.Add(most), Closed: closed}}} } for _, c := range []struct { what string b inventory.Plan quiet, most time.Duration armed bool }{ {"open", batch(90*time.Second, 10*time.Minute, false), 90 * time.Second, 10 * time.Minute, true}, {"almost up", batch(100*time.Millisecond, 5*time.Second, false), time.Second, 5 * time.Second, true}, {"closed by a stop", batch(90*time.Second, 10*time.Minute, true), 0, 0, false}, {"up", batch(-time.Second, 10*time.Minute, false), 0, 0, false}, {"cut at once", func() inventory.Plan { b := batch(90*time.Second, 10*time.Minute, false) b.Delivery.Batch.CutAtOnce = true return b }(), 0, 0, false}, {"no batch", inventory.Plan{}, 0, 0, false}, } { quiet, most, armed := windowDeadlines(c.b, now) if armed != c.armed || quiet != c.quiet || most != c.most { t.Errorf("%s: %v, %v, %v; want %v, %v, %v", c.what, quiet, most, armed, c.quiet, c.most, c.armed) } } } // A deadline that is not a whole number of seconds is rounded up, never down: the server rounds a key's time to live // down to whole seconds, so 89.999999 s would expire at 89 s, before the window is closed by the record's times, and // the look would cut nothing (review of issue 973). func TestAFractionalDeadlineIsRoundedUpToTheNextWholeSecond(t *testing.T) { now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) b := inventory.Plan{State: inventory.PlanAssembling, Delivery: &inventory.PlanDelivery{Batch: &inventory.PlanBatch{ ClosesAt: now.Add(89*time.Second + 999999*time.Microsecond), AtMost: now.Add(599*time.Second + time.Nanosecond)}}} quiet, most, armed := windowDeadlines(b, now) if !armed || quiet != 90*time.Second || most != 600*time.Second { t.Errorf("deadlines %v, %v (armed %v); want 1m30s, 10m0s", quiet, most, armed) } }