package fasttick import ( "io" "log/slog" "testing" "time" ) func quiet() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } // flagSource is a fake Source whose convergence the test flips at will. type flagSource struct { unconverged bool reason string } func (f *flagSource) Unconverged() (bool, string) { return f.unconverged, f.reason } // drain reports how many pulses are queued (channel is cap-1 in production; tests may use larger). func drain(ch chan struct{}) int { n := 0 for { select { case <-ch: n++ default: return n } } } // D1 — any source unconverged → one pulse per tick (drained between ticks). func TestFastTick_UnconvergedPulses(t *testing.T) { out := make(chan struct{}, 1) src := &flagSource{unconverged: true, reason: "test drift"} l := New(out, time.Hour, quiet(), src) for i := 0; i < 3; i++ { if !l.step() { t.Fatalf("tick %d: step reported converged, want unconverged", i) } if got := drain(out); got != 1 { t.Fatalf("tick %d: pulses = %d, want 1", i, got) } } } // D2 — ALL sources converged → zero pulses across N ticks (the emergent disarm). func TestFastTick_ConvergedSilent(t *testing.T) { out := make(chan struct{}, 4) l := New(out, time.Hour, quiet(), &flagSource{unconverged: false}, &flagSource{unconverged: false}) for i := 0; i < 5; i++ { if l.step() { t.Fatalf("tick %d: step reported unconverged with all sources converged", i) } } if got := drain(out); got != 0 { t.Fatalf("pulses on a fully-converged box = %d, want 0", got) } } // D3 (the ruled red-proof) — flip unconverged→converged mid-run → pulses STOP from the next tick. func TestFastTick_ConvergenceDisarms(t *testing.T) { out := make(chan struct{}, 8) src := &flagSource{unconverged: true, reason: "drift"} l := New(out, time.Hour, quiet(), src) // Two unconverged ticks pulse. l.step() l.step() if got := drain(out); got != 2 { t.Fatalf("pre-convergence pulses = %d, want 2", got) } // Converge. src.unconverged = false // Every subsequent tick is silent — the cadence returns to normal. for i := 0; i < 4; i++ { if l.step() { t.Fatalf("post-convergence tick %d still unconverged", i) } } if got := drain(out); got != 0 { t.Fatalf("pulses fired after convergence = %d, want 0 (disarm failed)", got) } } // D4 — the out channel is already full (a poke just landed): the non-blocking send drops, no block, // no goroutine leak, no queue growth. func TestFastTick_ChannelFullDrops(t *testing.T) { out := make(chan struct{}, 1) out <- struct{}{} // pre-fill: an out-of-band report is already pending l := New(out, time.Hour, quiet(), &flagSource{unconverged: true, reason: "drift"}) done := make(chan bool, 1) go func() { l.step() // must NOT block on the full channel l.step() done <- true }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("step blocked on a full channel (non-blocking send violated)") } if len(out) != 1 { t.Fatalf("channel depth = %d, want 1 (coalesced, no queue growth)", len(out)) } } // Priority ordering: the first unconverged source supplies the reason. func TestFastTick_FirstReasonWins(t *testing.T) { out := make(chan struct{}, 1) l := New(out, time.Hour, quiet(), &flagSource{unconverged: false}, &flagSource{unconverged: true, reason: "second"}, &flagSource{unconverged: true, reason: "third"}) if u, r := l.evaluate(); !u || r != "second" { t.Fatalf("evaluate = (%v, %q), want (true, second)", u, r) } }