Files
taskbuffer/test/test.11.errorhandling.ts

364 lines
10 KiB
TypeScript
Raw Normal View History

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();