@ai.ntellect/core 0.6.17 → 0.6.20

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 (79) hide show
  1. package/.mocharc.json +1 -2
  2. package/README.md +123 -178
  3. package/dist/graph/controller.js +29 -6
  4. package/dist/graph/index.js +302 -62
  5. package/dist/index.js +21 -6
  6. package/dist/interfaces/index.js +15 -0
  7. package/dist/modules/agenda/adapters/node-cron/index.js +29 -0
  8. package/dist/modules/agenda/index.js +140 -0
  9. package/dist/{services/embedding.js → modules/embedding/adapters/ai/index.js} +24 -7
  10. package/dist/modules/embedding/index.js +59 -0
  11. package/dist/modules/memory/adapters/in-memory/index.js +210 -0
  12. package/dist/{memory → modules/memory}/adapters/meilisearch/index.js +97 -2
  13. package/dist/{memory → modules/memory}/adapters/redis/index.js +77 -15
  14. package/dist/modules/memory/index.js +103 -0
  15. package/dist/utils/{stringifiy-zod-schema.js → generate-action-schema.js} +5 -5
  16. package/graph/controller.ts +46 -35
  17. package/graph/index.ts +534 -102
  18. package/graph.ts +74 -0
  19. package/index.ts +25 -7
  20. package/interfaces/index.ts +353 -27
  21. package/modules/agenda/adapters/node-cron/index.ts +25 -0
  22. package/modules/agenda/index.ts +159 -0
  23. package/modules/embedding/adapters/ai/index.ts +42 -0
  24. package/modules/embedding/index.ts +45 -0
  25. package/modules/memory/adapters/in-memory/index.ts +203 -0
  26. package/{memory → modules/memory}/adapters/meilisearch/index.ts +114 -12
  27. package/modules/memory/adapters/redis/index.ts +164 -0
  28. package/modules/memory/index.ts +93 -0
  29. package/package.json +3 -1
  30. package/test/graph/index.test.ts +578 -0
  31. package/test/modules/agenda/node-cron.test.ts +286 -0
  32. package/test/modules/embedding/ai.test.ts +78 -0
  33. package/test/modules/memory/adapters/in-memory.test.ts +153 -0
  34. package/test/{memory → modules/memory}/adapters/meilisearch.test.ts +79 -75
  35. package/test/modules/memory/adapters/redis.test.ts +169 -0
  36. package/test/modules/memory/base.test.ts +230 -0
  37. package/test/services/agenda.test.ts +279 -280
  38. package/types/index.ts +93 -202
  39. package/utils/{stringifiy-zod-schema.ts → generate-action-schema.ts} +3 -3
  40. package/app/README.md +0 -36
  41. package/app/app/favicon.ico +0 -0
  42. package/app/app/globals.css +0 -21
  43. package/app/app/gun.ts +0 -0
  44. package/app/app/layout.tsx +0 -18
  45. package/app/app/page.tsx +0 -321
  46. package/app/eslint.config.mjs +0 -16
  47. package/app/next.config.ts +0 -7
  48. package/app/package-lock.json +0 -5912
  49. package/app/package.json +0 -31
  50. package/app/pnpm-lock.yaml +0 -4031
  51. package/app/postcss.config.mjs +0 -8
  52. package/app/public/file.svg +0 -1
  53. package/app/public/globe.svg +0 -1
  54. package/app/public/next.svg +0 -1
  55. package/app/public/vercel.svg +0 -1
  56. package/app/public/window.svg +0 -1
  57. package/app/tailwind.config.ts +0 -18
  58. package/app/tsconfig.json +0 -27
  59. package/dist/memory/index.js +0 -9
  60. package/dist/services/agenda.js +0 -115
  61. package/dist/services/queue.js +0 -142
  62. package/dist/utils/experimental-graph-rag.js +0 -152
  63. package/dist/utils/generate-object.js +0 -111
  64. package/dist/utils/inject-actions.js +0 -16
  65. package/dist/utils/queue-item-transformer.js +0 -24
  66. package/dist/utils/sanitize-results.js +0 -60
  67. package/memory/adapters/redis/index.ts +0 -103
  68. package/memory/index.ts +0 -22
  69. package/services/agenda.ts +0 -118
  70. package/services/embedding.ts +0 -26
  71. package/services/queue.ts +0 -145
  72. package/test/memory/adapters/redis.test.ts +0 -159
  73. package/test/memory/base.test.ts +0 -225
  74. package/test/services/queue.test.ts +0 -286
  75. package/utils/experimental-graph-rag.ts +0 -170
  76. package/utils/generate-object.ts +0 -117
  77. package/utils/inject-actions.ts +0 -19
  78. package/utils/queue-item-transformer.ts +0 -38
  79. package/utils/sanitize-results.ts +0 -66
@@ -0,0 +1,93 @@
1
+ import { BaseMemoryType, CreateMemoryInput } from "types";
2
+ import { BaseMemory, IMemoryAdapter } from "../../interfaces";
3
+
4
+ /**
5
+ * @module Memory
6
+ * @description A module for managing memory storage and retrieval operations.
7
+ * Implements the BaseMemory abstract class and provides concrete implementations
8
+ * for memory-related operations using the provided adapter.
9
+ * @extends {BaseMemory}
10
+ */
11
+ export class Memory extends BaseMemory {
12
+ /**
13
+ * Creates an instance of Memory
14
+ * @param {IMemoryAdapter} adapter - The memory adapter implementation to use
15
+ */
16
+ constructor(adapter: IMemoryAdapter) {
17
+ super(adapter);
18
+ }
19
+
20
+ /**
21
+ * Initializes the memory module with default room
22
+ * @returns {Promise<void>}
23
+ */
24
+ async init(): Promise<void> {
25
+ await this.adapter.init("default");
26
+ }
27
+
28
+ /**
29
+ * Creates a new memory entry
30
+ * @param {CreateMemoryInput & { embedding?: number[] }} input - Memory data with optional embedding
31
+ * @returns {Promise<BaseMemoryType | undefined>} Created memory or undefined
32
+ */
33
+ async createMemory(
34
+ input: CreateMemoryInput & { embedding?: number[] }
35
+ ): Promise<BaseMemoryType | undefined> {
36
+ return this.adapter.createMemory(input);
37
+ }
38
+
39
+ /**
40
+ * Retrieves a memory by ID and room ID
41
+ * @param {string} id - Memory identifier
42
+ * @param {string} roomId - Room identifier
43
+ * @returns {Promise<BaseMemoryType | null>} Memory entry or null if not found
44
+ */
45
+ async getMemoryById(
46
+ id: string,
47
+ roomId: string
48
+ ): Promise<BaseMemoryType | null> {
49
+ return this.adapter.getMemoryById(id, roomId);
50
+ }
51
+
52
+ /**
53
+ * Searches for memories based on query and options
54
+ * @param {string} query - Search query
55
+ * @param {Object} options - Search options
56
+ * @param {string} options.roomId - Room identifier
57
+ * @param {number} [options.limit] - Maximum number of results to return
58
+ * @returns {Promise<BaseMemoryType[]>} Array of matching memories
59
+ */
60
+ async getMemoryByIndex(
61
+ query: string,
62
+ options: { roomId: string; limit?: number }
63
+ ): Promise<BaseMemoryType[]> {
64
+ return this.adapter.getMemoryByIndex(query, options);
65
+ }
66
+
67
+ /**
68
+ * Retrieves all memories for a specific room
69
+ * @param {string} roomId - Room identifier
70
+ * @returns {Promise<BaseMemoryType[]>} Array of all memories in the room
71
+ */
72
+ async getAllMemories(roomId: string): Promise<BaseMemoryType[]> {
73
+ return this.adapter.getAllMemories(roomId);
74
+ }
75
+
76
+ /**
77
+ * Deletes a specific memory
78
+ * @param {string} id - Memory identifier
79
+ * @param {string} roomId - Room identifier
80
+ * @returns {Promise<void>}
81
+ */
82
+ async clearMemoryById(id: string, roomId: string): Promise<void> {
83
+ await this.adapter.clearMemoryById(id, roomId);
84
+ }
85
+
86
+ /**
87
+ * Clears all memories across all rooms
88
+ * @returns {Promise<void>}
89
+ */
90
+ async clearAllMemories(): Promise<void> {
91
+ await this.adapter.clearAllMemories();
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@ai.ntellect/core",
3
- "version": "0.6.17",
3
+ "version": "0.6.20",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
7
  "build": "rm -rf dist && tsc",
8
8
  "test": "mocha --require ts-node/register",
9
+ "test:coverage": "nyc --reporter=text --reporter=html pnpm test",
9
10
  "test:watch": "mocha --require ts-node/register 'test/**/*.test.ts' --watch"
10
11
  },
11
12
  "keywords": [],
@@ -38,6 +39,7 @@
38
39
  "dotenv": "^16.4.7",
39
40
  "meilisearch": "^0.37.0",
40
41
  "mocha": "^10.0.0",
42
+ "nyc": "^17.1.0",
41
43
  "redis": "^4.6.13",
42
44
  "ts-node": "^10.9.0",
43
45
  "typescript": "^5.7.2"
@@ -0,0 +1,578 @@
1
+ import { expect } from "chai";
2
+ import EventEmitter from "events";
3
+ import sinon from "sinon";
4
+ import { z } from "zod";
5
+ import { GraphFlow } from "../../graph/index";
6
+ import { GraphDefinition, Node } from "../../types";
7
+ /**
8
+ * ✅ Define a valid schema using Zod.
9
+ */
10
+ const TestSchema = z.object({
11
+ value: z.number().default(0),
12
+ });
13
+
14
+ /**
15
+ * ✅ Define the schema type for TypeScript inference.
16
+ */
17
+ type TestSchema = typeof TestSchema;
18
+
19
+ describe("Graph", function () {
20
+ let graph: GraphFlow<TestSchema>;
21
+ let eventEmitter: EventEmitter;
22
+
23
+ beforeEach(() => {
24
+ eventEmitter = new EventEmitter();
25
+ graph = new GraphFlow(
26
+ "TestGraph",
27
+ {
28
+ name: "TestGraph",
29
+ nodes: [],
30
+ context: { value: 0 },
31
+ schema: TestSchema,
32
+ eventEmitter: eventEmitter,
33
+ },
34
+ { verbose: true }
35
+ );
36
+ });
37
+
38
+ /**
39
+ * ✅ Ensure a simple node executes and updates the context correctly.
40
+ */
41
+ it("should execute a simple node and update the context", async function () {
42
+ const simpleNode: Node<TestSchema> = {
43
+ name: "simpleNode",
44
+ execute: async (context) => {
45
+ context.value = (context.value ?? 0) + 1;
46
+ },
47
+ next: [],
48
+ };
49
+
50
+ graph.addNode(simpleNode);
51
+ await graph.execute("simpleNode");
52
+
53
+ const context = graph.getContext();
54
+ expect(context.value).to.equal(1);
55
+ });
56
+
57
+ /**
58
+ * ✅ Verify that `nodeStarted` and `nodeCompleted` events are triggered.
59
+ */
60
+ it("should trigger `nodeStarted` and `nodeCompleted` events", async function () {
61
+ const nodeStartedSpy = sinon.spy();
62
+ const nodeCompletedSpy = sinon.spy();
63
+
64
+ graph.on("nodeStarted", nodeStartedSpy);
65
+ graph.on("nodeCompleted", nodeCompletedSpy);
66
+
67
+ const testNode: Node<TestSchema> = {
68
+ name: "testNode",
69
+ execute: async (context) => {
70
+ context.value = (context.value ?? 0) + 1;
71
+ },
72
+ next: [],
73
+ };
74
+
75
+ graph.addNode(testNode);
76
+ await graph.execute("testNode");
77
+
78
+ expect(nodeStartedSpy.calledOnce).to.be.true;
79
+ expect(nodeCompletedSpy.calledOnce).to.be.true;
80
+ });
81
+
82
+ /**
83
+ * ✅ Ensure an error is thrown when a node fails and `nodeError` event is triggered.
84
+ */
85
+ it("should handle errors and trigger `nodeError` event", async function () {
86
+ const errorNode: Node<TestSchema> = {
87
+ name: "errorNode",
88
+ execute: async () => {
89
+ throw new Error("Test error");
90
+ },
91
+ next: [],
92
+ };
93
+
94
+ graph.addNode(errorNode);
95
+ const nodeErrorSpy = sinon.spy();
96
+ graph.on("nodeError", nodeErrorSpy);
97
+
98
+ try {
99
+ await graph.execute("errorNode");
100
+ } catch (error) {
101
+ expect((error as Error).message).to.equal("Test error");
102
+ }
103
+
104
+ expect(nodeErrorSpy.calledOnce).to.be.true;
105
+ });
106
+
107
+ /**
108
+ * ✅ Ensure that context validation using Zod works correctly.
109
+ */
110
+ it("should validate context with Zod", async function () {
111
+ const invalidContext = { value: "invalid_string" };
112
+
113
+ try {
114
+ const simpleNode: Node<TestSchema> = {
115
+ name: "simpleNode",
116
+ execute: async (context) => {
117
+ context.value = (context.value ?? 0) + 1;
118
+ },
119
+ next: [],
120
+ };
121
+
122
+ graph.addNode(simpleNode);
123
+ await graph.execute("simpleNode", invalidContext as any);
124
+ } catch (error) {
125
+ expect((error as Error & { errors: any[] }).errors[0].message).to.include(
126
+ "Expected number"
127
+ );
128
+ }
129
+ });
130
+
131
+ /**
132
+ * ✅ Ensure a node with validated inputs and outputs executes correctly.
133
+ */
134
+ it("should execute a node with validated inputs and outputs", async function () {
135
+ const paramNode: Node<TestSchema, { increment: number }> = {
136
+ name: "paramNode",
137
+ inputs: z.object({
138
+ increment: z.number(),
139
+ }),
140
+ outputs: z.object({
141
+ value: z.number().min(5),
142
+ }),
143
+ execute: async (context, inputs: { increment: number }) => {
144
+ context.value = (context.value ?? 0) + inputs.increment;
145
+ },
146
+ next: [],
147
+ };
148
+
149
+ graph.addNode(paramNode);
150
+ await graph.execute("paramNode", { increment: 5 });
151
+
152
+ const context = graph.getContext();
153
+ expect(context.value).to.equal(5);
154
+ });
155
+
156
+ /**
157
+ * ✅ Ensure a node does not execute if a condition is not met.
158
+ */
159
+ it("should not execute a node when condition is false", async function () {
160
+ const conditionalNode: Node<TestSchema> = {
161
+ name: "conditionalNode",
162
+ condition: (context) => (context.value ?? 0) > 0,
163
+ execute: async (context) => {
164
+ context.value = (context.value ?? 0) + 10;
165
+ },
166
+ next: [],
167
+ };
168
+
169
+ graph.addNode(conditionalNode);
170
+ await graph.execute("conditionalNode");
171
+
172
+ const context = graph.getContext();
173
+ expect(context.value).to.equal(0);
174
+ });
175
+
176
+ /**
177
+ * ✅ Ensure that a node retries execution when it fails.
178
+ */
179
+ it("should retry a node execution when it fails", async function () {
180
+ let attemptCount = 0;
181
+ const retryNode: Node<TestSchema> = {
182
+ name: "retryNode",
183
+ retry: { maxAttempts: 3, delay: 0 },
184
+ execute: async (context) => {
185
+ attemptCount++;
186
+ if (attemptCount < 3) {
187
+ throw new Error("Temporary failure");
188
+ }
189
+ context.value = (context.value ?? 0) + 1;
190
+ },
191
+ next: [],
192
+ };
193
+
194
+ graph.addNode(retryNode);
195
+ await graph.execute("retryNode");
196
+
197
+ const context = graph.getContext();
198
+ expect(context.value).to.equal(1);
199
+ expect(attemptCount).to.equal(3);
200
+ });
201
+
202
+ /**
203
+ * ✅ Ensure dynamic event-based execution works via `emit`.
204
+ */
205
+ it("should trigger a node execution from an event", async function () {
206
+ this.timeout(5000); // Ensure we have enough time to complete the test
207
+
208
+ const eventNode: Node<TestSchema> = {
209
+ name: "eventNode",
210
+ events: ["customEvent"],
211
+ execute: async (context) => {
212
+ context.value = (context.value ?? 0) + 1;
213
+ },
214
+ next: [],
215
+ };
216
+
217
+ graph.addNode(eventNode);
218
+
219
+ // Use a promise to ensure the event is properly handled
220
+ await new Promise<void>((resolve, reject) => {
221
+ const timeout = setTimeout(
222
+ () => reject(new Error("Event did not trigger")),
223
+ 1500
224
+ );
225
+
226
+ graph.on("nodeCompleted", ({ name }) => {
227
+ if (name === "eventNode") {
228
+ clearTimeout(timeout);
229
+ resolve();
230
+ }
231
+ });
232
+
233
+ graph.emit("customEvent").catch(reject);
234
+ });
235
+
236
+ const context = graph.getContext();
237
+ expect(context.value).to.equal(1);
238
+ });
239
+
240
+ /**
241
+ * ✅ Ensure that removing a node works correctly.
242
+ */
243
+ it("should remove a node from the graph", function () {
244
+ const testNode: Node<TestSchema> = {
245
+ name: "testNode",
246
+ execute: async () => {},
247
+ };
248
+ graph.addNode(testNode);
249
+ graph.removeNode("testNode");
250
+
251
+ expect(graph.getNodes().length).to.equal(0);
252
+ });
253
+
254
+ it("should clear and reload the graph using `load`", function () {
255
+ const nodeA: Node<TestSchema> = {
256
+ name: "A",
257
+ execute: async () => {},
258
+ };
259
+ const nodeB: Node<TestSchema> = {
260
+ name: "B",
261
+ execute: async () => {},
262
+ };
263
+
264
+ const newDefinition: GraphDefinition<TestSchema> = {
265
+ name: "TestGraph",
266
+ entryNode: "A",
267
+ nodes: [nodeA, nodeB],
268
+ context: { value: 0 },
269
+ schema: TestSchema,
270
+ };
271
+
272
+ graph.load(newDefinition);
273
+ expect(graph.getNodes().length).to.equal(2);
274
+ expect(graph.getNodes().map((n) => n.name)).to.include.members(["A", "B"]);
275
+ });
276
+
277
+ /**
278
+ * ✅ Test input validation failure
279
+ */
280
+ it("should throw error when node input validation fails", async function () {
281
+ const nodeWithInput: Node<TestSchema, { amount: number }> = {
282
+ name: "inputNode",
283
+ inputs: z.object({
284
+ amount: z.number().min(0),
285
+ }),
286
+ execute: async (context, inputs: { amount: number }) => {
287
+ context.value = (context.value ?? 0) + inputs.amount;
288
+ },
289
+ next: [],
290
+ };
291
+
292
+ graph.addNode(nodeWithInput);
293
+
294
+ try {
295
+ await graph.execute("inputNode", { amount: -1 });
296
+ expect.fail("Should have thrown an error");
297
+ } catch (error) {
298
+ expect((error as Error).message).to.include(
299
+ "Number must be greater than or equal to 0"
300
+ );
301
+ }
302
+ });
303
+
304
+ /**
305
+ * ✅ Test output validation failure
306
+ */
307
+ it("should throw error when node output validation fails", async function () {
308
+ const nodeWithOutput: Node<TestSchema> = {
309
+ name: "outputNode",
310
+ outputs: z.object({
311
+ value: z.number().max(10),
312
+ }),
313
+ execute: async (context) => {
314
+ context.value = 20; // This will fail output validation
315
+ },
316
+ next: [],
317
+ };
318
+
319
+ graph.addNode(nodeWithOutput);
320
+
321
+ try {
322
+ await graph.execute("outputNode");
323
+ expect.fail("Should have thrown an error");
324
+ } catch (error) {
325
+ expect((error as Error).message).to.include(
326
+ "Number must be less than or equal to 10"
327
+ );
328
+ }
329
+ });
330
+
331
+ /**
332
+ * ✅ Test successful input and output validation
333
+ */
334
+ it("should successfully validate both inputs and outputs", async function () {
335
+ const validatedNode: Node<TestSchema, { increment: number }> = {
336
+ name: "validatedNode",
337
+ inputs: z.object({
338
+ increment: z.number().min(0).max(5),
339
+ }),
340
+ outputs: z.object({
341
+ value: z.number().min(0).max(10),
342
+ }),
343
+ execute: async (context, inputs: { increment: number }) => {
344
+ context.value = (context.value ?? 0) + inputs.increment;
345
+ },
346
+ next: [],
347
+ };
348
+
349
+ graph.addNode(validatedNode);
350
+
351
+ // Test with valid input that produces valid output
352
+ await graph.execute("validatedNode", { increment: 3 });
353
+ expect(graph.getContext().value).to.equal(3);
354
+
355
+ // Test with valid input that would produce invalid output
356
+ try {
357
+ await graph.execute("validatedNode", { increment: 5 }, { value: 7 });
358
+ expect.fail("Should have thrown an error");
359
+ } catch (error) {
360
+ expect((error as Error).message).to.include(
361
+ "Number must be less than or equal to 10"
362
+ );
363
+ }
364
+ });
365
+
366
+ /**
367
+ * ✅ Test missing required inputs
368
+ */
369
+ it("should throw error when required inputs are missing", async function () {
370
+ const nodeWithRequiredInput: Node<TestSchema, { required: string }> = {
371
+ name: "requiredInputNode",
372
+ inputs: z.object({
373
+ required: z.string(),
374
+ }),
375
+ execute: async () => {},
376
+ next: [],
377
+ };
378
+
379
+ graph.addNode(nodeWithRequiredInput);
380
+
381
+ try {
382
+ await graph.execute("requiredInputNode");
383
+ expect.fail("Should have thrown an error");
384
+ } catch (error) {
385
+ expect((error as Error).message).to.include("Inputs required for node");
386
+ }
387
+ });
388
+
389
+ /**
390
+ * ✅ Test complex workflow with multiple branches
391
+ */
392
+ it("should execute a complex workflow with multiple branches", async function () {
393
+ const nodeA: Node<TestSchema> = {
394
+ name: "nodeA",
395
+ execute: async (context) => {
396
+ context.value = (context.value ?? 0) + 1;
397
+ },
398
+ next: ["nodeB1", "nodeB2"],
399
+ };
400
+
401
+ const nodeB1: Node<TestSchema> = {
402
+ name: "nodeB1",
403
+ execute: async (context) => {
404
+ context.value = (context.value ?? 0) * 2;
405
+ },
406
+ next: ["nodeC"],
407
+ };
408
+
409
+ const nodeB2: Node<TestSchema> = {
410
+ name: "nodeB2",
411
+ execute: async (context) => {
412
+ context.value = (context.value ?? 0) + 3;
413
+ },
414
+ next: ["nodeC"],
415
+ };
416
+
417
+ const nodeC: Node<TestSchema> = {
418
+ name: "nodeC",
419
+ execute: async (context) => {
420
+ // Créer une copie du contexte pour éviter les modifications concurrentes
421
+ const newValue = (context.value ?? 0) + 5;
422
+ context.value = newValue;
423
+ },
424
+ };
425
+
426
+ [nodeA, nodeB1, nodeB2, nodeC].forEach((node) => graph.addNode(node));
427
+
428
+ await graph.execute("nodeA");
429
+ expect(graph.getContext().value).to.equal(9);
430
+ });
431
+
432
+ /**
433
+ * ✅ Test conditional workflow branching
434
+ */
435
+ it("should execute different branches based on conditions", async function () {
436
+ const startNode: Node<TestSchema> = {
437
+ name: "start",
438
+ execute: async (context) => {
439
+ context.value = (context.value ?? 0) + 5;
440
+ },
441
+ next: ["branchA", "branchB"],
442
+ };
443
+
444
+ const branchA: Node<TestSchema> = {
445
+ name: "branchA",
446
+ condition: (context) => (context.value ?? 0) < 10,
447
+ execute: async (context) => {
448
+ context.value = (context.value ?? 0) * 2;
449
+ },
450
+ next: ["end"],
451
+ };
452
+
453
+ const branchB: Node<TestSchema> = {
454
+ name: "branchB",
455
+ condition: (context) => (context.value ?? 0) >= 10,
456
+ execute: async (context) => {
457
+ context.value = (context.value ?? 0) + 10;
458
+ },
459
+ next: ["end"],
460
+ };
461
+
462
+ const endNode: Node<TestSchema> = {
463
+ name: "end",
464
+ execute: async (context) => {
465
+ context.value = (context.value ?? 0) + 1;
466
+ },
467
+ };
468
+
469
+ [startNode, branchA, branchB, endNode].forEach((node) =>
470
+ graph.addNode(node)
471
+ );
472
+
473
+ await graph.load({
474
+ name: "TestGraph",
475
+ nodes: [startNode, branchA, branchB, endNode],
476
+ context: { value: 0 },
477
+ schema: TestSchema,
478
+ });
479
+
480
+ await graph.execute("start");
481
+ expect(graph.getContext().value).to.equal(11);
482
+ });
483
+
484
+ /**
485
+ * ✅ Test complex event-driven workflow
486
+ */
487
+ it("should handle complex event-driven workflows", async function () {
488
+ this.timeout(5000); // Augmenter le timeout pour les tests asynchrones
489
+ const eventCounter = { count: 0 };
490
+
491
+ const startNode: Node<TestSchema> = {
492
+ name: "start",
493
+ events: ["startWorkflow"],
494
+ execute: async (context) => {
495
+ context.value = 1;
496
+ },
497
+ next: ["process"],
498
+ };
499
+
500
+ const processNode: Node<TestSchema> = {
501
+ name: "process",
502
+ events: ["processData"],
503
+ execute: async (context) => {
504
+ context.value = (context.value ?? 0) * 2;
505
+ },
506
+ next: ["finalize"],
507
+ };
508
+
509
+ const finalizeNode: Node<TestSchema> = {
510
+ name: "finalize",
511
+ events: ["complete"],
512
+ execute: async (context) => {
513
+ context.value = (context.value ?? 0) + 3;
514
+ eventCounter.count++;
515
+ },
516
+ };
517
+
518
+ [startNode, processNode, finalizeNode].forEach((node) =>
519
+ graph.addNode(node)
520
+ );
521
+
522
+ // Test sequential event triggering
523
+ await graph.emit("startWorkflow");
524
+ await graph.emit("processData");
525
+ await graph.emit("complete");
526
+
527
+ expect(graph.getContext().value).to.equal(5); // (1 * 2) + 3
528
+ expect(eventCounter.count).to.equal(1);
529
+
530
+ // Reset context for concurrent test
531
+ graph.load({
532
+ name: "TestGraph",
533
+ nodes: [startNode, processNode, finalizeNode],
534
+ context: { value: 0 },
535
+ schema: TestSchema,
536
+ });
537
+
538
+ // Test concurrent event handling
539
+ await Promise.all([
540
+ graph.emit("startWorkflow"),
541
+ graph.emit("processData"),
542
+ graph.emit("complete"),
543
+ ]);
544
+
545
+ expect(eventCounter.count).to.equal(2);
546
+ });
547
+
548
+ /**
549
+ * ✅ Test cyclic workflow with conditional exit
550
+ */
551
+ it("should handle cyclic workflows with conditional exit", async function () {
552
+ const iterationCount = { count: 0 };
553
+
554
+ const cycleNode: Node<TestSchema> = {
555
+ name: "cycle",
556
+ execute: async (context) => {
557
+ context.value = (context.value ?? 0) + 1;
558
+ iterationCount.count++;
559
+ },
560
+ next: ["checkExit"],
561
+ };
562
+
563
+ const checkExitNode: Node<TestSchema> = {
564
+ name: "checkExit",
565
+ execute: async () => {},
566
+ next: (context) => {
567
+ return (context.value ?? 0) < 5 ? ["cycle"] : [];
568
+ },
569
+ };
570
+
571
+ [cycleNode, checkExitNode].forEach((node) => graph.addNode(node));
572
+
573
+ await graph.execute("cycle");
574
+
575
+ expect(graph.getContext().value).to.equal(5);
576
+ expect(iterationCount.count).to.equal(5);
577
+ });
578
+ });