364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
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();
|