@ai.ntellect/core 0.7.6 → 0.7.8

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,18 +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
- if (!inputs) throw new Error("Inputs required");
186
- 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;
187
192
  },
188
193
  next: [],
189
194
  };
@@ -304,12 +309,12 @@ describe("GraphFlow", function () {
304
309
  it("should throw error when node input validation fails", async () => {
305
310
  const node: Node<TestSchema> = {
306
311
  name: "test",
307
- inputs: z.object({
312
+ params: z.object({
308
313
  value: z.number().min(0),
309
314
  }),
310
- execute: async (context, inputs) => {
311
- if (!inputs) throw new Error("Inputs required");
312
- context.value = inputs.value;
315
+ execute: async (context, params) => {
316
+ if (!params) throw new Error("params required");
317
+ context.value = params.value;
313
318
  },
314
319
  };
315
320
 
@@ -327,98 +332,68 @@ describe("GraphFlow", function () {
327
332
  expect(error.message).to.include("Number must be greater than or equal");
328
333
  }
329
334
  });
330
-
331
- /**
332
- * Tests output validation error handling
333
- */
334
- it("should throw error when node output validation fails", async function () {
335
- const nodeWithOutput: Node<TestSchema> = {
336
- name: "outputNode",
337
- outputs: z.object({
338
- value: z.number().max(10),
339
- }),
340
- execute: async (context) => {
341
- context.value = 20; // This will fail output validation
342
- },
343
- next: [],
344
- };
345
-
346
- graph.addNode(nodeWithOutput);
347
-
348
- try {
349
- await graph.execute("outputNode");
350
- expect.fail("Should have thrown an error");
351
- } catch (error) {
352
- expect((error as Error).message).to.include(
353
- "Number must be less than or equal to 10"
354
- );
355
- }
356
- });
357
-
358
335
  /**
359
336
  * Tests successful input/output validation flow
360
337
  */
361
- it("should successfully validate both inputs and outputs", async function () {
338
+ it("should successfully validate both params and outputs", async function () {
362
339
  const graph = new GraphFlow("test", {
363
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
+ ],
364
356
  schema: TestSchema,
365
- context: { value: 0 },
366
- nodes: [],
357
+ context: { value: 0, counter: 0, message: "" },
367
358
  });
368
359
 
369
- const validatedNode: Node<TestSchema, { increment: number }> = {
370
- name: "validatedNode",
371
- inputs: z.object({
372
- increment: z.number().min(0).max(5),
373
- }),
374
- outputs: z.object({
375
- value: z.number().min(0).max(10),
376
- }),
377
- execute: async (context, inputs?: { increment: number }) => {
378
- if (!inputs) throw new Error("Inputs required");
379
- context.value = (context.value ?? 0) + inputs.increment;
380
- },
381
- next: [],
382
- };
383
-
384
- graph.addNode(validatedNode);
385
-
386
- // Test with valid input that produces valid output
360
+ // Test avec valeur valide
387
361
  await graph.execute("validatedNode", { increment: 3 });
388
362
  expect(graph.getContext().value).to.equal(3);
389
363
 
390
- // Test with valid input that would produce invalid output
391
- try {
392
- await graph.execute("validatedNode", { increment: 5 }, { value: 7 });
393
- expect.fail("Should have thrown an error");
394
- } catch (error) {
395
- expect((error as Error).message).to.include(
396
- "Number must be less than or equal to 10"
397
- );
398
- }
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");
399
368
  });
400
369
 
401
370
  /**
402
- * Tests handling of missing required inputs
371
+ * Tests handling of missing required params
403
372
  */
404
- it("should throw error when required inputs are missing", async function () {
405
- const nodeWithRequiredInput: Node<TestSchema, { required: string }> = {
406
- name: "requiredInputNode",
407
- inputs: z.object({
408
- required: z.string(),
409
- }),
410
- execute: async () => {},
411
- next: [],
412
- };
413
-
414
- 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
+ });
415
393
 
416
- try {
417
- await graph.execute("requiredInputNode");
418
- expect.fail("Should have thrown an error");
419
- } catch (error) {
420
- expect((error as Error).message).to.include("Inputs required for node");
421
- }
394
+ await expect(graph.execute("requiredInputNode")).to.be.rejectedWith(
395
+ "Params required for node"
396
+ );
422
397
  });
423
398
 
424
399
  /**
@@ -616,90 +591,6 @@ describe("GraphFlow", function () {
616
591
  expect(results[1].value).to.equal(5); // Graph2: 3 + 2
617
592
  });
618
593
 
619
- /**
620
- * Tests event correlation functionality
621
- * Demonstrates:
622
- * - Event correlation based on transaction ID
623
- * - Timeout handling
624
- * - Multiple event synchronization
625
- * - Context updates after correlation
626
- * Critical for integrating with external event sources
627
- */
628
- it("should handle correlated events correctly", async function () {
629
- this.timeout(10000);
630
- const graph = new GraphFlow("test", {
631
- name: "test",
632
- nodes: [],
633
- context: { value: 0 },
634
- schema: TestSchema,
635
- eventEmitter: new EventEmitter(),
636
- });
637
-
638
- let eventsReceived = 0;
639
- const node = {
640
- name: "testNode",
641
- waitForEvents: {
642
- events: ["eventA", "eventB"],
643
- timeout: 5000,
644
- strategy: "all" as const,
645
- },
646
- execute: async (context: GraphContext<typeof TestSchema>) => {
647
- eventsReceived = 2;
648
- context.value = 42;
649
- },
650
- };
651
-
652
- graph.addNode(node);
653
-
654
- graph.execute("testNode");
655
-
656
- await new Promise((resolve) => setTimeout(resolve, 500));
657
-
658
- await graph.emit("eventA", { eventPayload: { status: "A" } });
659
- await new Promise((resolve) => setTimeout(resolve, 100));
660
- await graph.emit("eventB", { eventPayload: { status: "B" } });
661
-
662
- await new Promise((resolve) => setTimeout(resolve, 100));
663
-
664
- expect(eventsReceived).to.equal(2);
665
- expect(graph.getContext().value).to.equal(42);
666
- });
667
-
668
- /**
669
- * Tests multiple event waiting functionality
670
- */
671
- it("should wait for multiple events before continuing", async function () {
672
- this.timeout(10000);
673
- const graph = new GraphFlow("test", {
674
- name: "test",
675
- nodes: [],
676
- context: { value: 0 },
677
- schema: TestSchema,
678
- eventEmitter: new EventEmitter(),
679
- });
680
-
681
- const node = {
682
- name: "testNode",
683
- waitForEvents: {
684
- events: ["event1", "event2"],
685
- timeout: 5000,
686
- strategy: "all" as const,
687
- },
688
- execute: async (context: GraphContext<typeof TestSchema>) => {
689
- context.value = 42; // Ajouter une modification du contexte
690
- },
691
- };
692
-
693
- graph.addNode(node);
694
- graph.execute("testNode");
695
-
696
- await new Promise((resolve) => setTimeout(resolve, 500));
697
- await graph.emit("event1", { eventPayload: { status: "1" } });
698
- await new Promise((resolve) => setTimeout(resolve, 100));
699
- await graph.emit("event2", { eventPayload: { status: "2" } });
700
- expect(graph.getContext().value).to.equal(42);
701
- });
702
-
703
594
  /**
704
595
  * Tests single event waiting functionality
705
596
  */
@@ -735,4 +626,59 @@ describe("GraphFlow", function () {
735
626
  const result = await resultPromise;
736
627
  expect(result.value).to.equal(6); // 1 (waitingNode) + 5 (finalNode)
737
628
  });
629
+
630
+ // Test de validation des paramètres
631
+ it("should successfully validate params", async () => {
632
+ const graph = new GraphFlow("test", {
633
+ name: "test",
634
+ nodes: [
635
+ {
636
+ name: "validationNode",
637
+ params: z.object({
638
+ value: z.number().max(10),
639
+ }),
640
+ execute: async (
641
+ context: GraphContext<TestSchema>,
642
+ params?: NodeParams
643
+ ) => {
644
+ context.counter = params?.value || 0;
645
+ },
646
+ },
647
+ ],
648
+ schema: TestSchema,
649
+ context: { value: 0, counter: 0, message: "" },
650
+ });
651
+
652
+ // Test avec valeur invalide
653
+ await expect(
654
+ graph.execute("validationNode", { value: 20 })
655
+ ).to.be.rejectedWith("Number must be less than or equal to 10");
656
+ });
657
+
658
+ // Test des paramètres requis
659
+ it("should throw error when required params are missing", async () => {
660
+ const graph = new GraphFlow("test", {
661
+ name: "test",
662
+ nodes: [
663
+ {
664
+ name: "requiredInputNode",
665
+ params: z.object({
666
+ value: z.number(),
667
+ }),
668
+ execute: async (
669
+ context: GraphContext<TestSchema>,
670
+ params?: NodeParams
671
+ ) => {
672
+ context.counter = params?.value || 0;
673
+ },
674
+ },
675
+ ],
676
+ schema: TestSchema,
677
+ context: { value: 0, counter: 0, message: "" },
678
+ });
679
+
680
+ await expect(graph.execute("requiredInputNode")).to.be.rejectedWith(
681
+ "Params required for node"
682
+ );
683
+ });
738
684
  });
@@ -329,4 +329,327 @@ describe("GraphNode", () => {
329
329
  expect(stateChanges.length).to.equal(uniqueChanges.size);
330
330
  expect(stateChanges).to.have.lengthOf(2); // Un pour counter, un pour message
331
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
+ });
510
+
511
+ it("should wait for events before executing node", async () => {
512
+ const nodes = new Map();
513
+ nodes.set("waitForEventsNode", {
514
+ name: "waitForEventsNode",
515
+ waitForEvents: {
516
+ events: ["event1", "event2"],
517
+ timeout: 1000,
518
+ strategy: "all",
519
+ },
520
+ execute: async (context: TestContext) => {
521
+ context.message = "Events received";
522
+ },
523
+ });
524
+
525
+ node = new GraphNode(
526
+ nodes,
527
+ logger,
528
+ eventManager,
529
+ eventSubject,
530
+ stateSubject
531
+ );
532
+
533
+ // Lancer l'exécution du nœud
534
+ const execution = node.executeNode(
535
+ "waitForEventsNode",
536
+ { counter: 0, message: "Hello" },
537
+ null
538
+ );
539
+
540
+ // Simuler les événements après un court délai
541
+ setTimeout(() => {
542
+ eventEmitter.emit("event1", { data: "test1" });
543
+ eventEmitter.emit("event2", { data: "test2" });
544
+ }, 100);
545
+
546
+ await execution;
547
+
548
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
549
+ expect(stateChanges).to.have.lengthOf(1);
550
+ expect(stateChanges[0].payload.newValue).to.equal("Events received");
551
+ });
552
+
553
+ it("should timeout if events are not received", async () => {
554
+ const nodes = new Map();
555
+ nodes.set("timeoutNode", {
556
+ name: "timeoutNode",
557
+ waitForEvents: {
558
+ events: ["event1", "event2"],
559
+ timeout: 100,
560
+ strategy: "all",
561
+ },
562
+ execute: async (context: TestContext) => {
563
+ context.message = "Should not execute";
564
+ },
565
+ });
566
+
567
+ node = new GraphNode(
568
+ nodes,
569
+ logger,
570
+ eventManager,
571
+ eventSubject,
572
+ stateSubject
573
+ );
574
+
575
+ await expect(
576
+ node.executeNode("timeoutNode", { counter: 0, message: "Hello" }, null)
577
+ ).to.be.rejectedWith("Timeout waiting for events");
578
+ });
579
+
580
+ it("should handle partial event reception", async () => {
581
+ const nodes = new Map();
582
+ nodes.set("partialEventsNode", {
583
+ name: "partialEventsNode",
584
+ waitForEvents: {
585
+ events: ["event1", "event2"],
586
+ timeout: 1000,
587
+ strategy: "all",
588
+ },
589
+ execute: async (context: TestContext) => {
590
+ context.message = "All events received";
591
+ },
592
+ });
593
+
594
+ node = new GraphNode(
595
+ nodes,
596
+ logger,
597
+ eventManager,
598
+ eventSubject,
599
+ stateSubject
600
+ );
601
+
602
+ const execution = node.executeNode(
603
+ "partialEventsNode",
604
+ { counter: 0, message: "Hello" },
605
+ null
606
+ );
607
+
608
+ // N'émettre qu'un seul événement
609
+ setTimeout(() => {
610
+ eventEmitter.emit("event1", { data: "test1" });
611
+ }, 100);
612
+
613
+ await expect(execution).to.be.rejectedWith("Timeout waiting for events");
614
+ });
615
+
616
+ it("should handle correlated events", (done) => {
617
+ const nodes = new Map();
618
+ nodes.set("correlatedEventsNode", {
619
+ name: "correlatedEventsNode",
620
+ correlateEvents: {
621
+ events: ["payment", "stock"],
622
+ timeout: 1000,
623
+ correlation: (events: Array<{ type: string; payload?: any }>) => {
624
+ const paymentEvent = events.find((e) => e.type === "payment");
625
+ const stockEvent = events.find((e) => e.type === "stock");
626
+ return paymentEvent?.payload?.id === stockEvent?.payload?.id;
627
+ },
628
+ },
629
+ execute: (context: TestContext) => {
630
+ context.message = "Correlated events received";
631
+ done();
632
+ return Promise.resolve();
633
+ },
634
+ });
635
+
636
+ node = new GraphNode(
637
+ nodes,
638
+ logger,
639
+ eventManager,
640
+ eventSubject,
641
+ stateSubject
642
+ );
643
+
644
+ node.executeNode(
645
+ "correlatedEventsNode",
646
+ { counter: 0, message: "Hello" },
647
+ null
648
+ );
649
+
650
+ setTimeout(() => {
651
+ eventEmitter.emit("payment", { id: "123", status: "completed" });
652
+ eventEmitter.emit("stock", { id: "123", status: "available" });
653
+ }, 100);
654
+ });
332
655
  });