import { DyNTS_Scheduler_Service } from './scheduler.service'; describe('| DyNTS_Scheduler_Service', (): void => { let svc: DyNTS_Scheduler_Service; beforeEach((): void => { svc = DyNTS_Scheduler_Service.getInstance(); svc._resetForTesting(); }); afterEach((): void => { svc._resetForTesting(); }); describe('| singleton', (): void => { it('| returns the same instance', (): void => { const a: DyNTS_Scheduler_Service = DyNTS_Scheduler_Service.getInstance(); const b: DyNTS_Scheduler_Service = DyNTS_Scheduler_Service.getInstance(); expect(a).toBe(b); expect(a).toBeInstanceOf(DyNTS_Scheduler_Service); }); }); describe('| registerJob', (): void => { it('| registers a job by key', (): void => { svc.registerJob({ key: 'a', intervalMs: 1000, handler: (): void => {} }); expect(svc.getJobKeys()).toEqual(['a']); }); it('| throws on duplicate key', (): void => { svc.registerJob({ key: 'dup', intervalMs: 1000, handler: (): void => {} }); expect((): void => svc.registerJob({ key: 'dup', intervalMs: 1000, handler: (): void => {} })).toThrow(); }); it('| throws on missing key', (): void => { expect((): void => svc.registerJob({ key: '', intervalMs: 1000, handler: (): void => {} })).toThrow(); }); it('| throws on non-positive intervalMs', (): void => { expect((): void => svc.registerJob({ key: 'x', intervalMs: 0, handler: (): void => {} })).toThrow(); }); }); describe('| unregisterJob', (): void => { it('| removes a registered job', (): void => { svc.registerJob({ key: 'a', intervalMs: 1000, handler: (): void => {} }); svc.unregisterJob('a'); expect(svc.getJobKeys()).toEqual([]); }); it('| is a no-op for unknown key', (): void => { expect((): void => svc.unregisterJob('nope')).not.toThrow(); }); }); describe('| start / stop', (): void => { it('| start marks the scheduler started', (): void => { expect(svc.isStarted()).toBe(false); svc.start(); expect(svc.isStarted()).toBe(true); }); it('| stop marks the scheduler stopped', (): void => { svc.start(); svc.stop(); expect(svc.isStarted()).toBe(false); }); it('| start is idempotent', (): void => { svc.start(); svc.start(); expect(svc.isStarted()).toBe(true); }); }); describe('| runNow', (): void => { it('| executes the handler and records a success run', async (): Promise => { let calls: number = 0; svc.registerJob({ key: 'ok', intervalMs: 1000, handler: (): void => { calls++; } }); await svc.runNow('ok'); expect(calls).toBe(1); const runs: ReturnType = svc.getRuns(); expect(runs.length).toBe(1); expect(runs[0].jobKey).toBe('ok'); expect(runs[0].status).toBe('success'); expect(runs[0].finishedAt).toBeDefined(); }); it('| records a failed run with a rich error message', async (): Promise => { svc.registerJob({ key: 'bad', intervalMs: 1000, handler: (): void => { throw new Error('boom'); } }); await svc.runNow('bad'); const runs: ReturnType = svc.getRuns(); expect(runs[0].status).toBe('failed'); expect(runs[0].error).toContain('boom'); }); it('| throws for an unknown job key', async (): Promise => { let thrown: unknown = null; try { await svc.runNow('nope'); } catch (error: unknown) { thrown = error; } expect(thrown).not.toBeNull(); }); it('| skip-if-running prevents overlapping executions', async (): Promise => { let active: number = 0; let maxActive: number = 0; svc.registerJob({ key: 'lock', intervalMs: 1000, handler: async (): Promise => { active++; maxActive = Math.max(maxActive, active); await new Promise((resolve: () => void): void => { setTimeout(resolve, 20); }); active--; }, }); await Promise.all([svc.runNow('lock'), svc.runNow('lock')]); expect(maxActive).toBe(1); }); }); describe('| getRuns', (): void => { it('| returns the most recent run first', async (): Promise => { svc.registerJob({ key: 'first', intervalMs: 1000, handler: (): void => {} }); svc.registerJob({ key: 'second', intervalMs: 1000, handler: (): void => {} }); await svc.runNow('first'); await svc.runNow('second'); const runs: ReturnType = svc.getRuns(); expect(runs.length).toBe(2); expect(runs[0].jobKey).toBe('second'); expect(runs[1].jobKey).toBe('first'); }); }); });