import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as taskbuffer from '../ts/index.js'; import * as smartdelay from '@push.rocks/smartdelay'; // Test 1: Default task rejects on error (catchErrors: false) tap.test('should reject when taskFunction throws (default catchErrors: false)', async () => { const failingTask = new taskbuffer.Task({ name: 'failing-task-default', taskFunction: async () => { throw new Error('intentional failure'); }, }); let didReject = false; try { await failingTask.trigger(); } catch (err) { didReject = true; expect(err).toBeInstanceOf(Error); expect((err as Error).message).toEqual('intentional failure'); } expect(didReject).toBeTrue(); }); // Test 2: Task with catchErrors: true resolves on error tap.test('should resolve with undefined when catchErrors is true', async () => { const failingTask = new taskbuffer.Task({ name: 'failing-task-catch', catchErrors: true, taskFunction: async () => { throw new Error('swallowed failure'); }, }); const result = await failingTask.trigger(); expect(result).toBeUndefined(); }); // Test 3: lastError and errorCount are set after failure tap.test('should set lastError and errorCount after failure', async () => { const failingTask = new taskbuffer.Task({ name: 'failing-task-state', catchErrors: true, taskFunction: async () => { throw new Error('tracked failure'); }, }); await failingTask.trigger(); expect(failingTask.lastError).toBeInstanceOf(Error); expect(failingTask.lastError.message).toEqual('tracked failure'); expect(failingTask.errorCount).toEqual(1); // Run again to verify errorCount increments await failingTask.trigger(); expect(failingTask.errorCount).toEqual(2); }); // Test 4: Error state resets on successful re-run tap.test('should reset lastError on successful re-run', async () => { let shouldFail = true; const task = new taskbuffer.Task({ name: 'intermittent-task', catchErrors: true, taskFunction: async () => { if (shouldFail) { throw new Error('first run fails'); } return 'success'; }, }); // First run: fail await task.trigger(); expect(task.lastError).toBeInstanceOf(Error); expect(task.errorCount).toEqual(1); // Second run: succeed shouldFail = false; const result = await task.trigger(); expect(result).toEqual('success'); expect(task.lastError).toBeUndefined(); // errorCount should still be 1 since the second run succeeded expect(task.errorCount).toEqual(1); }); // Test 5: Taskchain rejects when a child task throws tap.test('should reject Taskchain when a child task throws', async () => { const goodTask = new taskbuffer.Task({ name: 'good-chain-task', taskFunction: async () => 'ok', }); const badTask = new taskbuffer.Task({ name: 'bad-chain-task', taskFunction: async () => { throw new Error('chain failure'); }, }); const chain = new taskbuffer.Taskchain({ name: 'test-chain', taskArray: [goodTask, badTask], }); let didReject = false; try { await chain.trigger(); } catch (err) { didReject = true; } expect(didReject).toBeTrue(); }); // Test 6: Taskparallel rejects when a child task throws tap.test('should reject Taskparallel when a child task throws', async () => { const goodTask = new taskbuffer.Task({ name: 'good-parallel-task', taskFunction: async () => 'ok', }); const badTask = new taskbuffer.Task({ name: 'bad-parallel-task', taskFunction: async () => { throw new Error('parallel failure'); }, }); const parallel = new taskbuffer.Taskparallel({ taskArray: [goodTask, badTask], }); let didReject = false; try { await parallel.trigger(); } catch (err) { didReject = true; } expect(didReject).toBeTrue(); }); // Test 7: TaskRunner continues processing after a task error tap.test('should continue TaskRunner queue after a task error', async () => { const runner = new taskbuffer.TaskRunner(); const executionOrder: string[] = []; const badTask = new taskbuffer.Task({ name: 'runner-bad-task', taskFunction: async () => { executionOrder.push('bad'); throw new Error('runner task failure'); }, }); const goodTask = new taskbuffer.Task({ name: 'runner-good-task', taskFunction: async () => { executionOrder.push('good'); }, }); await runner.start(); runner.addTask(badTask); runner.addTask(goodTask); // Wait for both tasks to be processed await smartdelay.delayFor(500); await runner.stop(); expect(executionOrder).toContain('bad'); expect(executionOrder).toContain('good'); }); // Test 8: BufferRunner handles errors without hanging tap.test('should handle BufferRunner errors without hanging', async () => { let callCount = 0; const bufferedTask = new taskbuffer.Task({ name: 'buffer-error-task', buffered: true, bufferMax: 3, catchErrors: true, taskFunction: async () => { callCount++; throw new Error('buffer failure'); }, }); await bufferedTask.trigger(); // The task should have executed and not hung expect(callCount).toBeGreaterThan(0); }); // Test 9: clearError() resets error state tap.test('should reset error state with clearError()', async () => { const task = new taskbuffer.Task({ name: 'clearable-task', catchErrors: true, taskFunction: async () => { throw new Error('to be cleared'); }, }); await task.trigger(); expect(task.lastError).toBeInstanceOf(Error); task.clearError(); expect(task.lastError).toBeUndefined(); // errorCount should remain unchanged expect(task.errorCount).toEqual(1); }); // Test 10: getMetadata() reflects error state tap.test('should reflect error state in getMetadata()', async () => { const task = new taskbuffer.Task({ name: 'metadata-error-task', catchErrors: true, taskFunction: async () => { throw new Error('metadata failure'); }, }); // Before any run let metadata = task.getMetadata(); expect(metadata.status).toEqual('idle'); expect(metadata.errorCount).toEqual(0); // After failing await task.trigger(); metadata = task.getMetadata(); expect(metadata.status).toEqual('failed'); expect(metadata.lastError).toEqual('metadata failure'); expect(metadata.errorCount).toEqual(1); }); // Test 11: TaskChain error includes task name and index context tap.test('should include task name and index in TaskChain error message', async () => { const goodTask = new taskbuffer.Task({ name: 'chain-step-ok', taskFunction: async () => 'ok', }); const badTask = new taskbuffer.Task({ name: 'chain-step-fail', taskFunction: async () => { throw new Error('step exploded'); }, }); const chain = new taskbuffer.Taskchain({ name: 'context-chain', taskArray: [goodTask, badTask], }); let caughtError: Error | null = null; try { await chain.trigger(); } catch (err) { caughtError = err as Error; } expect(caughtError).toBeInstanceOf(Error); expect(caughtError.message).toInclude('context-chain'); expect(caughtError.message).toInclude('chain-step-fail'); expect(caughtError.message).toInclude('index 1'); expect((caughtError as any).cause).toBeInstanceOf(Error); expect(((caughtError as any).cause as Error).message).toEqual('step exploded'); }); // Test 12: BufferRunner error propagation with catchErrors: false tap.test('should reject buffered task when catchErrors is false', async () => { const bufferedTask = new taskbuffer.Task({ name: 'buffer-reject-task', buffered: true, bufferMax: 3, catchErrors: false, taskFunction: async () => { throw new Error('buffer reject failure'); }, }); let didReject = false; try { await bufferedTask.trigger(); } catch (err) { didReject = true; expect(err).toBeInstanceOf(Error); expect((err as Error).message).toEqual('buffer reject failure'); } expect(didReject).toBeTrue(); }); // Test 13: Taskchain removeTask removes and returns true tap.test('should remove a task from Taskchain', async () => { const task1 = new taskbuffer.Task({ name: 'removable-1', taskFunction: async () => 'a', }); const task2 = new taskbuffer.Task({ name: 'removable-2', taskFunction: async () => 'b', }); const chain = new taskbuffer.Taskchain({ name: 'remove-chain', taskArray: [task1, task2], }); const removed = chain.removeTask(task1); expect(removed).toBeTrue(); expect(chain.taskArray.length).toEqual(1); expect(chain.taskArray[0] === task2).toBeTrue(); }); // Test 14: Taskchain removeTask returns false for unknown task tap.test('should return false when removing a task not in Taskchain', async () => { const task1 = new taskbuffer.Task({ name: 'existing', taskFunction: async () => 'a', }); const unknown = new taskbuffer.Task({ name: 'unknown', taskFunction: async () => 'b', }); const chain = new taskbuffer.Taskchain({ name: 'remove-false-chain', taskArray: [task1], }); const removed = chain.removeTask(unknown); expect(removed).toBeFalse(); expect(chain.taskArray.length).toEqual(1); }); // Test 15: Taskchain shiftTask returns first task and shortens array tap.test('should shift the first task from Taskchain', async () => { const task1 = new taskbuffer.Task({ name: 'shift-1', taskFunction: async () => 'a', }); const task2 = new taskbuffer.Task({ name: 'shift-2', taskFunction: async () => 'b', }); const chain = new taskbuffer.Taskchain({ name: 'shift-chain', taskArray: [task1, task2], }); const shifted = chain.shiftTask(); expect(shifted === task1).toBeTrue(); expect(chain.taskArray.length).toEqual(1); expect(chain.taskArray[0] === task2).toBeTrue(); }); // Test 16: Taskchain shiftTask returns undefined on empty array tap.test('should return undefined when shifting from empty Taskchain', async () => { const chain = new taskbuffer.Taskchain({ name: 'empty-shift-chain', taskArray: [], }); const shifted = chain.shiftTask(); expect(shifted).toBeUndefined(); }); export default tap.start();