fix(tests): add and tighten constraint-related tests covering return values, error propagation, concurrency, cooldown timing, and constraint removal
This commit is contained in:
@@ -179,7 +179,7 @@ tap.test('should enforce cooldown between task executions', async () => {
|
||||
// Each execution should be at least ~300ms apart (with 200ms tolerance)
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
const gap = timestamps[i] - timestamps[i - 1];
|
||||
expect(gap).toBeGreaterThanOrEqual(100); // 300ms cooldown minus tolerance
|
||||
expect(gap).toBeGreaterThanOrEqual(250); // 300ms cooldown minus 50ms tolerance
|
||||
}
|
||||
|
||||
await manager.stop();
|
||||
@@ -388,4 +388,306 @@ tap.test('should reset constraint group state', async () => {
|
||||
expect(constraint.getCooldownRemaining('key')).toEqual(0);
|
||||
});
|
||||
|
||||
// Test 13: Queued task returns correct result
|
||||
tap.test('should return correct result from queued tasks', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'return-value-test',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'shared',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const t1 = new taskbuffer.Task({
|
||||
name: 'ret-1',
|
||||
taskFunction: async () => {
|
||||
await smartdelay.delayFor(100);
|
||||
return 'result-A';
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = new taskbuffer.Task({
|
||||
name: 'ret-2',
|
||||
taskFunction: async () => {
|
||||
return 'result-B';
|
||||
},
|
||||
});
|
||||
|
||||
manager.addTask(t1);
|
||||
manager.addTask(t2);
|
||||
|
||||
const [r1, r2] = await Promise.all([
|
||||
manager.triggerTaskConstrained(t1),
|
||||
manager.triggerTaskConstrained(t2),
|
||||
]);
|
||||
|
||||
expect(r1).toEqual('result-A');
|
||||
expect(r2).toEqual('result-B');
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 14: Error propagation for queued tasks (catchErrors: false)
|
||||
tap.test('should propagate errors from queued tasks (catchErrors: false)', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'error-propagation',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'shared',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const t1 = new taskbuffer.Task({
|
||||
name: 'err-first',
|
||||
taskFunction: async () => {
|
||||
await smartdelay.delayFor(100);
|
||||
return 'ok';
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = new taskbuffer.Task({
|
||||
name: 'err-second',
|
||||
catchErrors: false,
|
||||
taskFunction: async () => {
|
||||
throw new Error('queued-error');
|
||||
},
|
||||
});
|
||||
|
||||
manager.addTask(t1);
|
||||
manager.addTask(t2);
|
||||
|
||||
const r1Promise = manager.triggerTaskConstrained(t1);
|
||||
const r2Promise = manager.triggerTaskConstrained(t2);
|
||||
|
||||
const r1 = await r1Promise;
|
||||
expect(r1).toEqual('ok');
|
||||
|
||||
let caughtError: Error | null = null;
|
||||
try {
|
||||
await r2Promise;
|
||||
} catch (err) {
|
||||
caughtError = err as Error;
|
||||
}
|
||||
|
||||
expect(caughtError).toBeTruthy();
|
||||
expect(caughtError!.message).toEqual('queued-error');
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 15: triggerTask() routes through constraints
|
||||
tap.test('should route triggerTask() through constraints', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
let running = 0;
|
||||
let maxRunning = 0;
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'trigger-task-test',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'all',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const makeTask = (id: number) =>
|
||||
new taskbuffer.Task({
|
||||
name: `tt-${id}`,
|
||||
taskFunction: async () => {
|
||||
running++;
|
||||
maxRunning = Math.max(maxRunning, running);
|
||||
await smartdelay.delayFor(100);
|
||||
running--;
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = makeTask(1);
|
||||
const t2 = makeTask(2);
|
||||
|
||||
manager.addTask(t1);
|
||||
manager.addTask(t2);
|
||||
|
||||
await Promise.all([
|
||||
manager.triggerTask(t1),
|
||||
manager.triggerTask(t2),
|
||||
]);
|
||||
|
||||
expect(maxRunning).toBeLessThanOrEqual(1);
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 16: addExecuteRemoveTask() routes through constraints
|
||||
tap.test('should route addExecuteRemoveTask() through constraints', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
let running = 0;
|
||||
let maxRunning = 0;
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'add-execute-remove-test',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'all',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const makeTask = (id: number) =>
|
||||
new taskbuffer.Task({
|
||||
name: `aer-${id}`,
|
||||
taskFunction: async () => {
|
||||
running++;
|
||||
maxRunning = Math.max(maxRunning, running);
|
||||
await smartdelay.delayFor(100);
|
||||
running--;
|
||||
return `done-${id}`;
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = makeTask(1);
|
||||
const t2 = makeTask(2);
|
||||
|
||||
const [report1, report2] = await Promise.all([
|
||||
manager.addExecuteRemoveTask(t1),
|
||||
manager.addExecuteRemoveTask(t2),
|
||||
]);
|
||||
|
||||
expect(maxRunning).toBeLessThanOrEqual(1);
|
||||
expect(report1.result).toEqual('done-1');
|
||||
expect(report2.result).toEqual('done-2');
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 17: FIFO ordering of queued tasks
|
||||
tap.test('should execute queued tasks in FIFO order', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'fifo-test',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'shared',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const makeTask = (id: string) =>
|
||||
new taskbuffer.Task({
|
||||
name: `fifo-${id}`,
|
||||
taskFunction: async () => {
|
||||
executionOrder.push(id);
|
||||
await smartdelay.delayFor(50);
|
||||
},
|
||||
});
|
||||
|
||||
const tA = makeTask('A');
|
||||
const tB = makeTask('B');
|
||||
const tC = makeTask('C');
|
||||
|
||||
manager.addTask(tA);
|
||||
manager.addTask(tB);
|
||||
manager.addTask(tC);
|
||||
|
||||
await Promise.all([
|
||||
manager.triggerTaskConstrained(tA),
|
||||
manager.triggerTaskConstrained(tB),
|
||||
manager.triggerTaskConstrained(tC),
|
||||
]);
|
||||
|
||||
expect(executionOrder).toEqual(['A', 'B', 'C']);
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 18: Combined concurrency + cooldown
|
||||
tap.test('should enforce both concurrency and cooldown together', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
let running = 0;
|
||||
let maxRunning = 0;
|
||||
const timestamps: number[] = [];
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'combined-test',
|
||||
maxConcurrent: 2,
|
||||
cooldownMs: 200,
|
||||
constraintKeyForTask: () => 'shared',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const makeTask = (id: number) =>
|
||||
new taskbuffer.Task({
|
||||
name: `combo-${id}`,
|
||||
taskFunction: async () => {
|
||||
running++;
|
||||
maxRunning = Math.max(maxRunning, running);
|
||||
timestamps.push(Date.now());
|
||||
await smartdelay.delayFor(100);
|
||||
running--;
|
||||
},
|
||||
});
|
||||
|
||||
const tasks = [makeTask(1), makeTask(2), makeTask(3), makeTask(4)];
|
||||
for (const t of tasks) {
|
||||
manager.addTask(t);
|
||||
}
|
||||
|
||||
await Promise.all(tasks.map((t) => manager.triggerTaskConstrained(t)));
|
||||
|
||||
// Concurrency never exceeded 2
|
||||
expect(maxRunning).toBeLessThanOrEqual(2);
|
||||
|
||||
// First 2 tasks start nearly together, 3rd task starts after first batch completes + cooldown
|
||||
// First batch completes ~100ms after start, then 200ms cooldown
|
||||
const gap = timestamps[2] - timestamps[0];
|
||||
expect(gap).toBeGreaterThanOrEqual(250); // 100ms task + 200ms cooldown - 50ms tolerance
|
||||
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
// Test 19: Constraint removal unblocks queued tasks
|
||||
tap.test('should unblock queued tasks when constraint group is removed', async () => {
|
||||
const manager = new taskbuffer.TaskManager();
|
||||
const log: string[] = [];
|
||||
|
||||
const constraint = new taskbuffer.TaskConstraintGroup({
|
||||
name: 'removable-constraint',
|
||||
maxConcurrent: 1,
|
||||
constraintKeyForTask: () => 'shared',
|
||||
});
|
||||
manager.addConstraintGroup(constraint);
|
||||
|
||||
const t1 = new taskbuffer.Task({
|
||||
name: 'block-1',
|
||||
taskFunction: async () => {
|
||||
log.push('t1-start');
|
||||
// Remove constraint while t1 is running so t2 runs unconstrained after drain
|
||||
manager.removeConstraintGroup('removable-constraint');
|
||||
await smartdelay.delayFor(100);
|
||||
log.push('t1-end');
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = new taskbuffer.Task({
|
||||
name: 'block-2',
|
||||
taskFunction: async () => {
|
||||
log.push('t2-start');
|
||||
log.push('t2-end');
|
||||
},
|
||||
});
|
||||
|
||||
manager.addTask(t1);
|
||||
manager.addTask(t2);
|
||||
|
||||
await Promise.all([
|
||||
manager.triggerTaskConstrained(t1),
|
||||
manager.triggerTaskConstrained(t2),
|
||||
]);
|
||||
|
||||
// Both tasks completed (drain didn't deadlock after constraint removal)
|
||||
expect(log).toContain('t1-start');
|
||||
expect(log).toContain('t1-end');
|
||||
expect(log).toContain('t2-start');
|
||||
expect(log).toContain('t2-end');
|
||||
|
||||
// t2 started after t1 completed (drain fires after t1 finishes)
|
||||
const t1EndIdx = log.indexOf('t1-end');
|
||||
const t2StartIdx = log.indexOf('t2-start');
|
||||
expect(t2StartIdx).toBeGreaterThanOrEqual(t1EndIdx);
|
||||
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
Reference in New Issue
Block a user