@ai.ntellect/core 0.7.5 → 0.7.7

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.
@@ -1,19 +1,27 @@
1
- import { expect } from "chai";
1
+ import { expect, use } from "chai";
2
+ import chaiAsPromised from "chai-as-promised";
2
3
  import EventEmitter from "events";
3
4
  import sinon from "sinon";
4
5
  import { z } from "zod";
5
6
  import { GraphController } from "../../graph/controller";
6
7
  import { GraphFlow } from "../../graph/index";
8
+ import { NodeParams } from "../../graph/node";
7
9
  import { GraphContext, GraphDefinition, Node } from "../../types";
8
10
 
11
+ use(chaiAsPromised);
12
+
9
13
  /**
10
14
  * Test schema definition using Zod for graph context validation
11
15
  * Defines a schema with:
12
16
  * - value: numeric value for tracking state changes
17
+ * - counter: numeric value for tracking state changes
18
+ * - message: string for tracking state changes
13
19
  * - eventPayload: optional object containing transaction metadata
14
20
  */
15
21
  const TestSchema = z.object({
16
22
  value: z.number().default(0),
23
+ counter: z.number().default(0),
24
+ message: z.string().default(""),
17
25
  eventPayload: z
18
26
  .object({
19
27
  transactionId: z.string().optional(),
@@ -172,17 +180,15 @@ describe("GraphFlow", function () {
172
180
  * - Integration between node execution and validation
173
181
  * Ensures type safety and data consistency in node interactions
174
182
  */
175
- it("should execute a node with validated inputs and outputs", async function () {
183
+ it("should execute a node with validated params and outputs", async function () {
176
184
  const paramNode: Node<TestSchema, { increment: number }> = {
177
185
  name: "paramNode",
178
- inputs: z.object({
186
+ params: z.object({
179
187
  increment: z.number(),
180
188
  }),
181
- outputs: z.object({
182
- value: z.number().min(5),
183
- }),
184
- execute: async (context, inputs: { increment: number }) => {
185
- context.value = (context.value ?? 0) + inputs.increment;
189
+ execute: async (context, params?: { increment: number }) => {
190
+ if (!params) throw new Error("params required");
191
+ context.value = (context.value ?? 0) + params.increment;
186
192
  },
187
193
  next: [],
188
194
  };
@@ -230,30 +236,31 @@ describe("GraphFlow", function () {
230
236
  */
231
237
  it("should retry a node execution when it fails", async () => {
232
238
  let attempts = 0;
233
- const nodes = new Map();
234
- nodes.set("test", {
235
- name: "test",
236
- execute: async () => {
239
+ const retryNode: Node<TestSchema> = {
240
+ name: "retryNode",
241
+ execute: async (context) => {
237
242
  attempts++;
238
243
  if (attempts < 3) {
239
244
  throw new Error("Temporary failure");
240
245
  }
246
+ context.value = 42;
241
247
  },
242
248
  retry: {
243
249
  maxAttempts: 3,
244
250
  delay: 100,
245
251
  },
246
- });
252
+ };
247
253
 
248
254
  const graph = new GraphFlow("test", {
249
255
  name: "test",
250
256
  schema: TestSchema,
251
257
  context: { value: 0 },
252
- nodes: Array.from(nodes.values()),
258
+ nodes: [retryNode],
253
259
  });
254
260
 
255
- await graph.execute("test");
261
+ await graph.execute("retryNode");
256
262
  expect(attempts).to.equal(3);
263
+ expect(graph.getContext().value).to.equal(42);
257
264
  });
258
265
 
259
266
  /**
@@ -300,21 +307,22 @@ describe("GraphFlow", function () {
300
307
  * Tests input validation error handling
301
308
  */
302
309
  it("should throw error when node input validation fails", async () => {
303
- const InputSchema = z.object({
304
- value: z.number().min(0),
305
- });
310
+ const node: Node<TestSchema> = {
311
+ name: "test",
312
+ params: z.object({
313
+ value: z.number().min(0),
314
+ }),
315
+ execute: async (context, params) => {
316
+ if (!params) throw new Error("params required");
317
+ context.value = params.value;
318
+ },
319
+ };
306
320
 
307
321
  const graph = new GraphFlow("test", {
308
322
  name: "test",
309
323
  schema: TestSchema,
310
324
  context: { value: 0 },
311
- nodes: [
312
- {
313
- name: "test",
314
- inputs: InputSchema,
315
- execute: async () => {},
316
- },
317
- ],
325
+ nodes: [node],
318
326
  });
319
327
 
320
328
  try {
@@ -324,90 +332,68 @@ describe("GraphFlow", function () {
324
332
  expect(error.message).to.include("Number must be greater than or equal");
325
333
  }
326
334
  });
327
-
328
- /**
329
- * Tests output validation error handling
330
- */
331
- it("should throw error when node output validation fails", async function () {
332
- const nodeWithOutput: Node<TestSchema> = {
333
- name: "outputNode",
334
- outputs: z.object({
335
- value: z.number().max(10),
336
- }),
337
- execute: async (context) => {
338
- context.value = 20; // This will fail output validation
339
- },
340
- next: [],
341
- };
342
-
343
- graph.addNode(nodeWithOutput);
344
-
345
- try {
346
- await graph.execute("outputNode");
347
- expect.fail("Should have thrown an error");
348
- } catch (error) {
349
- expect((error as Error).message).to.include(
350
- "Number must be less than or equal to 10"
351
- );
352
- }
353
- });
354
-
355
335
  /**
356
336
  * Tests successful input/output validation flow
357
337
  */
358
- it("should successfully validate both inputs and outputs", async function () {
359
- const validatedNode: Node<TestSchema, { increment: number }> = {
360
- name: "validatedNode",
361
- inputs: z.object({
362
- increment: z.number().min(0).max(5),
363
- }),
364
- outputs: z.object({
365
- value: z.number().min(0).max(10),
366
- }),
367
- execute: async (context, inputs: { increment: number }) => {
368
- context.value = (context.value ?? 0) + inputs.increment;
369
- },
370
- next: [],
371
- };
372
-
373
- graph.addNode(validatedNode);
338
+ it("should successfully validate both params and outputs", async function () {
339
+ const graph = new GraphFlow("test", {
340
+ name: "test",
341
+ nodes: [
342
+ {
343
+ name: "validatedNode",
344
+ params: z.object({
345
+ increment: z.number().min(0).max(5),
346
+ }),
347
+ execute: async (
348
+ context: GraphContext<TestSchema>,
349
+ params?: NodeParams
350
+ ) => {
351
+ if (!params) throw new Error("params required");
352
+ context.value = (context.value ?? 0) + params.increment;
353
+ },
354
+ },
355
+ ],
356
+ schema: TestSchema,
357
+ context: { value: 0, counter: 0, message: "" },
358
+ });
374
359
 
375
- // Test with valid input that produces valid output
360
+ // Test avec valeur valide
376
361
  await graph.execute("validatedNode", { increment: 3 });
377
362
  expect(graph.getContext().value).to.equal(3);
378
363
 
379
- // Test with valid input that would produce invalid output
380
- try {
381
- await graph.execute("validatedNode", { increment: 5 }, { value: 7 });
382
- expect.fail("Should have thrown an error");
383
- } catch (error) {
384
- expect((error as Error).message).to.include(
385
- "Number must be less than or equal to 10"
386
- );
387
- }
364
+ // Test avec valeur invalide
365
+ await expect(
366
+ graph.execute("validatedNode", { increment: 10 })
367
+ ).to.be.rejectedWith("Number must be less than or equal to 5");
388
368
  });
389
369
 
390
370
  /**
391
- * Tests handling of missing required inputs
371
+ * Tests handling of missing required params
392
372
  */
393
- it("should throw error when required inputs are missing", async function () {
394
- const nodeWithRequiredInput: Node<TestSchema, { required: string }> = {
395
- name: "requiredInputNode",
396
- inputs: z.object({
397
- required: z.string(),
398
- }),
399
- execute: async () => {},
400
- next: [],
401
- };
402
-
403
- graph.addNode(nodeWithRequiredInput);
373
+ it("should throw error when required params are missing", async function () {
374
+ const graph = new GraphFlow("test", {
375
+ name: "test",
376
+ nodes: [
377
+ {
378
+ name: "requiredInputNode",
379
+ params: z.object({
380
+ value: z.number(),
381
+ }),
382
+ execute: async (
383
+ context: GraphContext<TestSchema>,
384
+ params?: NodeParams
385
+ ) => {
386
+ context.counter = params?.value || 0;
387
+ },
388
+ },
389
+ ],
390
+ schema: TestSchema,
391
+ context: { value: 0, counter: 0, message: "" },
392
+ });
404
393
 
405
- try {
406
- await graph.execute("requiredInputNode");
407
- expect.fail("Should have thrown an error");
408
- } catch (error) {
409
- expect((error as Error).message).to.include("Inputs required for node");
410
- }
394
+ await expect(graph.execute("requiredInputNode")).to.be.rejectedWith(
395
+ "Params required for node"
396
+ );
411
397
  });
412
398
 
413
399
  /**
@@ -724,4 +710,59 @@ describe("GraphFlow", function () {
724
710
  const result = await resultPromise;
725
711
  expect(result.value).to.equal(6); // 1 (waitingNode) + 5 (finalNode)
726
712
  });
713
+
714
+ // Test de validation des paramètres
715
+ it("should successfully validate params", async () => {
716
+ const graph = new GraphFlow("test", {
717
+ name: "test",
718
+ nodes: [
719
+ {
720
+ name: "validationNode",
721
+ params: z.object({
722
+ value: z.number().max(10),
723
+ }),
724
+ execute: async (
725
+ context: GraphContext<TestSchema>,
726
+ params?: NodeParams
727
+ ) => {
728
+ context.counter = params?.value || 0;
729
+ },
730
+ },
731
+ ],
732
+ schema: TestSchema,
733
+ context: { value: 0, counter: 0, message: "" },
734
+ });
735
+
736
+ // Test avec valeur invalide
737
+ await expect(
738
+ graph.execute("validationNode", { value: 20 })
739
+ ).to.be.rejectedWith("Number must be less than or equal to 10");
740
+ });
741
+
742
+ // Test des paramètres requis
743
+ it("should throw error when required params are missing", async () => {
744
+ const graph = new GraphFlow("test", {
745
+ name: "test",
746
+ nodes: [
747
+ {
748
+ name: "requiredInputNode",
749
+ params: z.object({
750
+ value: z.number(),
751
+ }),
752
+ execute: async (
753
+ context: GraphContext<TestSchema>,
754
+ params?: NodeParams
755
+ ) => {
756
+ context.counter = params?.value || 0;
757
+ },
758
+ },
759
+ ],
760
+ schema: TestSchema,
761
+ context: { value: 0, counter: 0, message: "" },
762
+ });
763
+
764
+ await expect(graph.execute("requiredInputNode")).to.be.rejectedWith(
765
+ "Params required for node"
766
+ );
767
+ });
727
768
  });
@@ -5,7 +5,7 @@ import { BehaviorSubject, Subject } from "rxjs";
5
5
  import { z } from "zod";
6
6
  import { GraphEventManager } from "../../graph/event-manager";
7
7
  import { GraphLogger } from "../../graph/logger";
8
- import { GraphNode } from "../../graph/node";
8
+ import { GraphNode, NodeParams } from "../../graph/node";
9
9
  import { GraphContext } from "../../types";
10
10
 
11
11
  use(chaiAsPromised);
@@ -107,7 +107,7 @@ describe("GraphNode", () => {
107
107
  const nodes = new Map();
108
108
  nodes.set("test", {
109
109
  name: "test",
110
- execute: async (_context: TestContext) => {
110
+ execute: async () => {
111
111
  throw new Error("Test error");
112
112
  },
113
113
  });
@@ -121,13 +121,17 @@ describe("GraphNode", () => {
121
121
  );
122
122
 
123
123
  try {
124
- await node.executeNode("test", { counter: 0, message: "Hello" }, null);
125
- expect.fail("Should have thrown an error");
124
+ await node.executeNode(
125
+ "test",
126
+ { counter: 0, message: "Hello" },
127
+ null,
128
+ false
129
+ );
130
+ expect.fail("Test error");
126
131
  } catch (error: any) {
127
132
  expect(error.message).to.equal("Test error");
128
133
  const errorEvents = events.filter((e) => e.type === "nodeError");
129
134
  expect(errorEvents).to.have.lengthOf(1);
130
- expect(errorEvents[0].payload.error.message).to.equal("Test error");
131
135
  }
132
136
  });
133
137
 
@@ -175,7 +179,7 @@ describe("GraphNode", () => {
175
179
  const nodes = new Map();
176
180
  nodes.set("test", {
177
181
  name: "test",
178
- execute: async (context: TestContext) => {
182
+ execute: async (context: TestContext, inputs?: any) => {
179
183
  context.counter = context.counter; // Même valeur
180
184
  context.message = "New"; // Nouvelle valeur
181
185
  },
@@ -194,4 +198,313 @@ describe("GraphNode", () => {
194
198
  expect(stateChanges).to.have.lengthOf(1); // Seulement pour message
195
199
  expect(stateChanges[0].payload.property).to.equal("message");
196
200
  });
201
+
202
+ it("should execute node with parameters", async () => {
203
+ const nodes = new Map();
204
+ nodes.set("test", {
205
+ name: "test",
206
+ execute: async (context: TestContext, inputs?: any) => {
207
+ context.counter = inputs?.value ?? 0;
208
+ context.message = inputs?.message ?? "Default";
209
+ },
210
+ });
211
+
212
+ node = new GraphNode(
213
+ nodes,
214
+ logger,
215
+ eventManager,
216
+ eventSubject,
217
+ stateSubject
218
+ );
219
+
220
+ await node.executeNode(
221
+ "test",
222
+ { counter: 0, message: "Hello" },
223
+ { value: 5, message: "Custom" },
224
+ false
225
+ );
226
+
227
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
228
+ expect(stateChanges).to.have.lengthOf(2);
229
+ expect(stateChanges[0].payload.newValue).to.equal(5);
230
+ expect(stateChanges[1].payload.newValue).to.equal("Custom");
231
+ });
232
+
233
+ it("should use default values when no parameters provided", async () => {
234
+ const nodes = new Map();
235
+ nodes.set("test", {
236
+ name: "test",
237
+ execute: async (
238
+ context: TestContext,
239
+ _inputs: any,
240
+ params?: NodeParams
241
+ ) => {
242
+ context.counter = params?.increment || 1;
243
+ context.message = params?.message || "Default";
244
+ },
245
+ });
246
+
247
+ node = new GraphNode(
248
+ nodes,
249
+ logger,
250
+ eventManager,
251
+ eventSubject,
252
+ stateSubject
253
+ );
254
+
255
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
256
+
257
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
258
+ expect(stateChanges).to.have.lengthOf(2);
259
+ expect(stateChanges[0].payload.newValue).to.equal(1); // counter (default)
260
+ expect(stateChanges[1].payload.newValue).to.equal("Default"); // message (default)
261
+ });
262
+
263
+ it("should properly handle node inputs", async () => {
264
+ const nodes = new Map();
265
+ nodes.set("test", {
266
+ name: "test",
267
+ execute: async (context: TestContext, inputs: any) => {
268
+ context.counter = inputs.value;
269
+ context.message = inputs.message;
270
+ },
271
+ });
272
+
273
+ node = new GraphNode(
274
+ nodes,
275
+ logger,
276
+ eventManager,
277
+ eventSubject,
278
+ stateSubject
279
+ );
280
+
281
+ const testInputs = {
282
+ value: 42,
283
+ message: "Test Input",
284
+ };
285
+
286
+ await node.executeNode(
287
+ "test",
288
+ { counter: 0, message: "Hello" },
289
+ testInputs
290
+ );
291
+
292
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
293
+ expect(stateChanges).to.have.lengthOf(2);
294
+ expect(stateChanges[0].payload.newValue).to.equal(42); // counter from input
295
+ expect(stateChanges[1].payload.newValue).to.equal("Test Input"); // message from input
296
+ });
297
+
298
+ it("should not emit duplicate state changes", async () => {
299
+ const nodes = new Map();
300
+ nodes.set("test", {
301
+ name: "test",
302
+ execute: async (context: TestContext) => {
303
+ context.counter = 1; // Valeur fixe au lieu d'incrémentations
304
+ context.counter = 1; // Même valeur
305
+ context.message = "New";
306
+ context.message = "New"; // Même valeur
307
+ },
308
+ });
309
+
310
+ node = new GraphNode(
311
+ nodes,
312
+ logger,
313
+ eventManager,
314
+ eventSubject,
315
+ stateSubject
316
+ );
317
+
318
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
319
+
320
+ // Vérifier qu'il n'y a pas de doublons dans les événements
321
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
322
+ const uniqueChanges = new Set(
323
+ stateChanges.map(
324
+ (e) =>
325
+ `${e.payload.property}-${e.payload.oldValue}-${e.payload.newValue}`
326
+ )
327
+ );
328
+
329
+ expect(stateChanges.length).to.equal(uniqueChanges.size);
330
+ expect(stateChanges).to.have.lengthOf(2); // Un pour counter, un pour message
331
+ });
332
+
333
+ it("should validate node parameters with Zod schema", async () => {
334
+ const paramSchema = z.object({
335
+ increment: z.number().min(1),
336
+ message: z.string().min(1),
337
+ });
338
+
339
+ const nodes = new Map();
340
+ nodes.set("test", {
341
+ name: "test",
342
+ params: paramSchema,
343
+ execute: async (context: TestContext, params?: NodeParams) => {
344
+ context.counter += params?.increment || 0;
345
+ context.message = params?.message || "";
346
+ },
347
+ });
348
+
349
+ node = new GraphNode(
350
+ nodes,
351
+ logger,
352
+ eventManager,
353
+ eventSubject,
354
+ stateSubject
355
+ );
356
+
357
+ // Test avec des paramètres valides
358
+ await node.executeNode(
359
+ "test",
360
+ { counter: 0, message: "Hello" },
361
+ { increment: 5, message: "Valid" }
362
+ );
363
+
364
+ // Test avec des paramètres invalides
365
+ await expect(
366
+ node.executeNode(
367
+ "test",
368
+ { counter: 0, message: "Hello" },
369
+ { increment: 0, message: "" }
370
+ )
371
+ ).to.be.rejected; // Enlever le .with() car le message d'erreur vient directement de Zod
372
+ });
373
+
374
+ it("should work without params schema", async () => {
375
+ const nodes = new Map();
376
+ nodes.set("test", {
377
+ name: "test",
378
+ execute: async (context: TestContext) => {
379
+ context.counter++;
380
+ },
381
+ });
382
+
383
+ node = new GraphNode(
384
+ nodes,
385
+ logger,
386
+ eventManager,
387
+ eventSubject,
388
+ stateSubject
389
+ );
390
+
391
+ // Devrait fonctionner sans erreur même sans schema de params
392
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
393
+ });
394
+
395
+ it("should not require params when node has no params schema", async () => {
396
+ const nodes = new Map();
397
+ nodes.set("test", {
398
+ name: "test",
399
+ // Pas de schéma de params défini
400
+ execute: async (context: TestContext) => {
401
+ context.counter++;
402
+ },
403
+ });
404
+
405
+ node = new GraphNode(
406
+ nodes,
407
+ logger,
408
+ eventManager,
409
+ eventSubject,
410
+ stateSubject
411
+ );
412
+
413
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
414
+
415
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
416
+ expect(stateChanges).to.have.lengthOf(1);
417
+ expect(stateChanges[0].payload.newValue).to.equal(1);
418
+ });
419
+
420
+ it("should require params only when node has params schema", async () => {
421
+ const nodes = new Map();
422
+ nodes.set("test", {
423
+ name: "test",
424
+ params: z.object({
425
+ // Avec un schéma de params
426
+ value: z.number(),
427
+ }),
428
+ execute: async (context: TestContext, params?: NodeParams) => {
429
+ context.counter = params?.value || 0;
430
+ },
431
+ });
432
+
433
+ node = new GraphNode(
434
+ nodes,
435
+ logger,
436
+ eventManager,
437
+ eventSubject,
438
+ stateSubject
439
+ );
440
+
441
+ // Devrait échouer sans params
442
+ await expect(
443
+ node.executeNode("test", { counter: 0, message: "Hello" }, null)
444
+ ).to.be.rejectedWith("Params required for node");
445
+ });
446
+
447
+ it("should execute node without params when no schema is defined (real world scenario)", async () => {
448
+ const nodes = new Map();
449
+ nodes.set("incrementCounter", {
450
+ name: "incrementCounter",
451
+ execute: async (context: TestContext) => {
452
+ context.counter++;
453
+ },
454
+ });
455
+
456
+ node = new GraphNode(
457
+ nodes,
458
+ logger,
459
+ eventManager,
460
+ eventSubject,
461
+ stateSubject
462
+ );
463
+
464
+ // Simuler l'appel comme dans examples/t2.ts
465
+ await node.executeNode(
466
+ "incrementCounter",
467
+ { message: "Hello", counter: 0 },
468
+ { test: "test" } // Passer des params même si non requis
469
+ );
470
+
471
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
472
+ expect(stateChanges).to.have.lengthOf(1);
473
+ expect(stateChanges[0].payload.newValue).to.equal(1);
474
+ });
475
+
476
+ it("should handle optional params schema", async () => {
477
+ const nodes = new Map();
478
+ nodes.set("test", {
479
+ name: "test",
480
+ params: z
481
+ .object({
482
+ test: z.string(),
483
+ })
484
+ .optional(),
485
+ execute: async (context: TestContext, params?: NodeParams) => {
486
+ context.counter++;
487
+ },
488
+ });
489
+
490
+ node = new GraphNode(
491
+ nodes,
492
+ logger,
493
+ eventManager,
494
+ eventSubject,
495
+ stateSubject
496
+ );
497
+
498
+ // Devrait fonctionner avec ou sans params
499
+ await node.executeNode(
500
+ "test",
501
+ { counter: 0, message: "Hello" },
502
+ { test: "test" }
503
+ );
504
+ await node.executeNode("test", { counter: 1, message: "Hello" }, null);
505
+
506
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
507
+ expect(stateChanges).to.have.lengthOf(2);
508
+ expect(stateChanges[1].payload.newValue).to.equal(2);
509
+ });
197
510
  });