/** * Core Subgraph Tests * * This file implements fundamental tests for the Subgraph class that establish * patterns for the rest of the testing team. These tests cover construction, * basic I/O management, and known issues. */ import { describe, expect, it } from "vitest" import { RecursionError } from "@/infrastructure/RecursionError" import { LGraph, Subgraph } from "@/litegraph" import { createUuidv4 } from "@/utils/uuid" import { subgraphTest } from "./fixtures/subgraphFixtures" import { assertSubgraphStructure, createTestSubgraph, createTestSubgraphData, } from "./fixtures/subgraphHelpers" describe("Subgraph Construction", () => { it("should create a subgraph with minimal data", () => { const subgraph = createTestSubgraph() assertSubgraphStructure(subgraph, { inputCount: 0, outputCount: 0, nodeCount: 0, name: "Test Subgraph", }) expect(subgraph.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) expect(subgraph.inputNode).toBeDefined() expect(subgraph.outputNode).toBeDefined() expect(subgraph.inputNode.id).toBe(-10) expect(subgraph.outputNode.id).toBe(-20) }) it("should require a root graph", () => { const subgraphData = createTestSubgraphData() expect(() => { // @ts-expect-error Testing invalid null parameter new Subgraph(null, subgraphData) }).toThrow("Root graph is required") }) it("should accept custom name and ID", () => { const customId = createUuidv4() const customName = "My Custom Subgraph" const subgraph = createTestSubgraph({ id: customId, name: customName, }) expect(subgraph.id).toBe(customId) expect(subgraph.name).toBe(customName) }) it("should initialize with empty inputs and outputs", () => { const subgraph = createTestSubgraph() expect(subgraph.inputs).toHaveLength(0) expect(subgraph.outputs).toHaveLength(0) expect(subgraph.widgets).toHaveLength(0) }) it("should have properly configured input and output nodes", () => { const subgraph = createTestSubgraph() // Input node should be positioned on the left expect(subgraph.inputNode.pos[0]).toBeLessThan(100) // Output node should be positioned on the right expect(subgraph.outputNode.pos[0]).toBeGreaterThan(300) // Both should reference the subgraph expect(subgraph.inputNode.subgraph).toBe(subgraph) expect(subgraph.outputNode.subgraph).toBe(subgraph) }) }) describe("Subgraph Input/Output Management", () => { subgraphTest("should add a single input", ({ emptySubgraph }) => { const input = emptySubgraph.addInput("test_input", "number") expect(emptySubgraph.inputs).toHaveLength(1) expect(input.name).toBe("test_input") expect(input.type).toBe("number") expect(emptySubgraph.inputs.indexOf(input)).toBe(0) }) subgraphTest("should add a single output", ({ emptySubgraph }) => { const output = emptySubgraph.addOutput("test_output", "string") expect(emptySubgraph.outputs).toHaveLength(1) expect(output.name).toBe("test_output") expect(output.type).toBe("string") expect(emptySubgraph.outputs.indexOf(output)).toBe(0) }) subgraphTest("should maintain correct indices when adding multiple inputs", ({ emptySubgraph }) => { const input1 = emptySubgraph.addInput("input_1", "number") const input2 = emptySubgraph.addInput("input_2", "string") const input3 = emptySubgraph.addInput("input_3", "boolean") expect(emptySubgraph.inputs.indexOf(input1)).toBe(0) expect(emptySubgraph.inputs.indexOf(input2)).toBe(1) expect(emptySubgraph.inputs.indexOf(input3)).toBe(2) expect(emptySubgraph.inputs).toHaveLength(3) }) subgraphTest("should maintain correct indices when adding multiple outputs", ({ emptySubgraph }) => { const output1 = emptySubgraph.addOutput("output_1", "number") const output2 = emptySubgraph.addOutput("output_2", "string") const output3 = emptySubgraph.addOutput("output_3", "boolean") expect(emptySubgraph.outputs.indexOf(output1)).toBe(0) expect(emptySubgraph.outputs.indexOf(output2)).toBe(1) expect(emptySubgraph.outputs.indexOf(output3)).toBe(2) expect(emptySubgraph.outputs).toHaveLength(3) }) subgraphTest("should remove inputs correctly", ({ simpleSubgraph }) => { // Add a second input first simpleSubgraph.addInput("second_input", "string") expect(simpleSubgraph.inputs).toHaveLength(2) // Remove the first input const firstInput = simpleSubgraph.inputs[0] simpleSubgraph.removeInput(firstInput) expect(simpleSubgraph.inputs).toHaveLength(1) expect(simpleSubgraph.inputs[0].name).toBe("second_input") // Verify it's at index 0 in the array expect(simpleSubgraph.inputs.indexOf(simpleSubgraph.inputs[0])).toBe(0) }) subgraphTest("should remove outputs correctly", ({ simpleSubgraph }) => { // Add a second output first simpleSubgraph.addOutput("second_output", "string") expect(simpleSubgraph.outputs).toHaveLength(2) // Remove the first output const firstOutput = simpleSubgraph.outputs[0] simpleSubgraph.removeOutput(firstOutput) expect(simpleSubgraph.outputs).toHaveLength(1) expect(simpleSubgraph.outputs[0].name).toBe("second_output") // Verify it's at index 0 in the array expect(simpleSubgraph.outputs.indexOf(simpleSubgraph.outputs[0])).toBe(0) }) }) describe("Subgraph Serialization", () => { subgraphTest("should serialize empty subgraph", ({ emptySubgraph }) => { const serialized = emptySubgraph.asSerialisable() expect(serialized.version).toBe(1) expect(serialized.id).toBeTruthy() expect(serialized.name).toBe("Empty Test Subgraph") expect(serialized.inputs).toHaveLength(0) expect(serialized.outputs).toHaveLength(0) expect(serialized.nodes).toHaveLength(0) expect(typeof serialized.links).toBe("object") }) subgraphTest("should serialize subgraph with inputs and outputs", ({ simpleSubgraph }) => { const serialized = simpleSubgraph.asSerialisable() expect(serialized.inputs).toHaveLength(1) expect(serialized.outputs).toHaveLength(1) expect(serialized.inputs[0].name).toBe("input") expect(serialized.inputs[0].type).toBe("number") expect(serialized.outputs[0].name).toBe("output") expect(serialized.outputs[0].type).toBe("number") }) subgraphTest("should include input and output nodes in serialization", ({ emptySubgraph }) => { const serialized = emptySubgraph.asSerialisable() expect(serialized.inputNode).toBeDefined() expect(serialized.outputNode).toBeDefined() expect(serialized.inputNode.id).toBe(-10) expect(serialized.outputNode.id).toBe(-20) }) }) describe("Subgraph Known Issues", () => { it.todo("should enforce MAX_NESTED_SUBGRAPHS limit", () => { // This test documents that MAX_NESTED_SUBGRAPHS = 1000 is defined // but not actually enforced anywhere in the code. // // Expected behavior: Should throw error when nesting exceeds limit // Actual behavior: No validation is performed // // This safety limit should be implemented to prevent runaway recursion. }) it("should provide MAX_NESTED_SUBGRAPHS constant", () => { expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000) }) it("should have recursion detection in place", () => { // Verify that RecursionError is available and can be thrown expect(() => { throw new RecursionError("test recursion") }).toThrow(RecursionError) expect(() => { throw new RecursionError("test recursion") }).toThrow("test recursion") }) }) describe("Subgraph Root Graph Relationship", () => { it("should maintain reference to root graph", () => { const rootGraph = new LGraph() const subgraphData = createTestSubgraphData() const subgraph = new Subgraph(rootGraph, subgraphData) expect(subgraph.rootGraph).toBe(rootGraph) }) it("should inherit root graph in nested subgraphs", () => { const rootGraph = new LGraph() const parentData = createTestSubgraphData({ name: "Parent Subgraph", }) const parentSubgraph = new Subgraph(rootGraph, parentData) // Create a nested subgraph const nestedData = createTestSubgraphData({ name: "Nested Subgraph", }) const nestedSubgraph = new Subgraph(rootGraph, nestedData) expect(nestedSubgraph.rootGraph).toBe(rootGraph) expect(parentSubgraph.rootGraph).toBe(rootGraph) }) }) describe("Subgraph Error Handling", () => { subgraphTest("should handle removing non-existent input gracefully", ({ emptySubgraph }) => { // Create a fake input that doesn't belong to this subgraph const fakeInput = emptySubgraph.addInput("temp", "number") emptySubgraph.removeInput(fakeInput) // Remove it first // Now try to remove it again expect(() => { emptySubgraph.removeInput(fakeInput) }).toThrow("Input not found") }) subgraphTest("should handle removing non-existent output gracefully", ({ emptySubgraph }) => { // Create a fake output that doesn't belong to this subgraph const fakeOutput = emptySubgraph.addOutput("temp", "number") emptySubgraph.removeOutput(fakeOutput) // Remove it first // Now try to remove it again expect(() => { emptySubgraph.removeOutput(fakeOutput) }).toThrow("Output not found") }) }) describe("Subgraph Integration", () => { it("should work with LGraph's node management", () => { const subgraph = createTestSubgraph({ nodeCount: 3, }) // Verify nodes were added to the subgraph expect(subgraph.nodes).toHaveLength(3) // Verify we can access nodes by ID const firstNode = subgraph.getNodeById(1) expect(firstNode).toBeDefined() expect(firstNode?.title).toContain("Test Node") }) it("should maintain link integrity", () => { const subgraph = createTestSubgraph({ nodeCount: 2, }) const node1 = subgraph.nodes[0] const node2 = subgraph.nodes[1] // Connect the nodes node1.connect(0, node2, 0) // Verify link was created expect(subgraph.links.size).toBe(1) // Verify link integrity const link = Array.from(subgraph.links.values())[0] expect(link.origin_id).toBe(node1.id) expect(link.target_id).toBe(node2.id) }) })