diff --git a/src/subgraph/Subgraph.ts b/src/subgraph/Subgraph.ts index 9abe36668..b40dfc4d7 100644 --- a/src/subgraph/Subgraph.ts +++ b/src/subgraph/Subgraph.ts @@ -5,7 +5,7 @@ import type { ExportedSubgraph, ExposedWidget, ISerialisedGraph, Serialisable, S import { CustomEventTarget } from "@/infrastructure/CustomEventTarget" import { type BaseLGraph, LGraph } from "@/LGraph" -import { createUuidv4 } from "@/litegraph" +import { createUuidv4 } from "@/utils/uuid" import { SubgraphInput } from "./SubgraphInput" import { SubgraphInputNode } from "./SubgraphInputNode" diff --git a/test/subgraph/Subgraph.test.ts b/test/subgraph/Subgraph.test.ts new file mode 100644 index 000000000..4df94d0c4 --- /dev/null +++ b/test/subgraph/Subgraph.test.ts @@ -0,0 +1,374 @@ +/** + * Core Subgraph Tests + * + * Fundamental tests for the Subgraph class covering construction, + * basic I/O management, and edge cases. + */ + +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, + verifyEventSequence, +} 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 Event System", () => { + subgraphTest("should fire events when adding inputs", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + subgraph.addInput("test_input", "number") + + verifyEventSequence(capture.events, ["adding-input", "input-added"]) + + expect(capture.events[0].detail.name).toBe("test_input") + expect(capture.events[0].detail.type).toBe("number") + expect(capture.events[1].detail.input.name).toBe("test_input") + }) + + subgraphTest("should fire events when adding outputs", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + subgraph.addOutput("test_output", "string") + + verifyEventSequence(capture.events, ["adding-output", "output-added"]) + + expect(capture.events[0].detail.name).toBe("test_output") + expect(capture.events[0].detail.type).toBe("string") + expect(capture.events[1].detail.output.name).toBe("test_output") + }) + + subgraphTest("should fire events when removing inputs", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + // Add an input first + const input = subgraph.addInput("test_input", "number") + capture.clear() // Clear the add events + + // Remove the input + subgraph.removeInput(input) + + verifyEventSequence(capture.events, ["removing-input"]) + expect(capture.events[0].detail.input.name).toBe("test_input") + expect(capture.events[0].detail.index).toBe(0) + }) + + subgraphTest("should fire events when removing outputs", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + // Add an output first + const output = subgraph.addOutput("test_output", "string") + capture.clear() // Clear the add events + + // Remove the output + subgraph.removeOutput(output) + + verifyEventSequence(capture.events, ["removing-output"]) + expect(capture.events[0].detail.output.name).toBe("test_output") + expect(capture.events[0].detail.index).toBe(0) + }) + + subgraphTest("should allow preventing input removal via event", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + // Add an input + const input = subgraph.addInput("protected_input", "number") + + // Add event listener that prevents removal + subgraph.events.addEventListener("removing-input", (event) => { + event.preventDefault() + }) + + capture.clear() + + // Try to remove the input + subgraph.removeInput(input) + + // Input should still exist + expect(subgraph.inputs).toHaveLength(1) + expect(subgraph.inputs[0].name).toBe("protected_input") + + // Event should have been fired but removal prevented + expect(capture.events).toHaveLength(1) + expect(capture.events[0].type).toBe("removing-input") + }) +}) + +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("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) + }) +}) diff --git a/test/subgraph/SubgraphNode.test.ts b/test/subgraph/SubgraphNode.test.ts new file mode 100644 index 000000000..66e70d7dc --- /dev/null +++ b/test/subgraph/SubgraphNode.test.ts @@ -0,0 +1,99 @@ +/** + * SubgraphNode Tests + * + * Tests for SubgraphNode instances including construction, + * IO synchronization, and edge cases. + */ + +import { describe, expect, it } from "vitest" + +import { LGraph } from "@/litegraph" + +import { subgraphTest } from "./fixtures/subgraphFixtures" +import { + createTestSubgraph, + createTestSubgraphNode, +} from "./fixtures/subgraphHelpers" + +describe("SubgraphNode Construction", () => { + it("should create a SubgraphNode from a subgraph definition", () => { + const subgraph = createTestSubgraph({ + name: "Test Definition", + inputs: [{ name: "input", type: "number" }], + outputs: [{ name: "output", type: "number" }], + }) + + const subgraphNode = createTestSubgraphNode(subgraph) + + expect(subgraphNode).toBeDefined() + expect(subgraphNode.subgraph).toBe(subgraph) + expect(subgraphNode.type).toBe(subgraph.id) + expect(subgraphNode.isVirtualNode).toBe(true) + }) + + subgraphTest("should synchronize slots with subgraph definition", ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode + + // SubgraphNode should have same number of inputs/outputs as definition + expect(subgraphNode.inputs).toHaveLength(subgraph.inputs.length) + expect(subgraphNode.outputs).toHaveLength(subgraph.outputs.length) + }) + + subgraphTest("should update slots when subgraph definition changes", ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode + + const initialInputCount = subgraphNode.inputs.length + + // Add an input to the subgraph definition + subgraph.addInput("new_input", "string") + + // SubgraphNode should automatically update (this tests the event system) + expect(subgraphNode.inputs).toHaveLength(initialInputCount + 1) + }) +}) + +describe("SubgraphNode Integration", () => { + it("should be addable to a parent graph", () => { + const subgraph = createTestSubgraph() + const subgraphNode = createTestSubgraphNode(subgraph) + const parentGraph = new LGraph() + + parentGraph.add(subgraphNode) + + expect(parentGraph.nodes).toContain(subgraphNode) + expect(subgraphNode.graph).toBe(parentGraph) + }) + + subgraphTest("should maintain reference to root graph", ({ subgraphWithNode }) => { + const { subgraphNode } = subgraphWithNode + + // For this test, parentGraph should be the root, but in nested scenarios + // it would traverse up to find the actual root + expect(subgraphNode.rootGraph).toBeDefined() + }) +}) + +describe("Foundation Test Utilities", () => { + it("should create test SubgraphNodes with custom options", () => { + const subgraph = createTestSubgraph() + const customPos: [number, number] = [500, 300] + const customSize: [number, number] = [250, 120] + + const subgraphNode = createTestSubgraphNode(subgraph, { + pos: customPos, + size: customSize, + }) + + expect(Array.from(subgraphNode.pos)).toEqual(customPos) + expect(Array.from(subgraphNode.size)).toEqual(customSize) + }) + + subgraphTest("fixtures should provide properly configured SubgraphNode", ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + + expect(subgraph).toBeDefined() + expect(subgraphNode).toBeDefined() + expect(parentGraph).toBeDefined() + expect(parentGraph.nodes).toContain(subgraphNode) + }) +}) diff --git a/test/subgraph/fixtures/README.md b/test/subgraph/fixtures/README.md new file mode 100644 index 000000000..ca5a11dac --- /dev/null +++ b/test/subgraph/fixtures/README.md @@ -0,0 +1,239 @@ +# Subgraph Testing Fixtures and Utilities + +Testing infrastructure for LiteGraph's subgraph functionality. A subgraph is a graph-within-a-graph that can be reused as a single node, with input/output slots mapping to internal IO nodes. + +## Quick Start + +```typescript +// Import what you need +import { createTestSubgraph, assertSubgraphStructure } from "./fixtures/subgraphHelpers" +import { subgraphTest } from "./fixtures/subgraphFixtures" + +// Option 1: Create a subgraph manually +it("should do something", () => { + const subgraph = createTestSubgraph({ + name: "My Test Subgraph", + inputCount: 2, + outputCount: 1 + }) + + // Test your functionality + expect(subgraph.inputs).toHaveLength(2) +}) + +// Option 2: Use pre-configured fixtures +subgraphTest("should handle events", ({ simpleSubgraph }) => { + // simpleSubgraph comes pre-configured with 1 input, 1 output, and 2 nodes + expect(simpleSubgraph.inputs).toHaveLength(1) +}) +``` + +## Files Overview + +### `subgraphHelpers.ts` - Core Helper Functions + +**Main Factory Functions:** +- `createTestSubgraph(options?)` - Creates a fully configured Subgraph instance with root graph +- `createTestSubgraphNode(subgraph, options?)` - Creates a SubgraphNode (instance of a subgraph) +- `createNestedSubgraphs(options?)` - Creates nested subgraph hierarchies for testing deep structures + +**Assertion & Validation:** +- `assertSubgraphStructure(subgraph, expected)` - Validates subgraph has expected inputs/outputs/nodes +- `verifyEventSequence(events, expectedSequence)` - Ensures events fired in correct order +- `logSubgraphStructure(subgraph, label?)` - Debug helper to print subgraph structure + +**Test Data & Events:** +- `createTestSubgraphData(overrides?)` - Creates raw ExportedSubgraph data for serialization tests +- `createComplexSubgraphData(nodeCount?)` - Generates complex subgraph with internal connections +- `createEventCapture(eventTarget, eventTypes)` - Sets up event monitoring with automatic cleanup + +### `subgraphFixtures.ts` - Vitest Fixtures + +Pre-configured test scenarios that automatically set up and tear down: + +**Basic Fixtures (`subgraphTest`):** +- `emptySubgraph` - Minimal subgraph with no inputs/outputs/nodes +- `simpleSubgraph` - 1 input ("input": number), 1 output ("output": number), 2 internal nodes +- `complexSubgraph` - 3 inputs (data, control, text), 2 outputs (result, status), 5 nodes +- `nestedSubgraph` - 3-level deep hierarchy with 2 nodes per level +- `subgraphWithNode` - Complete setup: subgraph definition + SubgraphNode instance + parent graph +- `eventCapture` - Subgraph with event monitoring for all I/O events + +**Edge Case Fixtures (`edgeCaseTest`):** +- `circularSubgraph` - Two subgraphs set up for circular reference testing +- `deeplyNestedSubgraph` - 50 levels deep for performance/limit testing +- `maxIOSubgraph` - 20 inputs and 20 outputs for stress testing + +### `testSubgraphs.json` - Sample Test Data +Pre-defined subgraph configurations for consistent testing across different scenarios. + +**Note on Static UUIDs**: The hardcoded UUIDs in this file (e.g., "simple-subgraph-uuid", "complex-subgraph-uuid") are intentionally static to ensure test reproducibility and snapshot testing compatibility. + +## Usage Examples + +### Basic Test Creation + +```typescript +import { describe, expect, it } from "vitest" +import { createTestSubgraph, assertSubgraphStructure } from "./fixtures/subgraphHelpers" + +describe("My Subgraph Feature", () => { + it("should work correctly", () => { + const subgraph = createTestSubgraph({ + name: "My Test", + inputCount: 2, + outputCount: 1, + nodeCount: 3 + }) + + assertSubgraphStructure(subgraph, { + inputCount: 2, + outputCount: 1, + nodeCount: 3, + name: "My Test" + }) + + // Your specific test logic... + }) +}) +``` + +### Using Fixtures + +```typescript +import { subgraphTest } from "./fixtures/subgraphFixtures" + +subgraphTest("should handle events", ({ eventCapture }) => { + const { subgraph, capture } = eventCapture + + subgraph.addInput("test", "number") + + expect(capture.events).toHaveLength(2) // adding-input, input-added +}) +``` + +### Event Testing + +```typescript +import { createEventCapture, verifyEventSequence } from "./fixtures/subgraphHelpers" + +it("should fire events in correct order", () => { + const subgraph = createTestSubgraph() + const capture = createEventCapture(subgraph.events, ["adding-input", "input-added"]) + + subgraph.addInput("test", "number") + + verifyEventSequence(capture.events, ["adding-input", "input-added"]) + + capture.cleanup() // Important: clean up listeners +}) +``` + +### Nested Structure Testing + +```typescript +import { createNestedSubgraphs } from "./fixtures/subgraphHelpers" + +it("should handle deep nesting", () => { + const nested = createNestedSubgraphs({ + depth: 5, + nodesPerLevel: 2 + }) + + expect(nested.subgraphs).toHaveLength(5) + expect(nested.leafSubgraph.nodes).toHaveLength(2) +}) +``` + +## Common Patterns + +### Testing SubgraphNode Instances + +```typescript +it("should create and configure a SubgraphNode", () => { + // First create the subgraph definition + const subgraph = createTestSubgraph({ + inputs: [{ name: "value", type: "number" }], + outputs: [{ name: "result", type: "number" }] + }) + + // Then create an instance of it + const subgraphNode = createTestSubgraphNode(subgraph, { + pos: [100, 200], + size: [180, 100] + }) + + // The SubgraphNode will have matching slots + expect(subgraphNode.inputs).toHaveLength(1) + expect(subgraphNode.outputs).toHaveLength(1) + expect(subgraphNode.subgraph).toBe(subgraph) +}) +``` + +### Complete Test with Parent Graph + +```typescript +subgraphTest("should work in a parent graph", ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + + // Everything is pre-configured and connected + expect(parentGraph.nodes).toContain(subgraphNode) + expect(subgraphNode.graph).toBe(parentGraph) + expect(subgraphNode.subgraph).toBe(subgraph) +}) +``` + +## Configuration Options + +### `createTestSubgraph(options)` +```typescript +interface TestSubgraphOptions { + id?: UUID // Custom UUID + name?: string // Custom name + nodeCount?: number // Number of internal nodes + inputCount?: number // Number of inputs (uses generic types) + outputCount?: number // Number of outputs (uses generic types) + inputs?: Array<{ // Specific input definitions + name: string + type: ISlotType + }> + outputs?: Array<{ // Specific output definitions + name: string + type: ISlotType + }> +} +``` + +**Note**: Cannot specify both `inputs` array and `inputCount` (or `outputs` array and `outputCount`) - the function will throw an error with details. + +### `createNestedSubgraphs(options)` +```typescript +interface NestedSubgraphOptions { + depth?: number // Nesting depth (default: 2) + nodesPerLevel?: number // Nodes per subgraph (default: 2) + inputsPerSubgraph?: number // Inputs per subgraph (default: 1) + outputsPerSubgraph?: number // Outputs per subgraph (default: 1) +} +``` + +## Important Architecture Notes + +### Subgraph vs SubgraphNode +- **Subgraph**: The definition/template (like a class definition) +- **SubgraphNode**: An instance of a subgraph placed in a graph (like a class instance) +- One Subgraph can have many SubgraphNode instances + +### Special Node IDs +- Input node always has ID `-10` (SUBGRAPH_INPUT_ID) +- Output node always has ID `-20` (SUBGRAPH_OUTPUT_ID) +- These are virtual nodes that exist in every subgraph + +### Common Pitfalls + +1. **Array items don't have index property** - Use `indexOf()` instead +2. **IO nodes have `subgraph` property** - Not `graph` like regular nodes +3. **Links are stored in a Map** - Use `.size` not `.length` +4. **Event detail structures** - Check exact property names: + - `"adding-input"`: `{ name, type }` + - `"input-added"`: `{ input, index }` + diff --git a/test/subgraph/fixtures/subgraphFixtures.ts b/test/subgraph/fixtures/subgraphFixtures.ts new file mode 100644 index 000000000..5c6fc469d --- /dev/null +++ b/test/subgraph/fixtures/subgraphFixtures.ts @@ -0,0 +1,293 @@ +/** + * Vitest Fixtures for Subgraph Testing + * + * Reusable Vitest fixtures for subgraph testing. + * Each fixture provides a clean, pre-configured subgraph + * setup for different testing scenarios. + */ + +import { LGraph, Subgraph } from "@/litegraph" +import { SubgraphNode } from "@/subgraph/SubgraphNode" + +import { test } from "../../testExtensions" +import { + createEventCapture, + createNestedSubgraphs, + createTestSubgraph, + createTestSubgraphNode, +} from "./subgraphHelpers" + +export interface SubgraphFixtures { + /** A minimal subgraph with no inputs, outputs, or nodes */ + emptySubgraph: Subgraph + + /** A simple subgraph with 1 input and 1 output */ + simpleSubgraph: Subgraph + + /** A complex subgraph with multiple inputs, outputs, and internal nodes */ + complexSubgraph: Subgraph + + /** A nested subgraph structure (3 levels deep) */ + nestedSubgraph: ReturnType + + /** A subgraph with its corresponding SubgraphNode instance */ + subgraphWithNode: { + subgraph: Subgraph + subgraphNode: SubgraphNode + parentGraph: LGraph + } + + /** Event capture system for testing subgraph events */ + eventCapture: { + subgraph: Subgraph + capture: ReturnType + } +} + +/** + * Extended test with subgraph fixtures. + * Use this instead of the base `test` for subgraph testing. + * @example + * ```typescript + * import { subgraphTest } from "./fixtures/subgraphFixtures" + * + * subgraphTest("should handle simple operations", ({ simpleSubgraph }) => { + * expect(simpleSubgraph.inputs.length).toBe(1) + * expect(simpleSubgraph.outputs.length).toBe(1) + * }) + * ``` + */ +export const subgraphTest = test.extend({ + + emptySubgraph: async ({ }, use: (value: unknown) => Promise) => { + const subgraph = createTestSubgraph({ + name: "Empty Test Subgraph", + inputCount: 0, + outputCount: 0, + nodeCount: 0, + }) + + await use(subgraph) + }, + + simpleSubgraph: async ({ }, use: (value: unknown) => Promise) => { + const subgraph = createTestSubgraph({ + name: "Simple Test Subgraph", + inputs: [{ name: "input", type: "number" }], + outputs: [{ name: "output", type: "number" }], + nodeCount: 2, + }) + + await use(subgraph) + }, + + complexSubgraph: async ({ }, use: (value: unknown) => Promise) => { + const subgraph = createTestSubgraph({ + name: "Complex Test Subgraph", + inputs: [ + { name: "data", type: "number" }, + { name: "control", type: "boolean" }, + { name: "text", type: "string" }, + ], + outputs: [ + { name: "result", type: "number" }, + { name: "status", type: "boolean" }, + ], + nodeCount: 5, + }) + + await use(subgraph) + }, + + nestedSubgraph: async ({ }, use: (value: unknown) => Promise) => { + const nested = createNestedSubgraphs({ + depth: 3, + nodesPerLevel: 2, + inputsPerSubgraph: 1, + outputsPerSubgraph: 1, + }) + + await use(nested) + }, + + subgraphWithNode: async ({ }, use: (value: unknown) => Promise) => { + // Create the subgraph definition + const subgraph = createTestSubgraph({ + name: "Subgraph With Node", + inputs: [{ name: "input", type: "*" }], + outputs: [{ name: "output", type: "*" }], + nodeCount: 1, + }) + + // Create the parent graph and subgraph node instance + const parentGraph = new LGraph() + const subgraphNode = createTestSubgraphNode(subgraph, { + pos: [200, 200], + size: [180, 80], + }) + + // Add the subgraph node to the parent graph + parentGraph.add(subgraphNode) + + await use({ + subgraph, + subgraphNode, + parentGraph, + }) + }, + + eventCapture: async ({ }, use: (value: unknown) => Promise) => { + const subgraph = createTestSubgraph({ + name: "Event Test Subgraph", + }) + + // Set up event capture for all subgraph events + const capture = createEventCapture(subgraph.events, [ + "adding-input", + "input-added", + "removing-input", + "renaming-input", + "adding-output", + "output-added", + "removing-output", + "renaming-output", + ]) + + await use({ subgraph, capture }) + + // Cleanup event listeners + capture.cleanup() + }, +}) + +/** + * Fixtures that test edge cases and error conditions. + * These may leave the system in an invalid state and should be used carefully. + */ +export interface EdgeCaseFixtures { + /** Subgraph with circular references (for testing recursion detection) */ + circularSubgraph: { + rootGraph: LGraph + subgraphA: Subgraph + subgraphB: Subgraph + nodeA: SubgraphNode + nodeB: SubgraphNode + } + + /** Deeply nested subgraphs approaching the theoretical limit */ + deeplyNestedSubgraph: ReturnType + + /** Subgraph with maximum inputs and outputs */ + maxIOSubgraph: Subgraph +} + +/** + * Test with edge case fixtures. Use sparingly and with caution. + * These tests may intentionally create invalid states. + */ +export const edgeCaseTest = subgraphTest.extend({ + + circularSubgraph: async ({ }, use: (value: unknown) => Promise) => { + const rootGraph = new LGraph() + + // Create two subgraphs that will reference each other + const subgraphA = createTestSubgraph({ + name: "Subgraph A", + inputs: [{ name: "input", type: "*" }], + outputs: [{ name: "output", type: "*" }], + }) + + const subgraphB = createTestSubgraph({ + name: "Subgraph B", + inputs: [{ name: "input", type: "*" }], + outputs: [{ name: "output", type: "*" }], + }) + + // Create instances (this doesn't create circular refs by itself) + const nodeA = createTestSubgraphNode(subgraphA, { pos: [100, 100] }) + const nodeB = createTestSubgraphNode(subgraphB, { pos: [300, 100] }) + + // Add nodes to root graph + rootGraph.add(nodeA) + rootGraph.add(nodeB) + + await use({ + rootGraph, + subgraphA, + subgraphB, + nodeA, + nodeB, + }) + }, + + deeplyNestedSubgraph: async ({ }, use: (value: unknown) => Promise) => { + // Create a very deep nesting structure (but not exceeding MAX_NESTED_SUBGRAPHS) + const nested = createNestedSubgraphs({ + depth: 50, // Deep but reasonable + nodesPerLevel: 1, + inputsPerSubgraph: 1, + outputsPerSubgraph: 1, + }) + + await use(nested) + }, + + maxIOSubgraph: async ({ }, use: (value: unknown) => Promise) => { + // Create a subgraph with many inputs and outputs + const inputs = Array.from({ length: 20 }, (_, i) => ({ + name: `input_${i}`, + type: i % 2 === 0 ? "number" : "string" as const, + })) + + const outputs = Array.from({ length: 20 }, (_, i) => ({ + name: `output_${i}`, + type: i % 2 === 0 ? "number" : "string" as const, + })) + + const subgraph = createTestSubgraph({ + name: "Max IO Subgraph", + inputs, + outputs, + nodeCount: 10, + }) + + await use(subgraph) + }, +}) + +/** + * Helper to verify fixture integrity. + * Use this in tests to ensure fixtures are properly set up. + */ +export function verifyFixtureIntegrity>( + fixture: T, + expectedProperties: (keyof T)[], +): void { + for (const prop of expectedProperties) { + if (!(prop in fixture)) { + throw new Error(`Fixture missing required property: ${String(prop)}`) + } + if (fixture[prop] === undefined || fixture[prop] === null) { + throw new Error(`Fixture property ${String(prop)} is null or undefined`) + } + } +} + +/** + * Creates a snapshot-friendly representation of a subgraph for testing. + * Useful for serialization tests and regression detection. + */ +export function createSubgraphSnapshot(subgraph: Subgraph) { + return { + id: subgraph.id, + name: subgraph.name, + inputCount: subgraph.inputs.length, + outputCount: subgraph.outputs.length, + nodeCount: subgraph.nodes.length, + linkCount: subgraph.links.size, + inputs: subgraph.inputs.map(i => ({ name: i.name, type: i.type })), + outputs: subgraph.outputs.map(o => ({ name: o.name, type: o.type })), + hasInputNode: !!subgraph.inputNode, + hasOutputNode: !!subgraph.outputNode, + } +} diff --git a/test/subgraph/fixtures/subgraphHelpers.ts b/test/subgraph/fixtures/subgraphHelpers.ts new file mode 100644 index 000000000..7c7c8458a --- /dev/null +++ b/test/subgraph/fixtures/subgraphHelpers.ts @@ -0,0 +1,487 @@ +/** + * Test Helper Functions for Subgraph Testing + * + * Core utilities for creating and testing subgraphs. + * Provides consistent APIs for test subgraph creation, node management, + * and behavior verification. + */ + +import type { ISlotType, NodeId } from "@/litegraph" +import type { ExportedSubgraph, ExportedSubgraphInstance } from "@/types/serialisation" +import type { UUID } from "@/utils/uuid" + +import { expect } from "vitest" + +import { LGraph, LGraphNode, Subgraph } from "@/litegraph" +import { SubgraphNode } from "@/subgraph/SubgraphNode" +import { createUuidv4 } from "@/utils/uuid" + +export interface TestSubgraphOptions { + id?: UUID + name?: string + nodeCount?: number + inputCount?: number + outputCount?: number + inputs?: Array<{ name: string, type: ISlotType }> + outputs?: Array<{ name: string, type: ISlotType }> +} + +export interface TestSubgraphNodeOptions { + id?: NodeId + pos?: [number, number] + size?: [number, number] +} + +export interface NestedSubgraphOptions { + depth?: number + nodesPerLevel?: number + inputsPerSubgraph?: number + outputsPerSubgraph?: number +} + +export interface SubgraphStructureExpectation { + inputCount?: number + outputCount?: number + nodeCount?: number + name?: string + hasInputNode?: boolean + hasOutputNode?: boolean +} + +export interface CapturedEvent { + type: string + detail: T + timestamp: number +} + +/** + * Creates a test subgraph with the specified configuration. + * This is the primary function for creating test subgraphs. + * @param options Configuration options for the subgraph + * @returns A configured Subgraph instance + * @example + * ```typescript + * const subgraph = createTestSubgraph({ + * name: "My Test Subgraph", + * inputCount: 2, + * outputCount: 1 + * }) + * ``` + */ +export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph { + // Validate options - cannot specify both inputs array and inputCount + if (options.inputs && options.inputCount) { + throw new Error(`Cannot specify both 'inputs' array and 'inputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`) + } + + // Validate options - cannot specify both outputs array and outputCount + if (options.outputs && options.outputCount) { + throw new Error(`Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`) + } + + const rootGraph = new LGraph() + + // Create the base subgraph data + const subgraphData: ExportedSubgraph = { + // Basic graph properties + version: 1, + nodes: [], + links: {}, + groups: [], + config: {}, + definitions: { subgraphs: [] }, + + // Subgraph-specific properties + id: options.id || createUuidv4(), + name: options.name || "Test Subgraph", + + // IO Nodes (required for subgraph functionality) + inputNode: { + id: -10, // SUBGRAPH_INPUT_ID + bounding: [10, 100, 150, 126], // [x, y, width, height] + pinned: false, + }, + outputNode: { + id: -20, // SUBGRAPH_OUTPUT_ID + bounding: [400, 100, 140, 126], // [x, y, width, height] + pinned: false, + }, + + // IO definitions - will be populated by addInput/addOutput calls + inputs: [], + outputs: [], + widgets: [], + } + + // Create the subgraph + const subgraph = new Subgraph(rootGraph, subgraphData) + + // Add requested inputs + if (options.inputs) { + for (const input of options.inputs) { + subgraph.addInput(input.name, input.type) + } + } else if (options.inputCount) { + for (let i = 0; i < options.inputCount; i++) { + subgraph.addInput(`input_${i}`, "*") + } + } + + // Add requested outputs + if (options.outputs) { + for (const output of options.outputs) { + subgraph.addOutput(output.name, output.type) + } + } else if (options.outputCount) { + for (let i = 0; i < options.outputCount; i++) { + subgraph.addOutput(`output_${i}`, "*") + } + } + + // Add test nodes if requested + if (options.nodeCount) { + for (let i = 0; i < options.nodeCount; i++) { + const node = new LGraphNode(`Test Node ${i}`) + node.addInput("in", "*") + node.addOutput("out", "*") + subgraph.add(node) + } + } + + return subgraph +} + +/** + * Creates a SubgraphNode instance from a subgraph definition. + * @param subgraph The subgraph definition to instantiate + * @param options Configuration options for the node instance + * @returns A SubgraphNode instance + * @example + * ```typescript + * const subgraph = createTestSubgraph() + * const subgraphNode = createTestSubgraphNode(subgraph, { + * pos: [100, 200] + * }) + * ``` + */ +export function createTestSubgraphNode( + subgraph: Subgraph, + options: TestSubgraphNodeOptions = {}, +): SubgraphNode { + const parentGraph = new LGraph() + + const instanceData: ExportedSubgraphInstance = { + id: options.id || 1, + type: subgraph.id, + pos: options.pos || [100, 100], + size: options.size || [200, 100], + inputs: [], + outputs: [], + properties: {}, + flags: {}, + mode: 0, + } + + return new SubgraphNode(parentGraph, subgraph, instanceData) +} + +/** + * Creates a nested hierarchy of subgraphs for testing deep nesting scenarios. + * @param options Configuration for the nested structure + * @returns Object containing the root graph and all created subgraphs + * @example + * ```typescript + * const nested = createNestedSubgraphs({ depth: 3, nodesPerLevel: 2 }) + * // Creates: Root -> Subgraph1 -> Subgraph2 -> Subgraph3 + * ``` + */ +export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) { + const { + depth = 2, + nodesPerLevel = 2, + inputsPerSubgraph = 1, + outputsPerSubgraph = 1, + } = options + + const rootGraph = new LGraph() + const subgraphs: Subgraph[] = [] + const subgraphNodes: SubgraphNode[] = [] + + let currentParent = rootGraph + + for (let level = 0; level < depth; level++) { + // Create subgraph for this level + const subgraph = createTestSubgraph({ + name: `Level ${level} Subgraph`, + nodeCount: nodesPerLevel, + inputCount: inputsPerSubgraph, + outputCount: outputsPerSubgraph, + }) + + subgraphs.push(subgraph) + + // Create instance in parent + const subgraphNode = createTestSubgraphNode(subgraph, { + pos: [100 + level * 200, 100], + }) + + if (currentParent instanceof LGraph) { + currentParent.add(subgraphNode) + } else { + currentParent.add(subgraphNode) + } + + subgraphNodes.push(subgraphNode) + + // Next level will be nested inside this subgraph + currentParent = subgraph + } + + return { + rootGraph, + subgraphs, + subgraphNodes, + depth, + leafSubgraph: subgraphs.at(-1), + } +} + +/** + * Asserts that a subgraph has the expected structure. + * This provides consistent validation across all tests. + * @param subgraph The subgraph to validate + * @param expected The expected structure + * @example + * ```typescript + * assertSubgraphStructure(subgraph, { + * inputCount: 2, + * outputCount: 1, + * name: "Expected Name" + * }) + * ``` + */ +export function assertSubgraphStructure( + subgraph: Subgraph, + expected: SubgraphStructureExpectation, +): void { + if (expected.inputCount !== undefined) { + expect(subgraph.inputs.length).toBe(expected.inputCount) + } + + if (expected.outputCount !== undefined) { + expect(subgraph.outputs.length).toBe(expected.outputCount) + } + + if (expected.nodeCount !== undefined) { + expect(subgraph.nodes.length).toBe(expected.nodeCount) + } + + if (expected.name !== undefined) { + expect(subgraph.name).toBe(expected.name) + } + + if (expected.hasInputNode !== false) { + expect(subgraph.inputNode).toBeDefined() + expect(subgraph.inputNode.id).toBe(-10) + } + + if (expected.hasOutputNode !== false) { + expect(subgraph.outputNode).toBeDefined() + expect(subgraph.outputNode.id).toBe(-20) + } +} + +/** + * Verifies that events were fired in the expected sequence. + * Useful for testing event-driven behavior. + * @param capturedEvents Array of captured events + * @param expectedSequence Expected sequence of event types + * @example + * ```typescript + * verifyEventSequence(events, [ + * "adding-input", + * "input-added", + * "adding-output", + * "output-added" + * ]) + * ``` + */ +export function verifyEventSequence( + capturedEvents: CapturedEvent[], + expectedSequence: string[], +): void { + expect(capturedEvents.length).toBe(expectedSequence.length) + + for (const [i, element] of expectedSequence.entries()) { + expect(capturedEvents[i].type).toBe(element) + } + + // Verify timestamps are in order + for (let i = 1; i < capturedEvents.length; i++) { + expect(capturedEvents[i].timestamp).toBeGreaterThanOrEqual( + capturedEvents[i - 1].timestamp, + ) + } +} + +/** + * Creates test subgraph data with optional overrides. + * Useful for serialization/deserialization tests. + * @param overrides Properties to override in the default data + * @returns ExportedSubgraph data structure + */ +export function createTestSubgraphData(overrides: Partial = {}): ExportedSubgraph { + return { + version: 1, + nodes: [], + links: {}, + groups: [], + config: {}, + definitions: { subgraphs: [] }, + + id: createUuidv4(), + name: "Test Data Subgraph", + + inputNode: { + id: -10, + bounding: [10, 100, 150, 126], + pinned: false, + }, + outputNode: { + id: -20, + bounding: [400, 100, 140, 126], + pinned: false, + }, + + inputs: [], + outputs: [], + widgets: [], + + ...overrides, + } +} + +/** + * Creates a complex subgraph with multiple nodes and connections. + * Useful for testing realistic scenarios. + * @param nodeCount Number of internal nodes to create + * @returns Complex subgraph data structure + */ +export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgraph { + const nodes = [] + const links: Record = {} + + // Create internal nodes + for (let i = 0; i < nodeCount; i++) { + nodes.push({ + id: i + 1, // Start from 1 to avoid conflicts with IO nodes + type: "basic/test", + pos: [100 + i * 150, 200], + size: [120, 60], + inputs: [{ name: "in", type: "*", link: null }], + outputs: [{ name: "out", type: "*", links: [] }], + properties: { value: i }, + flags: {}, + mode: 0, + }) + } + + // Create some internal links + for (let i = 0; i < nodeCount - 1; i++) { + const linkId = i + 1 + links[linkId] = { + id: linkId, + origin_id: i + 1, + origin_slot: 0, + target_id: i + 2, + target_slot: 0, + type: "*", + } + } + + return createTestSubgraphData({ + nodes, + links, + inputs: [ + { name: "input1", type: "number", pos: [0, 0] }, + { name: "input2", type: "string", pos: [0, 1] }, + ], + outputs: [ + { name: "output1", type: "number", pos: [0, 0] }, + { name: "output2", type: "string", pos: [0, 1] }, + ], + }) +} + +/** + * Creates an event capture system for testing event sequences. + * @param eventTarget The event target to monitor + * @param eventTypes Array of event types to capture + * @returns Object with captured events and helper methods + */ +export function createEventCapture( + eventTarget: EventTarget, + eventTypes: string[], +) { + const capturedEvents: CapturedEvent[] = [] + const listeners: Array<() => void> = [] + + // Set up listeners for each event type + for (const eventType of eventTypes) { + const listener = (event: Event) => { + capturedEvents.push({ + type: eventType, + detail: (event as CustomEvent).detail, + timestamp: Date.now(), + }) + } + + eventTarget.addEventListener(eventType, listener) + listeners.push(() => eventTarget.removeEventListener(eventType, listener)) + } + + return { + events: capturedEvents, + clear: () => { capturedEvents.length = 0 }, + cleanup: () => { + // Remove all event listeners to prevent memory leaks + for (const cleanup of listeners) cleanup() + }, + getEventsByType: (type: string) => capturedEvents.filter(e => e.type === type), + } +} + +/** + * Utility to log subgraph structure for debugging tests. + * @param subgraph The subgraph to inspect + * @param label Optional label for the log output + */ +export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgraph"): void { + console.log(`\n=== ${label} Structure ===`) + console.log(`Name: ${subgraph.name}`) + console.log(`ID: ${subgraph.id}`) + console.log(`Inputs: ${subgraph.inputs.length}`) + console.log(`Outputs: ${subgraph.outputs.length}`) + console.log(`Nodes: ${subgraph.nodes.length}`) + console.log(`Links: ${subgraph.links.size}`) + + if (subgraph.inputs.length > 0) { + console.log("Input details:", subgraph.inputs.map(i => ({ name: i.name, type: i.type }))) + } + + if (subgraph.outputs.length > 0) { + console.log("Output details:", subgraph.outputs.map(o => ({ name: o.name, type: o.type }))) + } + + console.log("========================\n") +} + +// Re-export expect from vitest for convenience +export { expect } from "vitest" diff --git a/test/subgraph/fixtures/testSubgraphs.json b/test/subgraph/fixtures/testSubgraphs.json new file mode 100644 index 000000000..afce66a3b --- /dev/null +++ b/test/subgraph/fixtures/testSubgraphs.json @@ -0,0 +1,444 @@ +{ + "simpleSubgraph": { + "version": 1, + "nodes": [ + { + "id": 1, + "type": "basic/math", + "pos": [200, 150], + "size": [120, 60], + "inputs": [ + { "name": "a", "type": "number", "link": null }, + { "name": "b", "type": "number", "link": null } + ], + "outputs": [ + { "name": "result", "type": "number", "links": [] } + ], + "properties": { "operation": "add" }, + "flags": {}, + "mode": 0 + } + ], + "links": {}, + "groups": [], + "config": {}, + "definitions": { "subgraphs": [] }, + + "id": "simple-subgraph-uuid", + "name": "Simple Math Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [400, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [ + { + "name": "input_a", + "type": "number", + "pos": [0, 0] + }, + { + "name": "input_b", + "type": "number", + "pos": [0, 1] + } + ], + "outputs": [ + { + "name": "result", + "type": "number", + "pos": [0, 0] + } + ], + "widgets": [] + }, + + "complexSubgraph": { + "version": 1, + "nodes": [ + { + "id": 1, + "type": "math/multiply", + "pos": [150, 100], + "size": [120, 60], + "inputs": [ + { "name": "a", "type": "number", "link": null }, + { "name": "b", "type": "number", "link": null } + ], + "outputs": [ + { "name": "result", "type": "number", "links": [1] } + ], + "properties": {}, + "flags": {}, + "mode": 0 + }, + { + "id": 2, + "type": "math/add", + "pos": [300, 100], + "size": [120, 60], + "inputs": [ + { "name": "a", "type": "number", "link": 1 }, + { "name": "b", "type": "number", "link": null } + ], + "outputs": [ + { "name": "result", "type": "number", "links": [2] } + ], + "properties": {}, + "flags": {}, + "mode": 0 + }, + { + "id": 3, + "type": "logic/compare", + "pos": [150, 200], + "size": [120, 60], + "inputs": [ + { "name": "a", "type": "number", "link": null }, + { "name": "b", "type": "number", "link": null } + ], + "outputs": [ + { "name": "result", "type": "boolean", "links": [] } + ], + "properties": { "operation": "greater_than" }, + "flags": {}, + "mode": 0 + }, + { + "id": 4, + "type": "string/concat", + "pos": [300, 200], + "size": [120, 60], + "inputs": [ + { "name": "a", "type": "string", "link": null }, + { "name": "b", "type": "string", "link": null } + ], + "outputs": [ + { "name": "result", "type": "string", "links": [] } + ], + "properties": {}, + "flags": {}, + "mode": 0 + } + ], + "links": { + "1": { + "id": 1, + "origin_id": 1, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "number" + }, + "2": { + "id": 2, + "origin_id": 2, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "number" + } + }, + "groups": [], + "config": {}, + "definitions": { "subgraphs": [] }, + + "id": "complex-subgraph-uuid", + "name": "Complex Processing Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 150], + "size": [140, 86], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [450, 150], + "size": [140, 66], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [ + { + "name": "number1", + "type": "number", + "pos": [0, 0] + }, + { + "name": "number2", + "type": "number", + "pos": [0, 1] + }, + { + "name": "text1", + "type": "string", + "pos": [0, 2] + }, + { + "name": "text2", + "type": "string", + "pos": [0, 3] + } + ], + "outputs": [ + { + "name": "calculated_result", + "type": "number", + "pos": [0, 0] + }, + { + "name": "comparison_result", + "type": "boolean", + "pos": [0, 1] + }, + { + "name": "concatenated_text", + "type": "string", + "pos": [0, 2] + } + ], + "widgets": [] + }, + + "nestedSubgraphLevel1": { + "version": 1, + "nodes": [], + "links": {}, + "groups": [], + "config": {}, + "definitions": { + "subgraphs": [ + { + "version": 1, + "nodes": [ + { + "id": 1, + "type": "basic/constant", + "pos": [200, 100], + "size": [100, 40], + "inputs": [], + "outputs": [ + { "name": "value", "type": "number", "links": [] } + ], + "properties": { "value": 42 }, + "flags": {}, + "mode": 0 + } + ], + "links": {}, + "groups": [], + "config": {}, + "definitions": { "subgraphs": [] }, + + "id": "nested-level2-uuid", + "name": "Level 2 Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [350, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [], + "outputs": [ + { + "name": "constant_value", + "type": "number", + "pos": [0, 0] + } + ], + "widgets": [] + } + ] + }, + + "id": "nested-level1-uuid", + "name": "Level 1 Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [400, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [ + { + "name": "external_input", + "type": "string", + "pos": [0, 0] + } + ], + "outputs": [ + { + "name": "processed_output", + "type": "number", + "pos": [0, 0] + } + ], + "widgets": [] + }, + + "emptySubgraph": { + "version": 1, + "nodes": [], + "links": {}, + "groups": [], + "config": {}, + "definitions": { "subgraphs": [] }, + + "id": "empty-subgraph-uuid", + "name": "Empty Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [400, 100], + "size": [140, 26], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [], + "outputs": [], + "widgets": [] + }, + + "maxIOSubgraph": { + "version": 1, + "nodes": [], + "links": {}, + "groups": [], + "config": {}, + "definitions": { "subgraphs": [] }, + + "id": "max-io-subgraph-uuid", + "name": "Max I/O Subgraph", + + "inputNode": { + "id": -10, + "type": "subgraph/input", + "pos": [10, 100], + "size": [140, 200], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + "outputNode": { + "id": -20, + "type": "subgraph/output", + "pos": [400, 100], + "size": [140, 200], + "inputs": [], + "outputs": [], + "properties": {}, + "flags": {}, + "mode": 0 + }, + + "inputs": [ + { "name": "input_0", "type": "number", "pos": [0, 0] }, + { "name": "input_1", "type": "string", "pos": [0, 1] }, + { "name": "input_2", "type": "boolean", "pos": [0, 2] }, + { "name": "input_3", "type": "number", "pos": [0, 3] }, + { "name": "input_4", "type": "string", "pos": [0, 4] }, + { "name": "input_5", "type": "boolean", "pos": [0, 5] }, + { "name": "input_6", "type": "number", "pos": [0, 6] }, + { "name": "input_7", "type": "string", "pos": [0, 7] }, + { "name": "input_8", "type": "boolean", "pos": [0, 8] }, + { "name": "input_9", "type": "number", "pos": [0, 9] } + ], + "outputs": [ + { "name": "output_0", "type": "number", "pos": [0, 0] }, + { "name": "output_1", "type": "string", "pos": [0, 1] }, + { "name": "output_2", "type": "boolean", "pos": [0, 2] }, + { "name": "output_3", "type": "number", "pos": [0, 3] }, + { "name": "output_4", "type": "string", "pos": [0, 4] }, + { "name": "output_5", "type": "boolean", "pos": [0, 5] }, + { "name": "output_6", "type": "number", "pos": [0, 6] }, + { "name": "output_7", "type": "string", "pos": [0, 7] }, + { "name": "output_8", "type": "boolean", "pos": [0, 8] }, + { "name": "output_9", "type": "number", "pos": [0, 9] } + ], + "widgets": [] + } +} \ No newline at end of file