package poster import ( "context" "path/filepath" "sync" "testing" "time" "pete/internal/config" "pete/internal/storage" ) // mockMatrix implements the subset of matrix.Client that Queue needs. // We test via the public Enqueue/Start interface and check storage state. func setupTestEnv(t *testing.T) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") if err := storage.Init(dbPath); err != nil { t.Fatal(err) } t.Cleanup(func() { storage.Close() }) } func TestQueue_EnqueueAndDrain(t *testing.T) { setupTestEnv(t) q := &Queue{ queues: make(map[string][]QueueItem), config: config.PostingConfig{ MinIntervalSeconds: 0, BurstCapCount: 10, BurstCapWindowSeconds: 1800, }, done: make(chan struct{}), } // Enqueue directly (bypass ChannelRoomID check) q.mu.Lock() q.queues["tech"] = append(q.queues["tech"], QueueItem{ GUID: "g1", Channel: "tech", }) q.mu.Unlock() // Verify item is in queue q.mu.Lock() if len(q.queues["tech"]) != 1 { t.Fatalf("expected 1 item in queue, got %d", len(q.queues["tech"])) } q.mu.Unlock() } func TestQueueItem_MaxRetries(t *testing.T) { // Verify the retry counter works item := QueueItem{GUID: "g1", Channel: "tech", retries: 0} item.retries++ if item.retries != 1 { t.Errorf("expected retries=1, got %d", item.retries) } item.retries++ item.retries++ if item.retries < maxRetries { t.Errorf("expected retries >= maxRetries after 3 increments, got %d", item.retries) } } func TestQueue_StartAndWait(t *testing.T) { setupTestEnv(t) q := &Queue{ queues: make(map[string][]QueueItem), config: config.PostingConfig{ MinIntervalSeconds: 0, BurstCapCount: 10, BurstCapWindowSeconds: 1800, }, done: make(chan struct{}), } ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() q.Start(ctx) }() // Cancel immediately — Start should return promptly cancel() done := make(chan struct{}) go func() { q.Wait() close(done) }() select { case <-done: // good case <-time.After(5 * time.Second): t.Fatal("queue.Wait() did not return after cancel") } } func TestQueue_BurstCap(t *testing.T) { setupTestEnv(t) cfg := config.PostingConfig{ MinIntervalSeconds: 0, BurstCapCount: 2, BurstCapWindowSeconds: 3600, } q := &Queue{ queues: make(map[string][]QueueItem), config: cfg, done: make(chan struct{}), } // Simulate 2 posts already in window now := time.Now().Unix() storage.Get().Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`, "prev1", "tech", "$e1", now-100) storage.Get().Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`, "prev2", "tech", "$e2", now-50) // Queue a new item q.mu.Lock() q.queues["tech"] = []QueueItem{{GUID: "g3", Channel: "tech"}} q.mu.Unlock() // drainChannel should not post (burst cap reached) q.drainChannel("tech") q.mu.Lock() remaining := len(q.queues["tech"]) q.mu.Unlock() if remaining != 1 { t.Errorf("expected item to remain in queue (burst cap), got %d remaining", remaining) } }