From 2f5a31fc995557e32dbaf27556a7baee59510f10 Mon Sep 17 00:00:00 2001 From: LobeHub Bot Date: Mon, 30 Mar 2026 18:53:03 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20test:=20add=20unit=20tests=20for=20?= =?UTF-8?q?LocalTaskScheduler=20(#13398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../taskScheduler/impls/local.test.ts | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/server/services/taskScheduler/impls/local.test.ts diff --git a/src/server/services/taskScheduler/impls/local.test.ts b/src/server/services/taskScheduler/impls/local.test.ts new file mode 100644 index 0000000000..445e34d0c5 --- /dev/null +++ b/src/server/services/taskScheduler/impls/local.test.ts @@ -0,0 +1,179 @@ +// @vitest-environment node +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { LocalTaskScheduler } from './local'; + +describe('LocalTaskScheduler', () => { + let scheduler: LocalTaskScheduler; + + beforeEach(() => { + vi.useFakeTimers(); + scheduler = new LocalTaskScheduler(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('scheduleNextTopic', () => { + it('should return a schedule ID immediately', async () => { + const scheduleId = await scheduler.scheduleNextTopic({ + taskId: 'task-1', + userId: 'user-1', + }); + + expect(scheduleId).toMatch(/^local-task-task-1-\d+$/); + }); + + it('should execute callback after delay', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + await scheduler.scheduleNextTopic({ delay: 5, taskId: 'task-1', userId: 'user-1' }); + + expect(callback).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(5000); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith('task-1', 'user-1'); + }); + + it('should execute callback immediately when delay is 0', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + await scheduler.scheduleNextTopic({ delay: 0, taskId: 'task-2', userId: 'user-2' }); + + await vi.advanceTimersByTimeAsync(0); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith('task-2', 'user-2'); + }); + + it('should execute callback immediately when delay is not provided', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + await scheduler.scheduleNextTopic({ taskId: 'task-3', userId: 'user-3' }); + + await vi.advanceTimersByTimeAsync(0); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith('task-3', 'user-3'); + }); + + it('should not execute callback when no callback is set', async () => { + // No callback set - should not throw + await scheduler.scheduleNextTopic({ delay: 1, taskId: 'task-4', userId: 'user-4' }); + + await expect(vi.advanceTimersByTimeAsync(1000)).resolves.not.toThrow(); + }); + + it('should remove schedule from pending after execution', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + const scheduleId = await scheduler.scheduleNextTopic({ + delay: 1, + taskId: 'task-5', + userId: 'user-5', + }); + + await vi.advanceTimersByTimeAsync(1000); + + // After execution, canceling should be a no-op (not throw) + await expect(scheduler.cancelScheduled(scheduleId)).resolves.not.toThrow(); + }); + + it('should handle callback errors without throwing', async () => { + const callback = vi.fn().mockRejectedValue(new Error('execution failed')); + scheduler.setExecutionCallback(callback); + + await scheduler.scheduleNextTopic({ delay: 1, taskId: 'task-6', userId: 'user-6' }); + + await expect(vi.advanceTimersByTimeAsync(1000)).resolves.not.toThrow(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should support multiple concurrent schedules', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + await scheduler.scheduleNextTopic({ delay: 1, taskId: 'task-a', userId: 'user-1' }); + await scheduler.scheduleNextTopic({ delay: 2, taskId: 'task-b', userId: 'user-1' }); + await scheduler.scheduleNextTopic({ delay: 3, taskId: 'task-c', userId: 'user-1' }); + + await vi.advanceTimersByTimeAsync(1000); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('task-a', 'user-1'); + + await vi.advanceTimersByTimeAsync(1000); + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledWith('task-b', 'user-1'); + + await vi.advanceTimersByTimeAsync(1000); + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenCalledWith('task-c', 'user-1'); + }); + }); + + describe('cancelScheduled', () => { + it('should cancel a pending schedule before it fires', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + const scheduleId = await scheduler.scheduleNextTopic({ + delay: 5, + taskId: 'task-7', + userId: 'user-7', + }); + + await scheduler.cancelScheduled(scheduleId); + + await vi.advanceTimersByTimeAsync(5000); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should be a no-op for unknown schedule IDs', async () => { + await expect(scheduler.cancelScheduled('unknown-schedule-id')).resolves.not.toThrow(); + }); + + it('should not affect other pending schedules when canceling one', async () => { + const callback = vi.fn().mockResolvedValue(undefined); + scheduler.setExecutionCallback(callback); + + const id1 = await scheduler.scheduleNextTopic({ + delay: 2, + taskId: 'task-8a', + userId: 'user-8', + }); + await scheduler.scheduleNextTopic({ delay: 2, taskId: 'task-8b', userId: 'user-8' }); + + await scheduler.cancelScheduled(id1); + + await vi.advanceTimersByTimeAsync(2000); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('task-8b', 'user-8'); + }); + }); + + describe('setExecutionCallback', () => { + it('should update the callback', async () => { + const firstCallback = vi.fn().mockResolvedValue(undefined); + const secondCallback = vi.fn().mockResolvedValue(undefined); + + scheduler.setExecutionCallback(firstCallback); + scheduler.setExecutionCallback(secondCallback); + + await scheduler.scheduleNextTopic({ delay: 1, taskId: 'task-9', userId: 'user-9' }); + + await vi.advanceTimersByTimeAsync(1000); + + expect(firstCallback).not.toHaveBeenCalled(); + expect(secondCallback).toHaveBeenCalledOnce(); + }); + }); +});