package calls import ( "context" "io" "log/slog" "sync/atomic" "testing" "time" "amadan.net/oficina/calls/internal/calls/hub" ) // Review W-12: the fifteen-second housekeeping pass reads standing once // per joined participation, serially, and it used to be given the // process context and nothing else. A driver that stopped answering // would then stall §2.4's quiet case for as long as it stayed stuck — // no lapsed resume windows collected, no kind changes applied, and a // removed member dropped only by the next thing they did. // // The property under test is that the pass ends on its own deadline. // `within` is a parameter so this costs milliseconds rather than the // fifteen seconds tickForever passes. func TestATickPassEndsOnItsOwnDeadline(t *testing.T) { var stuck atomic.Bool reached := make(chan struct{}, 1) h := hub.New(hub.Config{ Now: time.Now, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), StandingFor: func(ctx context.Context, room string, personID int64) (hub.Kind, bool, error) { if !stuck.Load() { return hub.KindMember, true, nil } select { case reached <- struct{}{}: default: } // A read that never comes back on its own: the only thing // that ends it is the context the tick handed down. <-ctx.Done() return 0, false, ctx.Err() }, }) p, err := h.Open("r1", "s1", 1, "Mia", hub.KindMember) if err != nil { t.Fatal(err) } // Joined, so the tick has a standing to read for it. if err := h.Join(context.Background(), "r1", "s1", p.Peer); err != nil { t.Fatal(err) } stuck.Store(true) a := &app{hub: h} done := make(chan struct{}) started := time.Now() go func() { a.tickOnce(context.Background(), 200*time.Millisecond) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("a tick pass whose standing read never returned did not end on its own deadline") } select { case <-reached: default: t.Fatal("the pass never reached a standing read, so it proves nothing about the deadline") } if took := time.Since(started); took < 150*time.Millisecond { t.Fatalf("the pass ended after %v, before its own deadline", took) } } // The deadline is not decoration: the scheduling path gives each pass // the interval between passes, so a pass can never overrun the slot the // next one needs. This used to assert that a constant equalled itself, // which left the relationship free to be changed with the test green // (Codex S2-14) — so what runs here is the loop itself, in milliseconds, // with a recorder in the pass's place. func TestTheSchedulingPathGivesEachPassTheIntervalAsItsDeadline(t *testing.T) { const every = 20 * time.Millisecond got := make(chan time.Duration, 1) a := &app{tickPass: func(ctx context.Context, within time.Duration) { select { case got <- within: default: } }} ctx, cancel := context.WithCancel(context.Background()) defer cancel() go a.tickLoop(ctx, every) select { case within := <-got: if within != every { t.Fatalf("a pass scheduled every %v was given %v to run in", every, within) } case <-time.After(5 * time.Second): t.Fatal("the scheduling path never ran a pass") } // And the interval production runs on is the spec's. if tickEvery != 15*time.Second { t.Fatalf("tickEvery is %v, and spec §2.4 says fifteen seconds", tickEvery) } }