import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as taskbuffer from '../ts/index.js'; import type { ITaskEvent } from '../ts/index.js'; // ─── Labels ─── tap.test('should accept labels in constructor', async () => { const task = new taskbuffer.Task({ name: 'labelled-task', taskFunction: async () => 'ok', labels: { userId: 'u1', tenantId: 't1' }, }); expect(task.labels).toEqual({ userId: 'u1', tenantId: 't1' }); }); tap.test('should default labels to empty object', async () => { const task = new taskbuffer.Task({ name: 'no-labels-task', taskFunction: async () => 'ok', }); expect(task.labels).toEqual({}); }); tap.test('setLabel / getLabel / removeLabel / hasLabel should work', async () => { const task = new taskbuffer.Task({ name: 'label-helpers-task', taskFunction: async () => 'ok', }); task.setLabel('env', 'prod'); expect(task.getLabel('env')).toEqual('prod'); expect(task.hasLabel('env')).toBeTrue(); expect(task.hasLabel('env', 'prod')).toBeTrue(); expect(task.hasLabel('env', 'dev')).toBeFalse(); expect(task.hasLabel('missing')).toBeFalse(); const removed = task.removeLabel('env'); expect(removed).toBeTrue(); expect(task.getLabel('env')).toBeUndefined(); const removedAgain = task.removeLabel('env'); expect(removedAgain).toBeFalse(); }); tap.test('getMetadata() should include labels', async () => { const task = new taskbuffer.Task({ name: 'metadata-labels-task', taskFunction: async () => 'ok', labels: { region: 'eu' }, }); const meta = task.getMetadata(); expect(meta.labels).toEqual({ region: 'eu' }); // Returned labels should be a copy meta.labels!['region'] = 'us'; expect(task.labels['region']).toEqual('eu'); }); tap.test('TaskManager.getTasksByLabel should filter correctly', async () => { const manager = new taskbuffer.TaskManager(); const t1 = new taskbuffer.Task({ name: 'label-filter-1', taskFunction: async () => 'ok', labels: { userId: 'alice' }, }); const t2 = new taskbuffer.Task({ name: 'label-filter-2', taskFunction: async () => 'ok', labels: { userId: 'bob' }, }); const t3 = new taskbuffer.Task({ name: 'label-filter-3', taskFunction: async () => 'ok', labels: { userId: 'alice' }, }); manager.addTask(t1); manager.addTask(t2); manager.addTask(t3); const aliceTasks = manager.getTasksByLabel('userId', 'alice'); expect(aliceTasks.length).toEqual(2); expect(aliceTasks.map((t) => t.name).sort()).toEqual(['label-filter-1', 'label-filter-3']); const bobMeta = manager.getTasksMetadataByLabel('userId', 'bob'); expect(bobMeta.length).toEqual(1); expect(bobMeta[0].name).toEqual('label-filter-2'); await manager.stop(); }); // ─── Events ─── tap.test('should emit started + completed on successful trigger', async () => { const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'event-success-task', taskFunction: async () => 'ok', }); task.eventSubject.subscribe((e) => events.push(e)); await task.trigger(); expect(events.length).toEqual(2); expect(events[0].type).toEqual('started'); expect(events[1].type).toEqual('completed'); expect(events[0].task.name).toEqual('event-success-task'); expect(typeof events[0].timestamp).toEqual('number'); }); tap.test('should emit step events on notifyStep', async () => { const steps = [ { name: 'build', description: 'Build artifacts', percentage: 50 }, { name: 'deploy', description: 'Deploy to prod', percentage: 50 }, ] as const; const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'step-event-task', steps, taskFunction: async () => { task.notifyStep('build'); task.notifyStep('deploy'); return 'done'; }, }); task.eventSubject.subscribe((e) => events.push(e)); await task.trigger(); const stepEvents = events.filter((e) => e.type === 'step'); expect(stepEvents.length).toEqual(2); expect(stepEvents[0].stepName).toEqual('build'); expect(stepEvents[1].stepName).toEqual('deploy'); }); tap.test('should emit started + failed on error', async () => { const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'event-fail-task', taskFunction: async () => { throw new Error('boom'); }, }); task.eventSubject.subscribe((e) => events.push(e)); try { await task.trigger(); } catch { // expected } expect(events.length).toEqual(2); expect(events[0].type).toEqual('started'); expect(events[1].type).toEqual('failed'); expect(events[1].error).toEqual('boom'); }); tap.test('should emit failed via done.then path when catchErrors is true', async () => { const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'event-catch-fail-task', catchErrors: true, taskFunction: async () => { throw new Error('swallowed'); }, }); task.eventSubject.subscribe((e) => events.push(e)); await task.trigger(); const types = events.map((e) => e.type); expect(types).toContain('started'); expect(types).toContain('failed'); }); tap.test('TaskManager.taskSubject should aggregate events from added tasks', async () => { const manager = new taskbuffer.TaskManager(); const events: ITaskEvent[] = []; const t1 = new taskbuffer.Task({ name: 'agg-task-1', taskFunction: async () => 'a', }); const t2 = new taskbuffer.Task({ name: 'agg-task-2', taskFunction: async () => 'b', }); manager.addTask(t1); manager.addTask(t2); manager.taskSubject.subscribe((e) => events.push(e)); await t1.trigger(); await t2.trigger(); const names = [...new Set(events.map((e) => e.task.name))]; expect(names.sort()).toEqual(['agg-task-1', 'agg-task-2']); expect(events.filter((e) => e.type === 'started').length).toEqual(2); expect(events.filter((e) => e.type === 'completed').length).toEqual(2); await manager.stop(); }); tap.test('events should stop after removeTask', async () => { const manager = new taskbuffer.TaskManager(); const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'removable-event-task', taskFunction: async () => 'ok', }); manager.addTask(task); manager.taskSubject.subscribe((e) => events.push(e)); await task.trigger(); const countBefore = events.length; expect(countBefore).toBeGreaterThan(0); manager.removeTask(task); // Trigger again — events should NOT appear on manager subject await task.trigger(); expect(events.length).toEqual(countBefore); await manager.stop(); }); tap.test('event metadata snapshots should include correct labels', async () => { const events: ITaskEvent[] = []; const task = new taskbuffer.Task({ name: 'labelled-event-task', taskFunction: async () => 'ok', labels: { team: 'platform' }, }); task.eventSubject.subscribe((e) => events.push(e)); await task.trigger(); for (const e of events) { expect(e.task.labels).toEqual({ team: 'platform' }); } }); export default tap.start();