Files
ComfyUI_frontend/test/subgraph/Subgraph.test.ts
2025-07-15 10:11:08 -07:00

314 lines
10 KiB
TypeScript

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