250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
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();
|