[feat] Add subgraph test infrastructure (#1111)

This commit is contained in:
Christian Byrne
2025-07-15 07:53:26 -07:00
committed by GitHub
parent 4ab223d651
commit ca5774c0df
7 changed files with 1937 additions and 1 deletions

View File

@@ -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"

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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 }`

View File

@@ -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<typeof createNestedSubgraphs>
/** 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<typeof createEventCapture>
}
}
/**
* 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<SubgraphFixtures>({
emptySubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
const subgraph = createTestSubgraph({
name: "Empty Test Subgraph",
inputCount: 0,
outputCount: 0,
nodeCount: 0,
})
await use(subgraph)
},
simpleSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
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<void>) => {
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<void>) => {
const nested = createNestedSubgraphs({
depth: 3,
nodesPerLevel: 2,
inputsPerSubgraph: 1,
outputsPerSubgraph: 1,
})
await use(nested)
},
subgraphWithNode: async ({ }, use: (value: unknown) => Promise<void>) => {
// 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<void>) => {
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<typeof createNestedSubgraphs>
/** 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<EdgeCaseFixtures>({
circularSubgraph: async ({ }, use: (value: unknown) => Promise<void>) => {
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<void>) => {
// 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<void>) => {
// 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<T extends Record<string, unknown>>(
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,
}
}

View File

@@ -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<T = unknown> {
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<T = unknown>(
capturedEvents: CapturedEvent<T>[],
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> = {}): 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<string, {
id: number
origin_id: number
origin_slot: number
target_id: number
target_slot: number
type: string
}> = {}
// 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<T = unknown>(
eventTarget: EventTarget,
eventTypes: string[],
) {
const capturedEvents: CapturedEvent<T>[] = []
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<T>).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"

View File

@@ -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": []
}
}