package dispatch import ( "sync" "testing" "time" panelv1 "github.com/dbledeez/panel/proto/panel/v1" ) // blockingSender simulates a controller that has stopped draining the stream: // every Send blocks until released. This is the backpressure condition that // used to wedge the whole agent when sendEnv called Send inline. type blockingSender struct { release chan struct{} mu sync.Mutex sent int } func (b *blockingSender) Send(*panelv1.AgentEnvelope) error { <-b.release // block until the test lets it through b.mu.Lock() b.sent++ b.mu.Unlock() return nil } func logEnv() *panelv1.AgentEnvelope { return &panelv1.AgentEnvelope{Payload: &panelv1.AgentEnvelope_Log{Log: &panelv1.LogLine{Line: "x"}}} } func stateEnv() *panelv1.AgentEnvelope { return &panelv1.AgentEnvelope{Payload: &panelv1.AgentEnvelope_InstanceState{InstanceState: &panelv1.InstanceStateUpdate{InstanceId: "i"}}} } // newTestDispatcher builds a Dispatcher with just the send machinery wired — // enough to exercise sendEnv / sendLoop without a real runtime. func newTestDispatcher() *Dispatcher { d := &Dispatcher{sendCh: make(chan *panelv1.AgentEnvelope, 4096)} go d.sendLoop() return d } // TestSendEnvDropsTelemetryUnderBackpressure is the core regression guard for // the VEIN-exposed wedge: with the sender blocked, a flood of log envelopes far // exceeding the buffer must NOT block the producer. Pre-fix, sendEnv called // Send inline and the producer (and the whole agent) froze here. func TestSendEnvDropsTelemetryUnderBackpressure(t *testing.T) { d := newTestDispatcher() bs := &blockingSender{release: make(chan struct{})} d.SetSender(bs) done := make(chan struct{}) go func() { // 50k log frames — 12x the 4096 buffer. Must all return promptly. for i := 0; i < 50000; i++ { d.sendEnv(logEnv()) } close(done) }() select { case <-done: // Producer completed without blocking on the stalled sender. Pass. case <-time.After(5 * time.Second): t.Fatal("sendEnv blocked under backpressure — the wedge regressed") } } // TestCriticalEnvelopeNotDropped verifies an instance-state envelope is queued // (not silently dropped) even when telemetry is being shed. We fill the buffer // with the sender blocked, then confirm a critical send blocks until drained // (i.e. it's preserved, not discarded). func TestCriticalEnvelopeNotDropped(t *testing.T) { d := &Dispatcher{sendCh: make(chan *panelv1.AgentEnvelope, 2)} // No drainer started yet — fill the buffer. d.sendEnv(logEnv()) d.sendEnv(logEnv()) // Buffer is full (cap 2). A telemetry frame must drop (non-blocking). dropDone := make(chan struct{}) go func() { d.sendEnv(logEnv()); close(dropDone) }() select { case <-dropDone: case <-time.After(time.Second): t.Fatal("telemetry send blocked on full buffer — should have dropped") } // A critical frame must NOT drop: it blocks until space frees. Start a // drainer to free space and confirm the critical send then completes. critDone := make(chan struct{}) go func() { d.sendEnv(stateEnv()); close(critDone) }() select { case <-critDone: t.Fatal("critical send returned before buffer had space — it was dropped") case <-time.After(100 * time.Millisecond): // Correctly still blocking. Now drain and it should complete. } go d.sendLoop() select { case <-critDone: // Drained and delivered. Pass. case <-time.After(time.Second): t.Fatal("critical send never completed after drain") } } // TestConcurrentProducersNoRace runs many producers through sendEnv at once. // Pre-fix, each called stream.Send directly — concurrent Sends on one gRPC // stream are a data race. The single-drainer design serializes them. Run with // -race to catch a regression. func TestConcurrentProducersNoRace(t *testing.T) { d := newTestDispatcher() bs := &blockingSender{release: make(chan struct{})} close(bs.release) // never block — just count d.SetSender(bs) var wg sync.WaitGroup for p := 0; p < 32; p++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < 1000; i++ { d.sendEnv(logEnv()) } }() } wg.Wait() // Give the drainer a moment to flush. time.Sleep(200 * time.Millisecond) // No assertion on exact count (some may still be in flight / dropped); // the point is the -race detector finds no concurrent Send. }