@bluelibs/runner 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +482 -34
- package/dist/cli/extract-docs.d.ts +2 -0
- package/dist/cli/extract-docs.js +88 -0
- package/dist/cli/extract-docs.js.map +1 -0
- package/dist/define.d.ts +21 -1
- package/dist/define.js +71 -0
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +163 -4
- package/dist/defs.js +30 -0
- package/dist/defs.js.map +1 -1
- package/dist/docs/introspect.d.ts +7 -0
- package/dist/docs/introspect.js +199 -0
- package/dist/docs/introspect.js.map +1 -0
- package/dist/docs/markdown.d.ts +2 -0
- package/dist/docs/markdown.js +148 -0
- package/dist/docs/markdown.js.map +1 -0
- package/dist/docs/model.d.ts +62 -0
- package/dist/docs/model.js +33 -0
- package/dist/docs/model.js.map +1 -0
- package/dist/express/docsRouter.d.ts +12 -0
- package/dist/express/docsRouter.js +54 -0
- package/dist/express/docsRouter.js.map +1 -0
- package/dist/globals/globalMiddleware.d.ts +1 -0
- package/dist/globals/globalMiddleware.js +2 -0
- package/dist/globals/globalMiddleware.js.map +1 -1
- package/dist/globals/middleware/timeout.middleware.d.ts +8 -0
- package/dist/globals/middleware/timeout.middleware.js +35 -0
- package/dist/globals/middleware/timeout.middleware.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/models/DependencyProcessor.js +2 -2
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/Store.d.ts +1 -1
- package/dist/models/StoreConstants.d.ts +1 -1
- package/dist/models/StoreConstants.js +2 -1
- package/dist/models/StoreConstants.js.map +1 -1
- package/dist/models/TaskRunner.d.ts +2 -3
- package/dist/models/TaskRunner.js +1 -2
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/testing.d.ts +24 -0
- package/dist/testing.js +41 -0
- package/dist/testing.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/benchmark/task-benchmark.test.ts +132 -0
- package/src/__tests__/createTestResource.test.ts +139 -0
- package/src/__tests__/globals/timeout.middleware.test.ts +88 -0
- package/src/__tests__/models/Semaphore.test.ts +1 -1
- package/src/__tests__/override.test.ts +104 -0
- package/src/__tests__/run.overrides.test.ts +50 -21
- package/src/__tests__/run.test.ts +19 -0
- package/src/__tests__/tags.test.ts +396 -0
- package/src/__tests__/typesafety.test.ts +109 -1
- package/src/define.ts +97 -0
- package/src/defs.ts +168 -8
- package/src/globals/globalMiddleware.ts +2 -0
- package/src/globals/middleware/timeout.middleware.ts +46 -0
- package/src/index.ts +6 -0
- package/src/models/DependencyProcessor.ts +2 -10
- package/src/models/StoreConstants.ts +2 -1
- package/src/models/TaskRunner.ts +1 -3
- package/src/testing.ts +66 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { definitions, task, resource, middleware, override } from "..";
|
|
2
|
+
|
|
3
|
+
describe("override() helper", () => {
|
|
4
|
+
it("should preserve id and override run for tasks", async () => {
|
|
5
|
+
const base = task({
|
|
6
|
+
id: "test.task",
|
|
7
|
+
run: async () => "base",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const changed = override(base, {
|
|
11
|
+
run: async () => "changed",
|
|
12
|
+
meta: { title: "Updated" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(changed).not.toBe(base);
|
|
16
|
+
expect(changed.id).toBe(base.id);
|
|
17
|
+
expect(await base.run(undefined as any, {} as any)).toBe("base");
|
|
18
|
+
expect(await changed.run(undefined as any, {} as any)).toBe("changed");
|
|
19
|
+
expect(changed.meta?.title).toBe("Updated");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should preserve id and override init for resources", async () => {
|
|
23
|
+
const base = resource({
|
|
24
|
+
id: "test.resource",
|
|
25
|
+
init: async () => 1,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const changed = override(base, {
|
|
29
|
+
init: async () => 2,
|
|
30
|
+
meta: { description: "Updated" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(changed).not.toBe(base);
|
|
34
|
+
expect(changed.id).toBe(base.id);
|
|
35
|
+
// Call the init functions directly (without runner) to validate override
|
|
36
|
+
// Signatures: init(config, deps, ctx)
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
38
|
+
const v1 = await base.init!(undefined as any, {} as any, undefined as any);
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
40
|
+
const v2 = await changed.init!(
|
|
41
|
+
undefined as any,
|
|
42
|
+
{} as any,
|
|
43
|
+
undefined as any
|
|
44
|
+
);
|
|
45
|
+
expect(v1).toBe(1);
|
|
46
|
+
expect(v2).toBe(2);
|
|
47
|
+
expect(changed.meta?.description).toBe("Updated");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should preserve id and override run for middleware", async () => {
|
|
51
|
+
const mw = middleware({
|
|
52
|
+
id: "test.middleware",
|
|
53
|
+
run: async ({ next }) => {
|
|
54
|
+
return next();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const changed = override(mw, {
|
|
59
|
+
run: async ({ task, next }) => {
|
|
60
|
+
const result = await next(task?.input as any);
|
|
61
|
+
return { wrapped: result } as any;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(changed).not.toBe(mw);
|
|
66
|
+
expect(changed.id).toBe(mw.id);
|
|
67
|
+
|
|
68
|
+
const input = {
|
|
69
|
+
task: { definition: undefined as any, input: 123 },
|
|
70
|
+
next: async () => 456,
|
|
71
|
+
} as definitions.IMiddlewareExecutionInput<any, any>;
|
|
72
|
+
|
|
73
|
+
const baseResult = await mw.run(input, {} as any, undefined as any);
|
|
74
|
+
const changedResult = await changed.run(input, {} as any, undefined as any);
|
|
75
|
+
expect(baseResult).toBe(456);
|
|
76
|
+
expect(changedResult).toEqual({ wrapped: 456 });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should be type-safe: cannot override id on task/resource/middleware", () => {
|
|
80
|
+
const t = task({ id: "tt", run: async () => undefined });
|
|
81
|
+
const r = resource({ id: "rr", init: async () => undefined });
|
|
82
|
+
const m = middleware({ id: "mm", run: async ({ next }) => next() });
|
|
83
|
+
|
|
84
|
+
// @ts-expect-error id cannot be overridden
|
|
85
|
+
override(t, { id: "new" });
|
|
86
|
+
|
|
87
|
+
// @ts-expect-error id cannot be overridden
|
|
88
|
+
override(r, { id: "new" });
|
|
89
|
+
|
|
90
|
+
// @ts-expect-error id cannot be overridden
|
|
91
|
+
override(m, { id: "new" });
|
|
92
|
+
|
|
93
|
+
expect(true).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should handle undefined patch (robustness)", () => {
|
|
97
|
+
const base = task({ id: "robust.task", run: async () => 1 });
|
|
98
|
+
const changed = (override as any)(base, undefined);
|
|
99
|
+
|
|
100
|
+
expect(changed).not.toBe(base);
|
|
101
|
+
expect(changed.id).toBe(base.id);
|
|
102
|
+
expect(changed.run).toBe(base.run);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
defineResource,
|
|
5
5
|
defineEvent,
|
|
6
6
|
defineMiddleware,
|
|
7
|
+
defineOverride,
|
|
7
8
|
} from "../define";
|
|
8
9
|
import { Errors } from "../errors";
|
|
9
10
|
import { run } from "../run";
|
|
@@ -16,8 +17,7 @@ describe("run.overrides", () => {
|
|
|
16
17
|
run: async () => "Task executed",
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
id: "task",
|
|
20
|
+
const overrideTask = defineOverride(task, {
|
|
21
21
|
run: async () => "Task overridden",
|
|
22
22
|
});
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ describe("run.overrides", () => {
|
|
|
27
27
|
dependencies: {
|
|
28
28
|
task,
|
|
29
29
|
},
|
|
30
|
-
overrides: [
|
|
30
|
+
overrides: [overrideTask],
|
|
31
31
|
async init(_, deps) {
|
|
32
32
|
return await deps.task();
|
|
33
33
|
},
|
|
@@ -43,15 +43,14 @@ describe("run.overrides", () => {
|
|
|
43
43
|
run: async () => "Task executed",
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
id: "task",
|
|
46
|
+
const overrideTask = defineOverride(task, {
|
|
48
47
|
run: async () => "Task overridden",
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
const middle = defineResource({
|
|
52
51
|
id: "app",
|
|
53
52
|
register: [task],
|
|
54
|
-
overrides: [
|
|
53
|
+
overrides: [overrideTask],
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
const root = defineResource({
|
|
@@ -73,15 +72,14 @@ describe("run.overrides", () => {
|
|
|
73
72
|
run: async () => "Task executed",
|
|
74
73
|
});
|
|
75
74
|
|
|
76
|
-
const
|
|
77
|
-
id: "task",
|
|
75
|
+
const overrideTask = defineOverride(task, {
|
|
78
76
|
run: async () => "Task overridden",
|
|
79
77
|
});
|
|
80
78
|
|
|
81
79
|
const middle = defineResource<{ test: string }>({
|
|
82
80
|
id: "app",
|
|
83
81
|
register: [task],
|
|
84
|
-
overrides: [
|
|
82
|
+
overrides: [overrideTask],
|
|
85
83
|
});
|
|
86
84
|
|
|
87
85
|
const root = defineResource({
|
|
@@ -103,8 +101,7 @@ describe("run.overrides", () => {
|
|
|
103
101
|
run: async () => "Task executed",
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
const
|
|
107
|
-
id: "task",
|
|
104
|
+
const overrideTask = defineOverride(task, {
|
|
108
105
|
run: async () => "Task overridden",
|
|
109
106
|
});
|
|
110
107
|
|
|
@@ -112,10 +109,9 @@ describe("run.overrides", () => {
|
|
|
112
109
|
id: "resource",
|
|
113
110
|
});
|
|
114
111
|
|
|
115
|
-
const resourceOverride = {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
112
|
+
const resourceOverride = defineOverride(resource, {
|
|
113
|
+
overrides: [overrideTask],
|
|
114
|
+
});
|
|
119
115
|
|
|
120
116
|
const middle = defineResource({
|
|
121
117
|
id: "app",
|
|
@@ -142,8 +138,7 @@ describe("run.overrides", () => {
|
|
|
142
138
|
run: async () => "Task executed",
|
|
143
139
|
});
|
|
144
140
|
|
|
145
|
-
const
|
|
146
|
-
id: "task",
|
|
141
|
+
const overrideTask = defineOverride(task, {
|
|
147
142
|
run: async () => "Task overridden",
|
|
148
143
|
});
|
|
149
144
|
|
|
@@ -153,7 +148,7 @@ describe("run.overrides", () => {
|
|
|
153
148
|
|
|
154
149
|
const resourceOverride: definitions.IResource<any> = {
|
|
155
150
|
...resource,
|
|
156
|
-
overrides: [
|
|
151
|
+
overrides: [overrideTask],
|
|
157
152
|
async init(config: { test: string }) {
|
|
158
153
|
return "Resource init";
|
|
159
154
|
},
|
|
@@ -186,8 +181,7 @@ describe("run.overrides", () => {
|
|
|
186
181
|
},
|
|
187
182
|
});
|
|
188
183
|
|
|
189
|
-
const
|
|
190
|
-
id: "middleware",
|
|
184
|
+
const middlewareOverride = defineOverride(middleware, {
|
|
191
185
|
run: async ({ next }) => {
|
|
192
186
|
return `Override: ${await next()}`;
|
|
193
187
|
},
|
|
@@ -203,7 +197,7 @@ describe("run.overrides", () => {
|
|
|
203
197
|
id: "resource",
|
|
204
198
|
register: [middleware, task],
|
|
205
199
|
dependencies: { task },
|
|
206
|
-
overrides: [
|
|
200
|
+
overrides: [middlewareOverride],
|
|
207
201
|
async init(_, deps) {
|
|
208
202
|
return deps.task();
|
|
209
203
|
},
|
|
@@ -392,4 +386,39 @@ describe("run.overrides", () => {
|
|
|
392
386
|
const result = await run(app);
|
|
393
387
|
expect(result.value).toBe("Task overriden.");
|
|
394
388
|
});
|
|
389
|
+
|
|
390
|
+
it("should choose precedence when two overrides target the same id", async () => {
|
|
391
|
+
const baseTask = defineTask({
|
|
392
|
+
id: "task.same",
|
|
393
|
+
run: async () => "Original",
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const middleOverride = defineOverride(baseTask, {
|
|
397
|
+
run: async () => "Middle",
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const rootOverride = defineOverride(baseTask, {
|
|
401
|
+
run: async () => "Root",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const middle = defineResource({
|
|
405
|
+
id: "middle",
|
|
406
|
+
register: [baseTask],
|
|
407
|
+
overrides: [middleOverride],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const app = defineResource({
|
|
411
|
+
id: "app",
|
|
412
|
+
register: [middle],
|
|
413
|
+
dependencies: { t: baseTask },
|
|
414
|
+
overrides: [rootOverride],
|
|
415
|
+
async init(_, deps) {
|
|
416
|
+
return await deps.t();
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const result = await run(app);
|
|
421
|
+
// Since root is visited after middle, its override takes precedence.
|
|
422
|
+
expect(result.value).toBe("Root");
|
|
423
|
+
});
|
|
395
424
|
});
|
|
@@ -16,12 +16,17 @@ describe("main exports", () => {
|
|
|
16
16
|
expect(typeof mainExports.resource).toBe("function");
|
|
17
17
|
expect(typeof mainExports.event).toBe("function");
|
|
18
18
|
expect(typeof mainExports.middleware).toBe("function");
|
|
19
|
+
expect(typeof mainExports.index).toBe("function");
|
|
20
|
+
expect(typeof mainExports.tag).toBe("function");
|
|
19
21
|
expect(typeof mainExports.run).toBe("function");
|
|
22
|
+
expect(typeof mainExports.createContext).toBe("function");
|
|
20
23
|
expect(typeof mainExports.globals).toBe("object");
|
|
21
24
|
expect(typeof mainExports.definitions).toBe("object");
|
|
22
25
|
expect(typeof mainExports.Store).toBe("function");
|
|
23
26
|
expect(typeof mainExports.EventManager).toBe("function");
|
|
24
27
|
expect(typeof mainExports.TaskRunner).toBe("function");
|
|
28
|
+
expect(typeof mainExports.Queue).toBe("function");
|
|
29
|
+
expect(typeof mainExports.Semaphore).toBe("function");
|
|
25
30
|
|
|
26
31
|
// Test that aliases work the same as direct imports
|
|
27
32
|
const directTask = defineTask({ id: "test", run: async () => "direct" });
|
|
@@ -33,6 +38,20 @@ describe("main exports", () => {
|
|
|
33
38
|
expect(directTask.id).toBe("test");
|
|
34
39
|
expect(aliasTask.id).toBe("test2");
|
|
35
40
|
|
|
41
|
+
// Test tag exports work
|
|
42
|
+
const testTag = mainExports.tag<{ value: number }>({ id: "test.tag" });
|
|
43
|
+
const testTag2 = mainExports.tag<{ name: string }>({ id: "test.tag2" });
|
|
44
|
+
|
|
45
|
+
expect(testTag.id).toBe("test.tag");
|
|
46
|
+
expect(testTag2.id).toBe("test.tag2");
|
|
47
|
+
expect(typeof testTag.with).toBe("function");
|
|
48
|
+
expect(typeof testTag2.extract).toBe("function");
|
|
49
|
+
|
|
50
|
+
// Test createContext export
|
|
51
|
+
const TestContext = mainExports.createContext<string>("test.context");
|
|
52
|
+
expect(typeof TestContext.provide).toBe("function");
|
|
53
|
+
expect(typeof TestContext.use).toBe("function");
|
|
54
|
+
|
|
36
55
|
// Test globals sub-properties for complete coverage
|
|
37
56
|
expect(typeof mainExports.globals.events).toBe("object");
|
|
38
57
|
expect(typeof mainExports.globals.resources).toBe("object");
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTag,
|
|
3
|
+
defineTask,
|
|
4
|
+
defineResource,
|
|
5
|
+
defineEvent,
|
|
6
|
+
defineMiddleware,
|
|
7
|
+
} from "../define";
|
|
8
|
+
import { run } from "../run";
|
|
9
|
+
|
|
10
|
+
describe("Configurable Tags", () => {
|
|
11
|
+
describe("Tag Definition", () => {
|
|
12
|
+
it("should create a tag with string id", () => {
|
|
13
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
14
|
+
id: "performance.track",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(performanceTag.id).toBe("performance.track");
|
|
18
|
+
expect(typeof performanceTag.with).toBe("function");
|
|
19
|
+
expect(typeof performanceTag.extract).toBe("function");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should create a tag with symbol id", () => {
|
|
23
|
+
const symbolId = Symbol("test.tag");
|
|
24
|
+
const testTag = defineTag<{ value: string }>({ id: symbolId });
|
|
25
|
+
|
|
26
|
+
expect(testTag.id).toBe(symbolId);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should create a tag without configuration", () => {
|
|
30
|
+
const simpleTag = defineTag({ id: "simple.tag" });
|
|
31
|
+
|
|
32
|
+
expect(simpleTag.id).toBe("simple.tag");
|
|
33
|
+
expect(typeof simpleTag.with).toBe("function");
|
|
34
|
+
expect(typeof simpleTag.extract).toBe("function");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("Tag Configuration with .with()", () => {
|
|
39
|
+
it("should create a configured tag instance", () => {
|
|
40
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
41
|
+
id: "performance.track",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const configuredTag = performanceTag.with({ alertAboveMs: 200 });
|
|
45
|
+
|
|
46
|
+
expect(configuredTag.id).toBe("performance.track");
|
|
47
|
+
expect(configuredTag.config).toEqual({ alertAboveMs: 200 });
|
|
48
|
+
expect(configuredTag.tag).toBe(performanceTag);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should allow multiple configurations of the same tag", () => {
|
|
52
|
+
const cacheTag = defineTag<{ ttl: number }>({ id: "cache.config" });
|
|
53
|
+
|
|
54
|
+
const shortCache = cacheTag.with({ ttl: 300 });
|
|
55
|
+
const longCache = cacheTag.with({ ttl: 3600 });
|
|
56
|
+
|
|
57
|
+
expect(shortCache.config.ttl).toBe(300);
|
|
58
|
+
expect(longCache.config.ttl).toBe(3600);
|
|
59
|
+
expect(shortCache.tag).toBe(cacheTag);
|
|
60
|
+
expect(longCache.tag).toBe(cacheTag);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("Tag Extraction with .extract()", () => {
|
|
65
|
+
it("should extract configured tag from tags array", () => {
|
|
66
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
67
|
+
id: "performance.track",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const tags = [
|
|
71
|
+
"simple-string-tag",
|
|
72
|
+
performanceTag.with({ alertAboveMs: 200 }),
|
|
73
|
+
"another-string",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const extracted = performanceTag.extract(tags);
|
|
77
|
+
|
|
78
|
+
expect(extracted).not.toBeNull();
|
|
79
|
+
expect(extracted?.id).toBe("performance.track");
|
|
80
|
+
expect(extracted?.config).toEqual({ alertAboveMs: 200 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should extract unconfigured tag from tags array", () => {
|
|
84
|
+
const simpleTag = defineTag({ id: "simple.tag" });
|
|
85
|
+
|
|
86
|
+
const tags = ["string-tag", simpleTag, "another-string"];
|
|
87
|
+
|
|
88
|
+
const extracted = simpleTag.extract(tags);
|
|
89
|
+
|
|
90
|
+
expect(extracted).not.toBeNull();
|
|
91
|
+
expect(extracted?.id).toBe("simple.tag");
|
|
92
|
+
expect(extracted?.config).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return null if tag not found", () => {
|
|
96
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
97
|
+
id: "performance.track",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const tags = ["string-tag", "another-string"];
|
|
101
|
+
|
|
102
|
+
const extracted = performanceTag.extract(tags);
|
|
103
|
+
|
|
104
|
+
expect(extracted).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should ignore string tags during extraction", () => {
|
|
108
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
109
|
+
id: "performance.track",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const tags = [
|
|
113
|
+
"performance.track", // This is a string, not the tag
|
|
114
|
+
performanceTag.with({ alertAboveMs: 100 }),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const extracted = performanceTag.extract(tags);
|
|
118
|
+
|
|
119
|
+
expect(extracted).not.toBeNull();
|
|
120
|
+
expect(extracted?.config).toEqual({ alertAboveMs: 100 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should handle symbol ids correctly", () => {
|
|
124
|
+
const symbolId = Symbol("test.tag");
|
|
125
|
+
const testTag = defineTag<{ data: string }>({ id: symbolId });
|
|
126
|
+
|
|
127
|
+
const tags = [testTag.with({ data: "test" })];
|
|
128
|
+
|
|
129
|
+
const extracted = testTag.extract(tags);
|
|
130
|
+
|
|
131
|
+
expect(extracted).not.toBeNull();
|
|
132
|
+
expect(extracted?.id).toBe(symbolId);
|
|
133
|
+
expect(extracted?.config).toEqual({ data: "test" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should extract configured tag from a taggable object (task.definition)", () => {
|
|
137
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
138
|
+
id: "performance.track",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const task = defineTask({
|
|
142
|
+
id: "task.with.tags",
|
|
143
|
+
meta: {
|
|
144
|
+
tags: [performanceTag.with({ alertAboveMs: 123 })],
|
|
145
|
+
},
|
|
146
|
+
run: async () => "ok",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const extracted = performanceTag.extract(task);
|
|
150
|
+
expect(extracted).not.toBeNull();
|
|
151
|
+
expect(extracted?.config).toEqual({ alertAboveMs: 123 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should return null when taggable has no tags", () => {
|
|
155
|
+
const t = defineTag({ id: "x" });
|
|
156
|
+
const task = defineTask({ id: "no.tags", run: async () => "ok" });
|
|
157
|
+
expect(t.extract(task)).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should work with a simple taggable carrying meta.tags directly", () => {
|
|
161
|
+
const t = defineTag<{ p: number }>({ id: "pp" });
|
|
162
|
+
const taggable = { meta: { tags: [t.with({ p: 9 })] } } as any;
|
|
163
|
+
const extracted = t.extract(taggable);
|
|
164
|
+
expect(extracted?.config).toEqual({ p: 9 });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("Integration with Tasks", () => {
|
|
169
|
+
it("should work with task metadata", () => {
|
|
170
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
171
|
+
id: "performance.track",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const testTask = defineTask({
|
|
175
|
+
id: "test.task",
|
|
176
|
+
meta: {
|
|
177
|
+
tags: [
|
|
178
|
+
"api",
|
|
179
|
+
performanceTag.with({ alertAboveMs: 200 }),
|
|
180
|
+
"important",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
run: async () => {
|
|
184
|
+
return "success";
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(testTask.meta?.tags).toHaveLength(3);
|
|
189
|
+
expect(testTask.meta?.tags?.[0]).toBe("api");
|
|
190
|
+
expect(testTask.meta?.tags?.[2]).toBe("important");
|
|
191
|
+
|
|
192
|
+
const extracted = performanceTag.extract(testTask.meta?.tags || []);
|
|
193
|
+
expect(extracted).not.toBeNull();
|
|
194
|
+
expect(extracted?.config).toEqual({ alertAboveMs: 200 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should work with middleware checking tags", async () => {
|
|
198
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
199
|
+
id: "performance.track",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const middlewareExecutions: Array<{ taskId: string; config: any }> = [];
|
|
203
|
+
|
|
204
|
+
const performanceMiddleware = defineMiddleware({
|
|
205
|
+
id: "performance.middleware",
|
|
206
|
+
run: async ({ task, next }) => {
|
|
207
|
+
if (task?.definition.meta?.tags) {
|
|
208
|
+
const extracted = performanceTag.extract(task.definition.meta.tags);
|
|
209
|
+
if (extracted) {
|
|
210
|
+
middlewareExecutions.push({
|
|
211
|
+
taskId: task.definition.id as string,
|
|
212
|
+
config: extracted.config,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return next(task?.input);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const fastTask = defineTask({
|
|
221
|
+
id: "fast.task",
|
|
222
|
+
meta: {
|
|
223
|
+
tags: [performanceTag.with({ alertAboveMs: 100 })],
|
|
224
|
+
},
|
|
225
|
+
run: async () => "fast",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const slowTask = defineTask({
|
|
229
|
+
id: "slow.task",
|
|
230
|
+
meta: {
|
|
231
|
+
tags: [performanceTag.with({ alertAboveMs: 500 })],
|
|
232
|
+
},
|
|
233
|
+
run: async () => "slow",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const normalTask = defineTask({
|
|
237
|
+
id: "normal.task",
|
|
238
|
+
meta: {
|
|
239
|
+
tags: ["just-a-string"],
|
|
240
|
+
},
|
|
241
|
+
run: async () => "normal",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const app = defineResource({
|
|
245
|
+
id: "test.app",
|
|
246
|
+
register: [
|
|
247
|
+
fastTask,
|
|
248
|
+
slowTask,
|
|
249
|
+
normalTask,
|
|
250
|
+
performanceMiddleware.everywhere(),
|
|
251
|
+
],
|
|
252
|
+
dependencies: { fastTask, slowTask, normalTask },
|
|
253
|
+
init: async (_, { fastTask, slowTask, normalTask }) => {
|
|
254
|
+
await fastTask();
|
|
255
|
+
await slowTask();
|
|
256
|
+
await normalTask();
|
|
257
|
+
return "done";
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { dispose } = await run(app);
|
|
262
|
+
await dispose();
|
|
263
|
+
|
|
264
|
+
expect(middlewareExecutions).toHaveLength(2);
|
|
265
|
+
expect(middlewareExecutions).toEqual([
|
|
266
|
+
{ taskId: "fast.task", config: { alertAboveMs: 100 } },
|
|
267
|
+
{ taskId: "slow.task", config: { alertAboveMs: 500 } },
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("Integration with Resources", () => {
|
|
273
|
+
it("should work with resource metadata", () => {
|
|
274
|
+
const dbTag = defineTag<{ connectionTimeout: number }>({
|
|
275
|
+
id: "db.config",
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const database = defineResource({
|
|
279
|
+
id: "database",
|
|
280
|
+
meta: {
|
|
281
|
+
tags: ["database", dbTag.with({ connectionTimeout: 5000 })],
|
|
282
|
+
},
|
|
283
|
+
init: async () => ({ query: () => "result" }),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(database.meta?.tags).toHaveLength(2);
|
|
287
|
+
const extracted = dbTag.extract(database.meta?.tags || []);
|
|
288
|
+
expect(extracted?.config).toEqual({ connectionTimeout: 5000 });
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("Integration with Events", () => {
|
|
293
|
+
it("should work with event metadata", () => {
|
|
294
|
+
const auditTag = defineTag<{ sensitive: boolean }>({
|
|
295
|
+
id: "audit.config",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const userEvent = defineEvent<{ userId: string }>({
|
|
299
|
+
id: "user.created",
|
|
300
|
+
meta: {
|
|
301
|
+
tags: ["user-event", auditTag.with({ sensitive: true })],
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(userEvent.meta?.tags).toHaveLength(2);
|
|
306
|
+
const extracted = auditTag.extract(userEvent.meta?.tags || []);
|
|
307
|
+
expect(extracted?.config).toEqual({ sensitive: true });
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("Integration with Middleware", () => {
|
|
312
|
+
it("should work with middleware metadata", () => {
|
|
313
|
+
const rateLimitTag = defineTag<{ requestsPerMinute: number }>({
|
|
314
|
+
id: "rate-limit",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const rateLimitMiddleware = defineMiddleware({
|
|
318
|
+
id: "rate.limit.middleware",
|
|
319
|
+
meta: {
|
|
320
|
+
tags: ["security", rateLimitTag.with({ requestsPerMinute: 60 })],
|
|
321
|
+
},
|
|
322
|
+
run: async ({ next, task }) => {
|
|
323
|
+
return next(task?.input);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(rateLimitMiddleware.meta?.tags).toHaveLength(2);
|
|
328
|
+
const extracted = rateLimitTag.extract(
|
|
329
|
+
rateLimitMiddleware.meta?.tags || []
|
|
330
|
+
);
|
|
331
|
+
expect(extracted?.config).toEqual({ requestsPerMinute: 60 });
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("Backward Compatibility", () => {
|
|
336
|
+
it("should work with existing string tags", () => {
|
|
337
|
+
const task = defineTask({
|
|
338
|
+
id: "legacy.task",
|
|
339
|
+
meta: {
|
|
340
|
+
tags: ["api", "legacy", "important"],
|
|
341
|
+
},
|
|
342
|
+
run: async () => "success",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// String tags should still work
|
|
346
|
+
expect(task.meta?.tags).toEqual(["api", "legacy", "important"]);
|
|
347
|
+
|
|
348
|
+
// New tags should not interfere with string tags
|
|
349
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
350
|
+
id: "performance.track",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const extracted = performanceTag.extract(task.meta?.tags || []);
|
|
354
|
+
expect(extracted).toBeNull(); // Should not find the tag
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should allow mixing string tags and configurable tags", () => {
|
|
358
|
+
const performanceTag = defineTag<{ alertAboveMs: number }>({
|
|
359
|
+
id: "performance.track",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const task = defineTask({
|
|
363
|
+
id: "mixed.task",
|
|
364
|
+
meta: {
|
|
365
|
+
tags: [
|
|
366
|
+
"api", // string tag
|
|
367
|
+
performanceTag.with({ alertAboveMs: 200 }), // configurable tag
|
|
368
|
+
"important", // string tag
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
run: async () => "success",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
expect(task.meta?.tags).toHaveLength(3);
|
|
375
|
+
expect(task.meta?.tags?.[0]).toBe("api");
|
|
376
|
+
expect(task.meta?.tags?.[2]).toBe("important");
|
|
377
|
+
|
|
378
|
+
const extracted = performanceTag.extract(task.meta?.tags || []);
|
|
379
|
+
expect(extracted?.config).toEqual({ alertAboveMs: 200 });
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("Edge Cases", () => {
|
|
384
|
+
it("should handle null/undefined config", () => {
|
|
385
|
+
const optionalTag = defineTag<{ value?: string }>({
|
|
386
|
+
id: "optional.config",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const configuredTag = optionalTag.with({});
|
|
390
|
+
expect(configuredTag.config).toEqual({});
|
|
391
|
+
|
|
392
|
+
const extracted = optionalTag.extract([configuredTag]);
|
|
393
|
+
expect(extracted?.config).toEqual({});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|