From b2c0553e305f94cde023538dd6875e57c99d3fff Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 15 Feb 2026 12:36:57 +0000 Subject: [PATCH] fix(tests): add and tighten constraint-related tests covering return values, error propagation, concurrency, cooldown timing, and constraint removal --- changelog.md | 8 + test/test.13.constraints.ts | 304 ++++++++++++++++++++++++++++++++++- ts/00_commitinfo_data.ts | 2 +- ts_web/00_commitinfo_data.ts | 2 +- 4 files changed, 313 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 5655f09..4250554 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-15 - 5.0.1 - fix(tests) +add and tighten constraint-related tests covering return values, error propagation, concurrency, cooldown timing, and constraint removal + +- Tightened cooldown timing assertion from >=100ms to >=250ms to reflect 300ms cooldown with 50ms tolerance. +- Added tests for queued task return values, error propagation when catchErrors is false, and error swallowing behavior when catchErrors is true. +- Added concurrency and cooldown interaction tests to ensure maxConcurrent is respected and batch timing is correct. +- Added test verifying removing a constraint group unblocks queued tasks and drain behavior completes correctly. + ## 2026-02-15 - 5.0.0 - BREAKING CHANGE(taskbuffer) Introduce constraint-based concurrency with TaskConstraintGroup and TaskManager integration; remove legacy TaskRunner and several Task APIs (breaking); add typed Task.data and update exports and tests. diff --git a/test/test.13.constraints.ts b/test/test.13.constraints.ts index a22232c..c90c81d 100644 --- a/test/test.13.constraints.ts +++ b/test/test.13.constraints.ts @@ -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(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 56ca4f4..959a6f8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/taskbuffer', - version: '5.0.0', + version: '5.0.1', description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.' } diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 56ca4f4..959a6f8 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/taskbuffer', - version: '5.0.0', + version: '5.0.1', description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.' }