@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.
Files changed (62) hide show
  1. package/README.md +482 -34
  2. package/dist/cli/extract-docs.d.ts +2 -0
  3. package/dist/cli/extract-docs.js +88 -0
  4. package/dist/cli/extract-docs.js.map +1 -0
  5. package/dist/define.d.ts +21 -1
  6. package/dist/define.js +71 -0
  7. package/dist/define.js.map +1 -1
  8. package/dist/defs.d.ts +163 -4
  9. package/dist/defs.js +30 -0
  10. package/dist/defs.js.map +1 -1
  11. package/dist/docs/introspect.d.ts +7 -0
  12. package/dist/docs/introspect.js +199 -0
  13. package/dist/docs/introspect.js.map +1 -0
  14. package/dist/docs/markdown.d.ts +2 -0
  15. package/dist/docs/markdown.js +148 -0
  16. package/dist/docs/markdown.js.map +1 -0
  17. package/dist/docs/model.d.ts +62 -0
  18. package/dist/docs/model.js +33 -0
  19. package/dist/docs/model.js.map +1 -0
  20. package/dist/express/docsRouter.d.ts +12 -0
  21. package/dist/express/docsRouter.js +54 -0
  22. package/dist/express/docsRouter.js.map +1 -0
  23. package/dist/globals/globalMiddleware.d.ts +1 -0
  24. package/dist/globals/globalMiddleware.js +2 -0
  25. package/dist/globals/globalMiddleware.js.map +1 -1
  26. package/dist/globals/middleware/timeout.middleware.d.ts +8 -0
  27. package/dist/globals/middleware/timeout.middleware.js +35 -0
  28. package/dist/globals/middleware/timeout.middleware.js.map +1 -0
  29. package/dist/index.d.ts +4 -2
  30. package/dist/index.js +5 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/models/DependencyProcessor.js +2 -2
  33. package/dist/models/DependencyProcessor.js.map +1 -1
  34. package/dist/models/Store.d.ts +1 -1
  35. package/dist/models/StoreConstants.d.ts +1 -1
  36. package/dist/models/StoreConstants.js +2 -1
  37. package/dist/models/StoreConstants.js.map +1 -1
  38. package/dist/models/TaskRunner.d.ts +2 -3
  39. package/dist/models/TaskRunner.js +1 -2
  40. package/dist/models/TaskRunner.js.map +1 -1
  41. package/dist/testing.d.ts +24 -0
  42. package/dist/testing.js +41 -0
  43. package/dist/testing.js.map +1 -0
  44. package/package.json +4 -4
  45. package/src/__tests__/benchmark/task-benchmark.test.ts +132 -0
  46. package/src/__tests__/createTestResource.test.ts +139 -0
  47. package/src/__tests__/globals/timeout.middleware.test.ts +88 -0
  48. package/src/__tests__/models/Semaphore.test.ts +1 -1
  49. package/src/__tests__/override.test.ts +104 -0
  50. package/src/__tests__/run.overrides.test.ts +50 -21
  51. package/src/__tests__/run.test.ts +19 -0
  52. package/src/__tests__/tags.test.ts +396 -0
  53. package/src/__tests__/typesafety.test.ts +109 -1
  54. package/src/define.ts +97 -0
  55. package/src/defs.ts +168 -8
  56. package/src/globals/globalMiddleware.ts +2 -0
  57. package/src/globals/middleware/timeout.middleware.ts +46 -0
  58. package/src/index.ts +6 -0
  59. package/src/models/DependencyProcessor.ts +2 -10
  60. package/src/models/StoreConstants.ts +2 -1
  61. package/src/models/TaskRunner.ts +1 -3
  62. 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 override = defineTask({
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: [override],
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 override = defineTask({
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: [override],
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 override = defineTask({
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: [override],
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 override = defineTask({
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
- ...resource,
117
- overrides: [override],
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 override = defineTask({
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: [override],
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 override = defineMiddleware({
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: [override],
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
+ });