Files
ComfyUI_frontend/test/subgraph/SubgraphEdgeCases.test.ts
2025-07-20 18:36:33 -07:00

365 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* SubgraphEdgeCases Tests
*
* Tests for edge cases, error handling, and boundary conditions in the subgraph system.
* This covers unusual scenarios, invalid states, and stress testing.
*/
import { describe, expect, it } from "vitest"
import { LGraph, LGraphNode, Subgraph } from "@/litegraph"
import {
createNestedSubgraphs,
createTestSubgraph,
createTestSubgraphNode,
} from "./fixtures/subgraphHelpers"
describe("SubgraphEdgeCases - Recursion Detection", () => {
it("should handle circular subgraph references without crashing", () => {
const sub1 = createTestSubgraph({ name: "Sub1" })
const sub2 = createTestSubgraph({ name: "Sub2" })
// Create circular reference
const node1 = createTestSubgraphNode(sub1, { id: 1 })
const node2 = createTestSubgraphNode(sub2, { id: 2 })
sub1.add(node2)
sub2.add(node1)
// Should not crash or hang - currently throws path resolution error due to circular structure
expect(() => {
const executableNodes = new Map()
node1.getInnerNodes(executableNodes)
}).toThrow(/Node \[\d+\] not found/) // Current behavior: path resolution fails
})
it("should handle deep nesting scenarios", () => {
// Test with reasonable depth to avoid timeout
const nested = createNestedSubgraphs({ depth: 10, nodesPerLevel: 1 })
// Should create nested structure without errors
expect(nested.subgraphs).toHaveLength(10)
expect(nested.subgraphNodes).toHaveLength(10)
// First level should exist and be accessible
const firstLevel = nested.rootGraph.nodes[0]
expect(firstLevel).toBeDefined()
expect(firstLevel.isSubgraphNode()).toBe(true)
})
it.todo("should use WeakSet for cycle detection", () => {
// TODO: This test is currently skipped because cycle detection has a bug
// The fix is to pass 'visited' directly instead of 'new Set(visited)' in SubgraphNode.ts:299
const subgraph = createTestSubgraph({ nodeCount: 1 })
const subgraphNode = createTestSubgraphNode(subgraph)
// Add to own subgraph to create cycle
subgraph.add(subgraphNode)
// Should throw due to cycle detection
const executableNodes = new Map()
expect(() => {
subgraphNode.getInnerNodes(executableNodes)
}).toThrow(/while flattening subgraph/i)
})
it("should respect MAX_NESTED_SUBGRAPHS constant", () => {
// Verify the constant exists and is a reasonable positive number
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeDefined()
expect(typeof Subgraph.MAX_NESTED_SUBGRAPHS).toBe("number")
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeGreaterThan(0)
expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeLessThanOrEqual(10_000) // Reasonable upper bound
// Note: Currently not enforced in implementation
// This test documents the intended behavior
})
})
describe("SubgraphEdgeCases - Invalid States", () => {
it("should handle removing non-existent inputs gracefully", () => {
const subgraph = createTestSubgraph()
const fakeInput = { name: "fake", type: "number", disconnect: () => {} } as any
// Should throw appropriate error for non-existent input
expect(() => {
subgraph.removeInput(fakeInput)
}).toThrow(/Input not found/) // Expected error
})
it("should handle removing non-existent outputs gracefully", () => {
const subgraph = createTestSubgraph()
const fakeOutput = { name: "fake", type: "number", disconnect: () => {} } as any
expect(() => {
subgraph.removeOutput(fakeOutput)
}).toThrow(/Output not found/) // Expected error
})
it("should handle null/undefined input names", () => {
const subgraph = createTestSubgraph()
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
// TODO: Consider adding validation to prevent null/undefined names
// This test documents the current permissive behavior
expect(() => {
subgraph.addInput(null as any, "number")
}).not.toThrow() // Current behavior: allows null
expect(() => {
subgraph.addInput(undefined as any, "number")
}).not.toThrow() // Current behavior: allows undefined
})
it("should handle null/undefined output names", () => {
const subgraph = createTestSubgraph()
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
// TODO: Consider adding validation to prevent null/undefined names
// This test documents the current permissive behavior
expect(() => {
subgraph.addOutput(null as any, "number")
}).not.toThrow() // Current behavior: allows null
expect(() => {
subgraph.addOutput(undefined as any, "number")
}).not.toThrow() // Current behavior: allows undefined
})
it("should handle empty string names", () => {
const subgraph = createTestSubgraph()
// Current implementation may allow empty strings
// Document the actual behavior
expect(() => {
subgraph.addInput("", "number")
}).not.toThrow() // Current behavior: allows empty strings
expect(() => {
subgraph.addOutput("", "number")
}).not.toThrow() // Current behavior: allows empty strings
})
it("should handle undefined types gracefully", () => {
const subgraph = createTestSubgraph()
// Undefined type should not crash but may have default behavior
expect(() => {
subgraph.addInput("test", undefined as any)
}).not.toThrow()
expect(() => {
subgraph.addOutput("test", undefined as any)
}).not.toThrow()
})
it("should handle duplicate slot names", () => {
const subgraph = createTestSubgraph()
// Add first input
subgraph.addInput("duplicate", "number")
// Adding duplicate should not crash (current behavior allows it)
expect(() => {
subgraph.addInput("duplicate", "string")
}).not.toThrow()
// Should now have 2 inputs with same name
expect(subgraph.inputs.length).toBe(2)
expect(subgraph.inputs[0].name).toBe("duplicate")
expect(subgraph.inputs[1].name).toBe("duplicate")
})
})
describe("SubgraphEdgeCases - Boundary Conditions", () => {
it("should handle empty subgraphs (no nodes, no IO)", () => {
const subgraph = createTestSubgraph({ nodeCount: 0 })
const subgraphNode = createTestSubgraphNode(subgraph)
// Should handle empty subgraph without errors
const executableNodes = new Map()
const flattened = subgraphNode.getInnerNodes(executableNodes)
expect(flattened).toHaveLength(0)
expect(subgraph.inputs).toHaveLength(0)
expect(subgraph.outputs).toHaveLength(0)
})
it("should handle single input/output subgraphs", () => {
const subgraph = createTestSubgraph({
inputs: [{ name: "single_in", type: "number" }],
outputs: [{ name: "single_out", type: "number" }],
nodeCount: 1,
})
const subgraphNode = createTestSubgraphNode(subgraph)
expect(subgraphNode.inputs).toHaveLength(1)
expect(subgraphNode.outputs).toHaveLength(1)
expect(subgraphNode.inputs[0].name).toBe("single_in")
expect(subgraphNode.outputs[0].name).toBe("single_out")
})
it("should handle subgraphs with many slots", () => {
const subgraph = createTestSubgraph({ nodeCount: 1 })
// Add many inputs (test with 20 to keep test fast)
for (let i = 0; i < 20; i++) {
subgraph.addInput(`input_${i}`, "number")
}
// Add many outputs
for (let i = 0; i < 20; i++) {
subgraph.addOutput(`output_${i}`, "number")
}
const subgraphNode = createTestSubgraphNode(subgraph)
expect(subgraph.inputs).toHaveLength(20)
expect(subgraph.outputs).toHaveLength(20)
expect(subgraphNode.inputs).toHaveLength(20)
expect(subgraphNode.outputs).toHaveLength(20)
// Should still flatten correctly
const executableNodes = new Map()
const flattened = subgraphNode.getInnerNodes(executableNodes)
expect(flattened).toHaveLength(1) // Original node count
})
it("should handle very long slot names", () => {
const subgraph = createTestSubgraph()
const longName = "a".repeat(1000) // 1000 character name
expect(() => {
subgraph.addInput(longName, "number")
subgraph.addOutput(longName, "string")
}).not.toThrow()
expect(subgraph.inputs[0].name).toBe(longName)
expect(subgraph.outputs[0].name).toBe(longName)
})
it("should handle Unicode characters in names", () => {
const subgraph = createTestSubgraph()
const unicodeName = "测试_🚀_تست_тест"
expect(() => {
subgraph.addInput(unicodeName, "number")
subgraph.addOutput(unicodeName, "string")
}).not.toThrow()
expect(subgraph.inputs[0].name).toBe(unicodeName)
expect(subgraph.outputs[0].name).toBe(unicodeName)
})
})
describe("SubgraphEdgeCases - Type Validation", () => {
it("should allow connecting mismatched types (no validation currently)", () => {
const rootGraph = new LGraph()
const subgraph = createTestSubgraph()
subgraph.addInput("num", "number")
subgraph.addOutput("str", "string")
// Create a basic node manually since createNode is not available
const numberNode = new LGraphNode("basic/const")
numberNode.addOutput("value", "number")
rootGraph.add(numberNode)
const subgraphNode = createTestSubgraphNode(subgraph)
rootGraph.add(subgraphNode)
// Currently allows mismatched connections (no type validation)
expect(() => {
numberNode.connect(0, subgraphNode, 0)
}).not.toThrow()
})
it("should handle invalid type strings", () => {
const subgraph = createTestSubgraph()
// These should not crash (current behavior)
expect(() => {
subgraph.addInput("test1", "invalid_type")
subgraph.addInput("test2", "")
subgraph.addInput("test3", "123")
subgraph.addInput("test4", "special!@#$%")
}).not.toThrow()
})
it("should handle complex type strings", () => {
const subgraph = createTestSubgraph()
expect(() => {
subgraph.addInput("array", "array<number>")
subgraph.addInput("object", "object<{x: number, y: string}>")
subgraph.addInput("union", "number|string")
}).not.toThrow()
expect(subgraph.inputs).toHaveLength(3)
expect(subgraph.inputs[0].type).toBe("array<number>")
expect(subgraph.inputs[1].type).toBe("object<{x: number, y: string}>")
expect(subgraph.inputs[2].type).toBe("number|string")
})
})
describe("SubgraphEdgeCases - Performance and Scale", () => {
it("should handle large numbers of nodes in subgraph", () => {
// Create subgraph with many nodes (keep reasonable for test speed)
const subgraph = createTestSubgraph({ nodeCount: 50 })
const subgraphNode = createTestSubgraphNode(subgraph)
const executableNodes = new Map()
const flattened = subgraphNode.getInnerNodes(executableNodes)
expect(flattened).toHaveLength(50)
// Performance is acceptable for 50 nodes (typically < 1ms)
})
it("should handle rapid IO changes", () => {
const subgraph = createTestSubgraph()
// Rapidly add and remove inputs/outputs
for (let i = 0; i < 10; i++) {
const input = subgraph.addInput(`rapid_${i}`, "number")
const output = subgraph.addOutput(`rapid_${i}`, "number")
// Remove them immediately
subgraph.removeInput(input)
subgraph.removeOutput(output)
}
// Should end up with no inputs/outputs
expect(subgraph.inputs).toHaveLength(0)
expect(subgraph.outputs).toHaveLength(0)
})
it("should handle concurrent modifications safely", () => {
// This test ensures the system doesn't crash under concurrent access
// Note: JavaScript is single-threaded, so this tests rapid sequential access
const subgraph = createTestSubgraph({ nodeCount: 5 })
const subgraphNode = createTestSubgraphNode(subgraph)
// Simulate concurrent operations
const operations = []
for (let i = 0; i < 20; i++) {
operations.push(() => {
const executableNodes = new Map()
subgraphNode.getInnerNodes(executableNodes)
}, () => {
subgraph.addInput(`concurrent_${i}`, "number")
}, () => {
if (subgraph.inputs.length > 0) {
subgraph.removeInput(subgraph.inputs[0])
}
})
}
// Execute all operations - should not crash
expect(() => {
for (const op of operations) op()
}).not.toThrow()
})
})