package crontab import ( "context" "io" "log" "sync" "testing" "time" "github.com/stretchr/testify/require" "senlinai-agent/backend/internal/models" ) func TestDatasetSchedulerCollectsImmediatelyAndRepeats(t *testing.T) { collector := &recordingDatasetCollector{calls: make(chan time.Time, 3)} scheduler := &DatasetScheduler{ collector: collector, interval: 10 * time.Millisecond, logger: log.New(io.Discard, "", 0), } ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { defer close(done) scheduler.Run(ctx) }() first := receiveCollectionCall(t, collector.calls) second := receiveCollectionCall(t, collector.calls) require.GreaterOrEqual(t, second.Sub(first), 8*time.Millisecond) cancel() require.Eventually(t, func() bool { select { case <-done: return true default: return false } }, time.Second, 5*time.Millisecond) } func TestDatasetSchedulerStopsWhenContextIsAlreadyCanceled(t *testing.T) { collector := &recordingDatasetCollector{calls: make(chan time.Time, 1)} scheduler := &DatasetScheduler{ collector: collector, interval: 10 * time.Millisecond, logger: log.New(io.Discard, "", 0), } ctx, cancel := context.WithCancel(context.Background()) cancel() scheduler.Run(ctx) require.Zero(t, collector.callCount()) } func TestDatasetSchedulerSkipsTicksWhileCollectionIsRunning(t *testing.T) { collector := &blockingDatasetCollector{ started: make(chan struct{}, 2), release: make(chan struct{}), } scheduler := &DatasetScheduler{ collector: collector, interval: 5 * time.Millisecond, logger: log.New(io.Discard, "", 0), } ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { defer close(done) scheduler.Run(ctx) }() select { case <-collector.started: case <-time.After(time.Second): t.Fatal("timed out waiting for first collection") } time.Sleep(20 * time.Millisecond) require.Equal(t, 1, collector.callCount()) close(collector.release) require.Eventually(t, func() bool { return collector.callCount() >= 2 }, time.Second, 5*time.Millisecond) cancel() <-done } type recordingDatasetCollector struct { mu sync.Mutex count int calls chan time.Time } func (c *recordingDatasetCollector) SyncAllSources(context.Context) ([]models.SaDatasetCron, error) { now := time.Now() c.mu.Lock() c.count++ c.mu.Unlock() c.calls <- now return []models.SaDatasetCron{{Status: "completed"}}, nil } func (c *recordingDatasetCollector) callCount() int { c.mu.Lock() defer c.mu.Unlock() return c.count } func receiveCollectionCall(t *testing.T, calls <-chan time.Time) time.Time { t.Helper() select { case calledAt := <-calls: return calledAt case <-time.After(time.Second): t.Fatal("timed out waiting for dataset collection") return time.Time{} } } type blockingDatasetCollector struct { mu sync.Mutex count int started chan struct{} release chan struct{} } func (c *blockingDatasetCollector) SyncAllSources(ctx context.Context) ([]models.SaDatasetCron, error) { c.mu.Lock() c.count++ c.mu.Unlock() select { case c.started <- struct{}{}: default: } select { case <-c.release: case <-ctx.Done(): } return nil, nil } func (c *blockingDatasetCollector) callCount() int { c.mu.Lock() defer c.mu.Unlock() return c.count }