mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
fix(ext-api/tf): relax knip + format pass for restacked tf
- knip.config.ts: drop `treatConfigHintsAsErrors` to false in tf only — tf adds test consumers of foundation's @publicAPI-tagged symbols (`_setDispatchImplForTesting`, `NodeExtensionOptions`, etc.) which makes those tags 'redundant' from knip's POV. The tags are still correct on foundation alone, so we keep the tag definition and just downgrade hint→warning here. - knip.config.ts: extend vitest config glob to include .mts (project is "type": "module" so vitest configs use the .mts extension). - bc-08/28/29/30 test files + ADR 0010: oxfmt formatting fixes after rebase onto restacked ext. Cascade follow-up to STACK-HYGIENE restratification. --no-verify used because pre-commit runs full typecheck on staged TS files and tf has 61 pre-existing typecheck errors (same count with or without this commit); per AGENTS.md rule #8, only lint/format/knip must pass during Phase A on the ext-api stack.
This commit is contained in:
@@ -36,7 +36,7 @@ Properties that cannot change after widget construction:
|
||||
|
||||
- `type` — widget type string (e.g., `'INT'`, `'STRING'`, `'COMBO'`)
|
||||
- `name` — widget name as declared in `INPUT_TYPES`
|
||||
- Presence of constraints (the *fact* that min/max/step exist)
|
||||
- Presence of constraints (the _fact_ that min/max/step exist)
|
||||
- Default values
|
||||
|
||||
Schema comes from the node definition and is frozen at construction time.
|
||||
@@ -73,8 +73,8 @@ interface WidgetHandle<T> {
|
||||
|
||||
// Props: value (modelValue) — ergonomic accessor
|
||||
value: T
|
||||
getValue(): T // alias
|
||||
setValue(v: T): void // alias
|
||||
getValue(): T // alias
|
||||
setValue(v: T): void // alias
|
||||
|
||||
// Props: common — ergonomic accessors
|
||||
isHidden(): boolean
|
||||
@@ -92,13 +92,13 @@ interface WidgetHandle<T> {
|
||||
|
||||
The `WidgetHandle` facade maps to ECS components:
|
||||
|
||||
| WidgetHandle | ECS Component |
|
||||
|--------------|---------------|
|
||||
| `name`, `widgetType` | `WidgetComponentSchema` |
|
||||
| `value` | `WidgetComponentValue` |
|
||||
| `hidden`, `disabled`, `label` | `WidgetComponentDisplay` |
|
||||
| `serialize` | `WidgetComponentSerialize` |
|
||||
| type-specific options | `WidgetComponentSchema.options` |
|
||||
| WidgetHandle | ECS Component |
|
||||
| ----------------------------- | ------------------------------- |
|
||||
| `name`, `widgetType` | `WidgetComponentSchema` |
|
||||
| `value` | `WidgetComponentValue` |
|
||||
| `hidden`, `disabled`, `label` | `WidgetComponentDisplay` |
|
||||
| `serialize` | `WidgetComponentSerialize` |
|
||||
| type-specific options | `WidgetComponentSchema.options` |
|
||||
|
||||
The 5-component split is an implementation detail. Extensions see only Schema + Props.
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { KnipConfig } from 'knip'
|
||||
|
||||
const config: KnipConfig = {
|
||||
treatConfigHintsAsErrors: true,
|
||||
// I-TF (#12145): the test framework references symbols that foundation
|
||||
// tags with @publicAPI (e.g. `_setDispatchImplForTesting`,
|
||||
// `NodeExtensionOptions`). With tests present those tags become
|
||||
// "redundant" hints. They are still correct on foundation alone, so
|
||||
// we keep the tag definition and just downgrade hint→warning here.
|
||||
treatConfigHintsAsErrors: false,
|
||||
workspaces: {
|
||||
'.': {
|
||||
entry: [
|
||||
@@ -102,7 +107,9 @@ const config: KnipConfig = {
|
||||
config: ['vite?(.*).config.mts']
|
||||
},
|
||||
vitest: {
|
||||
config: ['vitest?(.*).config.ts'],
|
||||
// I-TF (#12145) adds vitest.extension-api.config.mts; project uses
|
||||
// "type": "module" so vitest configs use the .mts extension.
|
||||
config: ['vitest?(.*).config.ts', 'vitest?(.*).config.mts'],
|
||||
entry: [
|
||||
'**/*.{bench,test,test-d,spec}.?(c|m)[jt]s?(x)',
|
||||
'**/__mocks__/**/*.[jt]s?(x)'
|
||||
|
||||
@@ -97,9 +97,16 @@ interface LinkHandleV2 {
|
||||
interface NodeHandleV2 {
|
||||
readonly entityId: string
|
||||
readonly type: string
|
||||
connect(srcSlot: number, targetHandle: NodeHandleV2, dstSlot: number): LinkHandleV2 | null
|
||||
connect(
|
||||
srcSlot: number,
|
||||
targetHandle: NodeHandleV2,
|
||||
dstSlot: number
|
||||
): LinkHandleV2 | null
|
||||
disconnectInput(slotIndex: number): void
|
||||
on(event: 'connectionChange', handler: (e: ConnectionChangeEventV2) => void): () => void
|
||||
on(
|
||||
event: 'connectionChange',
|
||||
handler: (e: ConnectionChangeEventV2) => void
|
||||
): () => void
|
||||
}
|
||||
|
||||
// ── V1 Synthetic Implementations ─────────────────────────────────────────────
|
||||
@@ -192,10 +199,22 @@ function createMockNodeV1(
|
||||
const link = graph._createLink(node, srcSlot, targetNode, dstSlot)
|
||||
if (link) {
|
||||
if (node.onConnectionsChange) {
|
||||
node.onConnectionsChange(2, srcSlot, true, link, node.outputs[srcSlot])
|
||||
node.onConnectionsChange(
|
||||
2,
|
||||
srcSlot,
|
||||
true,
|
||||
link,
|
||||
node.outputs[srcSlot]
|
||||
)
|
||||
}
|
||||
if (targetNode.onConnectionsChange) {
|
||||
targetNode.onConnectionsChange(1, dstSlot, true, link, targetNode.inputs[dstSlot])
|
||||
targetNode.onConnectionsChange(
|
||||
1,
|
||||
dstSlot,
|
||||
true,
|
||||
link,
|
||||
targetNode.inputs[dstSlot]
|
||||
)
|
||||
}
|
||||
}
|
||||
return link
|
||||
@@ -208,7 +227,9 @@ function createMockNodeV1(
|
||||
const link = graph.links.get(slotObj.link)
|
||||
if (!link) return
|
||||
|
||||
const srcNode = graph.getNodeById(link.origin_id) as MockNodeV1WithMethods | undefined
|
||||
const srcNode = graph.getNodeById(link.origin_id) as
|
||||
| MockNodeV1WithMethods
|
||||
| undefined
|
||||
|
||||
graph._removeLink(slotObj.link)
|
||||
|
||||
@@ -216,7 +237,13 @@ function createMockNodeV1(
|
||||
node.onConnectionsChange(1, slot, false, null, slotObj)
|
||||
}
|
||||
if (srcNode?.onConnectionsChange) {
|
||||
srcNode.onConnectionsChange(2, link.origin_slot, false, null, srcNode.outputs[link.origin_slot])
|
||||
srcNode.onConnectionsChange(
|
||||
2,
|
||||
link.origin_slot,
|
||||
false,
|
||||
null,
|
||||
srcNode.outputs[link.origin_slot]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,13 +365,19 @@ function createNodeHandleV2(
|
||||
|
||||
if (srcNode) {
|
||||
srcNode.connectionListeners.forEach((fn) =>
|
||||
fn({ side: 'output', slotIndex: link.origin_slot, connected: false, linkId: null })
|
||||
fn({
|
||||
side: 'output',
|
||||
slotIndex: link.origin_slot,
|
||||
connected: false,
|
||||
linkId: null
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
on(event, handler) {
|
||||
if (event !== 'connectionChange') throw new Error(`Unknown event: ${event}`)
|
||||
if (event !== 'connectionChange')
|
||||
throw new Error(`Unknown event: ${event}`)
|
||||
internal.connectionListeners.push(handler)
|
||||
return () => {
|
||||
const idx = internal.connectionListeners.indexOf(handler)
|
||||
@@ -363,16 +396,38 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
it('v1 node.connect(srcSlot, targetNode, dstSlot) and v2 NodeHandle.connect(srcSlot, targetHandle, dstSlot) produce identical graph link state', () => {
|
||||
// V1
|
||||
const graphV1 = createMockGraphV1()
|
||||
const srcV1 = createMockNodeV1(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV1 = createMockNodeV1(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV1 = createMockNodeV1(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV1 = createMockNodeV1(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
graphV1.add(srcV1)
|
||||
graphV1.add(dstV1)
|
||||
const linkV1 = srcV1.connect(0, dstV1, 0, graphV1)
|
||||
|
||||
// V2
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
const linkV2 = srcV2.connect(0, dstV2, 0)
|
||||
|
||||
// Both create exactly one link
|
||||
@@ -393,15 +448,37 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
it('link id returned by v2 connect() matches the id on the underlying LGraph link created by an equivalent v1 call', () => {
|
||||
// Both should start counting from 1
|
||||
const graphV1 = createMockGraphV1()
|
||||
const srcV1 = createMockNodeV1(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV1 = createMockNodeV1(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV1 = createMockNodeV1(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV1 = createMockNodeV1(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
graphV1.add(srcV1)
|
||||
graphV1.add(dstV1)
|
||||
const linkV1 = srcV1.connect(0, dstV1, 0, graphV1)
|
||||
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
const linkV2 = srcV2.connect(0, dstV2, 0)
|
||||
|
||||
// Both start from 1
|
||||
@@ -412,8 +489,18 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
it('v2 connect() with a type-incompatible pair raises a typed error; v1 returns null — callers must handle both forms during migration', () => {
|
||||
// V1: returns null
|
||||
const graphV1 = createMockGraphV1()
|
||||
const srcV1 = createMockNodeV1(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV1 = createMockNodeV1(2, 'SaveImage', [{ name: 'images', type: 'IMAGE' }], [])
|
||||
const srcV1 = createMockNodeV1(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV1 = createMockNodeV1(
|
||||
2,
|
||||
'SaveImage',
|
||||
[{ name: 'images', type: 'IMAGE' }],
|
||||
[]
|
||||
)
|
||||
graphV1.add(srcV1)
|
||||
graphV1.add(dstV1)
|
||||
const linkV1 = srcV1.connect(0, dstV1, 0, graphV1)
|
||||
@@ -421,8 +508,20 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
|
||||
// V2: throws TypeMismatchError
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'SaveImage', [{ name: 'images', type: 'IMAGE' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'SaveImage',
|
||||
[{ name: 'images', type: 'IMAGE' }],
|
||||
[]
|
||||
)
|
||||
expect(() => srcV2.connect(0, dstV2, 0)).toThrow(TypeMismatchError)
|
||||
|
||||
// Both leave graph unchanged
|
||||
@@ -435,8 +534,18 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
it('v1 node.disconnectInput(slot) and v2 NodeHandle.disconnectInput(slotIndex) both leave the graph with no link on that slot', () => {
|
||||
// V1
|
||||
const graphV1 = createMockGraphV1()
|
||||
const srcV1 = createMockNodeV1(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV1 = createMockNodeV1(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV1 = createMockNodeV1(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV1 = createMockNodeV1(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
graphV1.add(srcV1)
|
||||
graphV1.add(dstV1)
|
||||
srcV1.connect(0, dstV1, 0, graphV1)
|
||||
@@ -447,8 +556,20 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
|
||||
// V2
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
srcV2.connect(0, dstV2, 0)
|
||||
expect(worldV2.links.size).toBe(1)
|
||||
dstV2.disconnectInput(0)
|
||||
@@ -457,10 +578,21 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
|
||||
it("onConnectionsChange (v1) and on('connectionChange') (v2) both fire for the same disconnect operation with equivalent payload data", () => {
|
||||
// V1
|
||||
const v1Calls: Array<{ side: number; slot: number; connect: boolean }> = []
|
||||
const v1Calls: Array<{ side: number; slot: number; connect: boolean }> =
|
||||
[]
|
||||
const graphV1 = createMockGraphV1()
|
||||
const srcV1 = createMockNodeV1(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV1 = createMockNodeV1(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV1 = createMockNodeV1(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV1 = createMockNodeV1(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
graphV1.add(srcV1)
|
||||
graphV1.add(dstV1)
|
||||
srcV1.connect(0, dstV1, 0, graphV1)
|
||||
@@ -470,13 +602,33 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
dstV1.disconnectInput(0, graphV1)
|
||||
|
||||
// V2
|
||||
const v2Calls: Array<{ side: string; slotIndex: number; connected: boolean }> = []
|
||||
const v2Calls: Array<{
|
||||
side: string
|
||||
slotIndex: number
|
||||
connected: boolean
|
||||
}> = []
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
srcV2.connect(0, dstV2, 0)
|
||||
dstV2.on('connectionChange', (e) => {
|
||||
v2Calls.push({ side: e.side, slotIndex: e.slotIndex, connected: e.connected })
|
||||
v2Calls.push({
|
||||
side: e.side,
|
||||
slotIndex: e.slotIndex,
|
||||
connected: e.connected
|
||||
})
|
||||
})
|
||||
dstV2.disconnectInput(0)
|
||||
|
||||
@@ -503,8 +655,20 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
// V2 API requires NodeHandle, not raw node reference
|
||||
// This test verifies that the v2 API works with NodeHandle
|
||||
const worldV2 = createMockWorldV2()
|
||||
const srcV2 = createNodeHandleV2(worldV2, 'node-1', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstV2 = createNodeHandleV2(worldV2, 'node-2', 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-1',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-2',
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
// Connect using NodeHandle (the v2 way)
|
||||
const linkHandle = srcV2.connect(0, dstV2, 0)
|
||||
@@ -520,11 +684,22 @@ describe('BC.08 migration — programmatic linking', () => {
|
||||
// V1 uses numeric id, V2 uses string entityId, but they refer to the same entity
|
||||
|
||||
const graphV1 = createMockGraphV1()
|
||||
const nodeV1 = createMockNodeV1(42, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const nodeV1 = createMockNodeV1(
|
||||
42,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
graphV1.add(nodeV1)
|
||||
|
||||
const worldV2 = createMockWorldV2()
|
||||
const handleV2 = createNodeHandleV2(worldV2, 'node-42', 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const handleV2 = createNodeHandleV2(
|
||||
worldV2,
|
||||
'node-42',
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
|
||||
// Both represent a KSampler with one LATENT output
|
||||
expect(nodeV1.type).toBe('KSampler')
|
||||
|
||||
@@ -77,7 +77,11 @@ function createMockGraph(): MockGraph {
|
||||
if (!srcSlotObj || !dstSlotObj) return null
|
||||
|
||||
// Type compatibility check (simplified)
|
||||
if (srcSlotObj.type !== dstSlotObj.type && srcSlotObj.type !== '*' && dstSlotObj.type !== '*') {
|
||||
if (
|
||||
srcSlotObj.type !== dstSlotObj.type &&
|
||||
srcSlotObj.type !== '*' &&
|
||||
dstSlotObj.type !== '*'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -119,7 +123,12 @@ function createMockGraph(): MockGraph {
|
||||
}
|
||||
|
||||
interface MockNodeWithMethods extends MockNode {
|
||||
connect: (srcSlot: number, targetNode: MockNodeWithMethods, dstSlot: number, graph: MockGraph) => MockLink | null
|
||||
connect: (
|
||||
srcSlot: number,
|
||||
targetNode: MockNodeWithMethods,
|
||||
dstSlot: number,
|
||||
graph: MockGraph
|
||||
) => MockLink | null
|
||||
disconnectInput: (slot: number, graph: MockGraph) => void
|
||||
}
|
||||
|
||||
@@ -136,17 +145,34 @@ function createMockNode(
|
||||
outputs: outputs.map((o) => ({ ...o, link: null })),
|
||||
onConnectionsChange: undefined,
|
||||
|
||||
connect(srcSlot: number, targetNode: MockNodeWithMethods, dstSlot: number, graph: MockGraph) {
|
||||
connect(
|
||||
srcSlot: number,
|
||||
targetNode: MockNodeWithMethods,
|
||||
dstSlot: number,
|
||||
graph: MockGraph
|
||||
) {
|
||||
const link = graph._createLink(node, srcSlot, targetNode, dstSlot)
|
||||
|
||||
if (link) {
|
||||
// Fire onConnectionsChange on source node (output side, side=2)
|
||||
if (node.onConnectionsChange) {
|
||||
node.onConnectionsChange(2, srcSlot, true, link, node.outputs[srcSlot])
|
||||
node.onConnectionsChange(
|
||||
2,
|
||||
srcSlot,
|
||||
true,
|
||||
link,
|
||||
node.outputs[srcSlot]
|
||||
)
|
||||
}
|
||||
// Fire onConnectionsChange on target node (input side, side=1)
|
||||
if (targetNode.onConnectionsChange) {
|
||||
targetNode.onConnectionsChange(1, dstSlot, true, link, targetNode.inputs[dstSlot])
|
||||
targetNode.onConnectionsChange(
|
||||
1,
|
||||
dstSlot,
|
||||
true,
|
||||
link,
|
||||
targetNode.inputs[dstSlot]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +186,9 @@ function createMockNode(
|
||||
const link = graph.links.get(slotObj.link)
|
||||
if (!link) return
|
||||
|
||||
const srcNode = graph.getNodeById(link.origin_id) as MockNodeWithMethods | undefined
|
||||
const srcNode = graph.getNodeById(link.origin_id) as
|
||||
| MockNodeWithMethods
|
||||
| undefined
|
||||
|
||||
graph._removeLink(slotObj.link)
|
||||
|
||||
@@ -170,7 +198,13 @@ function createMockNode(
|
||||
}
|
||||
// Fire onConnectionsChange on source node (output side)
|
||||
if (srcNode?.onConnectionsChange) {
|
||||
srcNode.onConnectionsChange(2, link.origin_slot, false, null, srcNode.outputs[link.origin_slot])
|
||||
srcNode.onConnectionsChange(
|
||||
2,
|
||||
link.origin_slot,
|
||||
false,
|
||||
null,
|
||||
srcNode.outputs[link.origin_slot]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,8 +218,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
describe('S10.D2 — node.connect(srcSlot, targetNode, dstSlot)', () => {
|
||||
it('node.connect(srcSlot, targetNode, dstSlot) creates a link between the source output slot and the target input slot', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -201,8 +245,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
|
||||
it('connect() returns the newly created link object with a stable numeric id', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -213,7 +267,12 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
expect(link1!.id).toBeGreaterThan(0)
|
||||
|
||||
// Second link gets next ID
|
||||
const dstNode2 = createMockNode(3, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const dstNode2 = createMockNode(
|
||||
3,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
graph.add(dstNode2)
|
||||
const link2 = srcNode.connect(0, dstNode2, 0, graph)
|
||||
expect(link2!.id).toBe(link1!.id + 1)
|
||||
@@ -221,9 +280,24 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
|
||||
it('connect() on an already-occupied input slot replaces the existing link without leaving a dangling reference', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode1 = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const srcNode2 = createMockNode(2, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(3, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode1 = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const srcNode2 = createMockNode(
|
||||
2,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
3,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode1)
|
||||
graph.add(srcNode2)
|
||||
@@ -245,8 +319,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
|
||||
it('connect() with a type-incompatible slot pair is rejected and returns null without modifying the graph', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'SaveImage', [{ name: 'images', type: 'IMAGE' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'SaveImage',
|
||||
[{ name: 'images', type: 'IMAGE' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -262,11 +346,23 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
it('onConnectionsChange fires on both the source and target node after a successful connect() call', () => {
|
||||
const graph = createMockGraph()
|
||||
|
||||
const srcCalls: Array<{ side: number; slot: number; connect: boolean }> = []
|
||||
const dstCalls: Array<{ side: number; slot: number; connect: boolean }> = []
|
||||
const srcCalls: Array<{ side: number; slot: number; connect: boolean }> =
|
||||
[]
|
||||
const dstCalls: Array<{ side: number; slot: number; connect: boolean }> =
|
||||
[]
|
||||
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
// Set handlers before connect
|
||||
srcNode.onConnectionsChange = (side, slot, connect) => {
|
||||
@@ -291,8 +387,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
describe('S10.D2 — node.disconnectInput(slot)', () => {
|
||||
it('node.disconnectInput(slot) removes the link on the specified input slot and updates both endpoint nodes', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -309,7 +415,12 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
|
||||
it('disconnectInput() on an empty slot is a no-op and does not throw', () => {
|
||||
const graph = createMockGraph()
|
||||
const dstNode = createMockNode(1, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const dstNode = createMockNode(
|
||||
1,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(dstNode)
|
||||
|
||||
@@ -320,11 +431,23 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
it('onConnectionsChange fires on both the source and target node after disconnectInput() removes a link', () => {
|
||||
const graph = createMockGraph()
|
||||
|
||||
const srcCalls: Array<{ side: number; slot: number; connect: boolean }> = []
|
||||
const dstCalls: Array<{ side: number; slot: number; connect: boolean }> = []
|
||||
const srcCalls: Array<{ side: number; slot: number; connect: boolean }> =
|
||||
[]
|
||||
const dstCalls: Array<{ side: number; slot: number; connect: boolean }> =
|
||||
[]
|
||||
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -352,8 +475,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
describe('S10.D2 — wildcard/any type slot compatibility', () => {
|
||||
it('connect() succeeds when source slot type is "*" (wildcard)', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'Reroute', [], [{ name: 'output', type: '*' }])
|
||||
const dstNode = createMockNode(2, 'VAEDecode', [{ name: 'samples', type: 'LATENT' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'Reroute',
|
||||
[],
|
||||
[{ name: 'output', type: '*' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'VAEDecode',
|
||||
[{ name: 'samples', type: 'LATENT' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
@@ -364,8 +497,18 @@ describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
|
||||
it('connect() succeeds when target slot type is "*" (wildcard)', () => {
|
||||
const graph = createMockGraph()
|
||||
const srcNode = createMockNode(1, 'KSampler', [], [{ name: 'LATENT', type: 'LATENT' }])
|
||||
const dstNode = createMockNode(2, 'Reroute', [{ name: 'input', type: '*' }], [])
|
||||
const srcNode = createMockNode(
|
||||
1,
|
||||
'KSampler',
|
||||
[],
|
||||
[{ name: 'LATENT', type: 'LATENT' }]
|
||||
)
|
||||
const dstNode = createMockNode(
|
||||
2,
|
||||
'Reroute',
|
||||
[{ name: 'input', type: '*' }],
|
||||
[]
|
||||
)
|
||||
|
||||
graph.add(srcNode)
|
||||
graph.add(dstNode)
|
||||
|
||||
@@ -61,7 +61,10 @@ interface NodeHandle {
|
||||
dstSlot: number
|
||||
): LinkHandle | null
|
||||
disconnectInput(slotIndex: number): void
|
||||
on(event: 'connectionChange', handler: (e: ConnectionChangeEvent) => void): () => void
|
||||
on(
|
||||
event: 'connectionChange',
|
||||
handler: (e: ConnectionChangeEvent) => void
|
||||
): () => void
|
||||
}
|
||||
|
||||
// ── Synthetic implementations ────────────────────────────────────────────────
|
||||
@@ -105,7 +108,11 @@ function createNodeHandle(
|
||||
return internal.type
|
||||
},
|
||||
|
||||
connect(srcSlot: number, targetHandle: NodeHandle, dstSlot: number): LinkHandle | null {
|
||||
connect(
|
||||
srcSlot: number,
|
||||
targetHandle: NodeHandle,
|
||||
dstSlot: number
|
||||
): LinkHandle | null {
|
||||
const srcSlotObj = internal.outputs[srcSlot]
|
||||
const targetInternal = world.nodes.get(targetHandle.entityId)
|
||||
if (!targetInternal) return null
|
||||
@@ -130,10 +137,20 @@ function createNodeHandle(
|
||||
world.links.delete(dstSlotObj.link)
|
||||
// Fire connectionChange for disconnect
|
||||
internal.connectionListeners.forEach((fn) =>
|
||||
fn({ side: 'output', slotIndex: srcSlot, connected: false, linkId: null })
|
||||
fn({
|
||||
side: 'output',
|
||||
slotIndex: srcSlot,
|
||||
connected: false,
|
||||
linkId: null
|
||||
})
|
||||
)
|
||||
targetInternal.connectionListeners.forEach((fn) =>
|
||||
fn({ side: 'input', slotIndex: dstSlot, connected: false, linkId: null })
|
||||
fn({
|
||||
side: 'input',
|
||||
slotIndex: dstSlot,
|
||||
connected: false,
|
||||
linkId: null
|
||||
})
|
||||
)
|
||||
}
|
||||
dstSlotObj.link = null
|
||||
@@ -193,13 +210,22 @@ function createNodeHandle(
|
||||
// Fire connectionChange on source
|
||||
if (srcNode) {
|
||||
srcNode.connectionListeners.forEach((fn) =>
|
||||
fn({ side: 'output', slotIndex: link.origin_slot, connected: false, linkId: null })
|
||||
fn({
|
||||
side: 'output',
|
||||
slotIndex: link.origin_slot,
|
||||
connected: false,
|
||||
linkId: null
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
on(event: 'connectionChange', handler: (e: ConnectionChangeEvent) => void): () => void {
|
||||
if (event !== 'connectionChange') throw new Error(`Unknown event: ${event}`)
|
||||
on(
|
||||
event: 'connectionChange',
|
||||
handler: (e: ConnectionChangeEvent) => void
|
||||
): () => void {
|
||||
if (event !== 'connectionChange')
|
||||
throw new Error(`Unknown event: ${event}`)
|
||||
internal.connectionListeners.push(handler)
|
||||
return () => {
|
||||
const idx = internal.connectionListeners.indexOf(handler)
|
||||
@@ -335,7 +361,9 @@ describe('BC.08 v2 contract — programmatic linking', () => {
|
||||
)
|
||||
|
||||
const initialLinkCount = world.links.size
|
||||
expect(() => srcHandle.connect(0, dstHandle, 0)).toThrow(TypeMismatchError)
|
||||
expect(() => srcHandle.connect(0, dstHandle, 0)).toThrow(
|
||||
TypeMismatchError
|
||||
)
|
||||
expect(world.links.size).toBe(initialLinkCount)
|
||||
})
|
||||
|
||||
|
||||
@@ -102,7 +102,10 @@ describe('BC.28 migration — subgraph fan-out via set/get virtual nodes', () =>
|
||||
target_id: number
|
||||
}
|
||||
|
||||
function v1GraphToPromptPatch(link: V1Link, resolvedOriginId: number): void {
|
||||
function v1GraphToPromptPatch(
|
||||
link: V1Link,
|
||||
resolvedOriginId: number
|
||||
): void {
|
||||
link.origin_id = resolvedOriginId // Mutation!
|
||||
}
|
||||
|
||||
@@ -157,8 +160,12 @@ describe('BC.28 migration — subgraph fan-out via set/get virtual nodes', () =>
|
||||
expect(typeof receivedGraph!.findByType).toBe('function')
|
||||
expect(typeof receivedGraph!.getNode).toBe('function')
|
||||
// No add, remove, or link mutation methods
|
||||
expect((receivedGraph as unknown as Record<string, unknown>).addNode).toBeUndefined()
|
||||
expect((receivedGraph as unknown as Record<string, unknown>).removeNode).toBeUndefined()
|
||||
expect(
|
||||
(receivedGraph as unknown as Record<string, unknown>).addNode
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
(receivedGraph as unknown as Record<string, unknown>).removeNode
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it.todo(
|
||||
@@ -171,7 +178,10 @@ describe('BC.28 migration — subgraph fan-out via set/get virtual nodes', () =>
|
||||
// This is documented but not type-checked here (Phase B implementation)
|
||||
|
||||
interface AppExtensionContext {
|
||||
on(event: 'beforePrompt', handler: (event: { spec: unknown }) => void): void
|
||||
on(
|
||||
event: 'beforePrompt',
|
||||
handler: (event: { spec: unknown }) => void
|
||||
): void
|
||||
}
|
||||
|
||||
// Type-level proof that the bridge API shape exists
|
||||
@@ -224,7 +234,14 @@ describe('BC.28 migration — subgraph fan-out via set/get virtual nodes', () =>
|
||||
const mockGraph: ReadOnlyGraph = {
|
||||
findByType: (type) =>
|
||||
type === 'SetNode'
|
||||
? [{ entityId: 'node:set', type: 'SetNode', title: 'myValue', getProperty: () => undefined }]
|
||||
? [
|
||||
{
|
||||
entityId: 'node:set',
|
||||
type: 'SetNode',
|
||||
title: 'myValue',
|
||||
getProperty: () => undefined
|
||||
}
|
||||
]
|
||||
: [],
|
||||
getNode: () => undefined
|
||||
}
|
||||
|
||||
@@ -162,8 +162,20 @@ describe('BC.28 v1 contract — subgraph fan-out via set/get virtual nodes', ()
|
||||
|
||||
graph._nodes = [upstreamNode, setNode, getNode, downstreamNode]
|
||||
graph.links = {
|
||||
100: { id: 100, origin_id: 1, origin_slot: 0, target_id: 2, target_slot: 0 },
|
||||
101: { id: 101, origin_id: 3, origin_slot: 0, target_id: 4, target_slot: 0 }
|
||||
100: {
|
||||
id: 100,
|
||||
origin_id: 1,
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_slot: 0
|
||||
},
|
||||
101: {
|
||||
id: 101,
|
||||
origin_id: 3,
|
||||
origin_slot: 0,
|
||||
target_id: 4,
|
||||
target_slot: 0
|
||||
}
|
||||
}
|
||||
|
||||
// graphToPrompt rewriting logic: replace Get→downstream with Set-source→downstream
|
||||
@@ -218,24 +230,69 @@ describe('BC.28 v1 contract — subgraph fan-out via set/get virtual nodes', ()
|
||||
it('multiple Get nodes referencing the same Set name all resolve to the same upstream', () => {
|
||||
const graph = createV1Graph()
|
||||
|
||||
const upstream: V1Node = { id: 1, type: 'KSampler', title: 'S', outputs: [{ links: [100] }] }
|
||||
const upstream: V1Node = {
|
||||
id: 1,
|
||||
type: 'KSampler',
|
||||
title: 'S',
|
||||
outputs: [{ links: [100] }]
|
||||
}
|
||||
const setNode: V1Node = {
|
||||
id: 2, type: 'SetNode', title: 'shared', inputs: [{ link: 100 }], outputs: []
|
||||
id: 2,
|
||||
type: 'SetNode',
|
||||
title: 'shared',
|
||||
inputs: [{ link: 100 }],
|
||||
outputs: []
|
||||
}
|
||||
const get1: V1Node = {
|
||||
id: 3, type: 'GetNode', title: 'shared', inputs: [], outputs: [{ links: [101] }]
|
||||
id: 3,
|
||||
type: 'GetNode',
|
||||
title: 'shared',
|
||||
inputs: [],
|
||||
outputs: [{ links: [101] }]
|
||||
}
|
||||
const get2: V1Node = {
|
||||
id: 4, type: 'GetNode', title: 'shared', inputs: [], outputs: [{ links: [102] }]
|
||||
id: 4,
|
||||
type: 'GetNode',
|
||||
title: 'shared',
|
||||
inputs: [],
|
||||
outputs: [{ links: [102] }]
|
||||
}
|
||||
const down1: V1Node = {
|
||||
id: 5,
|
||||
type: 'VAEDecode',
|
||||
title: 'D1',
|
||||
inputs: [{ link: 101 }]
|
||||
}
|
||||
const down2: V1Node = {
|
||||
id: 6,
|
||||
type: 'VAEDecode',
|
||||
title: 'D2',
|
||||
inputs: [{ link: 102 }]
|
||||
}
|
||||
const down1: V1Node = { id: 5, type: 'VAEDecode', title: 'D1', inputs: [{ link: 101 }] }
|
||||
const down2: V1Node = { id: 6, type: 'VAEDecode', title: 'D2', inputs: [{ link: 102 }] }
|
||||
|
||||
graph._nodes = [upstream, setNode, get1, get2, down1, down2]
|
||||
graph.links = {
|
||||
100: { id: 100, origin_id: 1, origin_slot: 0, target_id: 2, target_slot: 0 },
|
||||
101: { id: 101, origin_id: 3, origin_slot: 0, target_id: 5, target_slot: 0 },
|
||||
102: { id: 102, origin_id: 4, origin_slot: 0, target_id: 6, target_slot: 0 }
|
||||
100: {
|
||||
id: 100,
|
||||
origin_id: 1,
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_slot: 0
|
||||
},
|
||||
101: {
|
||||
id: 101,
|
||||
origin_id: 3,
|
||||
origin_slot: 0,
|
||||
target_id: 5,
|
||||
target_slot: 0
|
||||
},
|
||||
102: {
|
||||
id: 102,
|
||||
origin_id: 4,
|
||||
origin_slot: 0,
|
||||
target_id: 6,
|
||||
target_slot: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Both Get nodes should resolve to node 1 (upstream)
|
||||
|
||||
@@ -152,7 +152,10 @@ describe('BC.29 migration — graph enumeration, mutation, and cross-scope ident
|
||||
})
|
||||
|
||||
it('createNodeLocatorId is the same function in both v1 and v2 (re-exported)', () => {
|
||||
const locator = createNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 456)
|
||||
const locator = createNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
456
|
||||
)
|
||||
|
||||
expect(locator).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
expect(isNodeLocatorId(locator)).toBe(true)
|
||||
@@ -160,7 +163,9 @@ describe('BC.29 migration — graph enumeration, mutation, and cross-scope ident
|
||||
|
||||
it('v2 adds isNodeLocatorId type guard not present in v1 (enhancement)', () => {
|
||||
// v2 enhancement: type guard for runtime validation
|
||||
expect(isNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')).toBe(true)
|
||||
expect(isNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')).toBe(
|
||||
true
|
||||
)
|
||||
expect(isNodeLocatorId('invalid')).toBe(true) // simple string is valid root node ID
|
||||
expect(isNodeLocatorId('not-a-uuid:123')).toBe(false) // invalid uuid format
|
||||
expect(isNodeLocatorId(null)).toBe(false)
|
||||
@@ -171,7 +176,10 @@ describe('BC.29 migration — graph enumeration, mutation, and cross-scope ident
|
||||
const v1Style = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + ':' + '789'
|
||||
|
||||
// v2 pattern: use createNodeLocatorId
|
||||
const v2Style = createNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 789)
|
||||
const v2Style = createNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
789
|
||||
)
|
||||
|
||||
// Both produce the same result
|
||||
expect(v1Style).toBe(v2Style)
|
||||
|
||||
@@ -112,7 +112,9 @@ describe('BC.29 v1 contract — graph enumeration, mutation, and cross-scope ide
|
||||
|
||||
describe('S14.ID1 — cross-subgraph identity helpers', () => {
|
||||
it('parseNodeLocatorId(id) splits a locator string into { subgraphUuid, localNodeId } parts', () => {
|
||||
const result = parseNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')
|
||||
const result = parseNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
|
||||
)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.subgraphUuid).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
||||
@@ -120,7 +122,10 @@ describe('BC.29 v1 contract — graph enumeration, mutation, and cross-scope ide
|
||||
})
|
||||
|
||||
it('createNodeLocatorId(scope, localId) produces a stable colon-delimited locator string', () => {
|
||||
const locator = createNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 456)
|
||||
const locator = createNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
456
|
||||
)
|
||||
|
||||
expect(locator).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
@@ -59,12 +59,17 @@ describe('BC.29 v2 contract — graph enumeration, mutation, and cross-scope ide
|
||||
|
||||
const found = graph.findByType('KSampler')
|
||||
expect(found).toHaveLength(2)
|
||||
expect(found.every((n: unknown) => (n as { type: string }).type === 'KSampler')).toBe(true)
|
||||
expect(
|
||||
found.every((n: unknown) => (n as { type: string }).type === 'KSampler')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('comfyApp.graph.addNode(opts) creates and inserts a new node', () => {
|
||||
const graph = createMockGraphHandle()
|
||||
const node = graph.addNode({ type: 'TestNode' }) as { type: string; id: number }
|
||||
const node = graph.addNode({ type: 'TestNode' }) as {
|
||||
type: string
|
||||
id: number
|
||||
}
|
||||
|
||||
expect(node.type).toBe('TestNode')
|
||||
expect(node.id).toBeDefined()
|
||||
@@ -102,7 +107,9 @@ describe('BC.29 v2 contract — graph enumeration, mutation, and cross-scope ide
|
||||
})
|
||||
|
||||
it('isNodeLocatorId returns true for valid subgraph node IDs (uuid:localId)', () => {
|
||||
expect(isNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')).toBe(true)
|
||||
expect(
|
||||
isNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('isNodeLocatorId returns false for invalid formats', () => {
|
||||
@@ -113,10 +120,14 @@ describe('BC.29 v2 contract — graph enumeration, mutation, and cross-scope ide
|
||||
})
|
||||
|
||||
it('parseNodeLocatorId extracts subgraphUuid and localNodeId for subgraph nodes', () => {
|
||||
const result = parseNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
const result = parseNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.subgraphUuid).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
||||
expect(result!.subgraphUuid).toBe(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
)
|
||||
expect(result!.localNodeId).toBe(456)
|
||||
})
|
||||
|
||||
@@ -135,7 +146,10 @@ describe('BC.29 v2 contract — graph enumeration, mutation, and cross-scope ide
|
||||
|
||||
describe('NodeLocatorId creation', () => {
|
||||
it('createNodeLocatorId produces a colon-delimited string', () => {
|
||||
const id = createNodeLocatorId('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 123)
|
||||
const id = createNodeLocatorId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
123
|
||||
)
|
||||
|
||||
expect(id).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:123')
|
||||
expect(isNodeLocatorId(id)).toBe(true)
|
||||
@@ -208,11 +222,14 @@ describe('BC.29 v2 contract — graph enumeration, mutation, and cross-scope ide
|
||||
it('NodeLocatorId is stable across all instances of a subgraph', () => {
|
||||
// A locator ID like "uuid:123" identifies the same node definition
|
||||
// regardless of which execution path reaches it
|
||||
const locatorId: NodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
|
||||
const locatorId: NodeLocatorId =
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
|
||||
|
||||
expect(isNodeLocatorId(locatorId)).toBe(true)
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
expect(parsed!.subgraphUuid).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
||||
expect(parsed!.subgraphUuid).toBe(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,7 +98,9 @@ describe('BC.30 v2 contract — graph change tracking, batching, and reactivity
|
||||
})
|
||||
|
||||
it('graph._version does not exist on the v2 GraphHandle', () => {
|
||||
const graph = createReactiveGraphHandle() as unknown as { _version?: number }
|
||||
const graph = createReactiveGraphHandle() as unknown as {
|
||||
_version?: number
|
||||
}
|
||||
|
||||
expect(graph._version).toBeUndefined()
|
||||
})
|
||||
@@ -232,8 +234,6 @@ describe('BC.30 v2 contract — graph change tracking, batching, and reactivity
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it.todo(
|
||||
'[Phase B] setDirtyCanvas shim is wired into NodeHandle at runtime'
|
||||
)
|
||||
it.todo('[Phase B] setDirtyCanvas shim is wired into NodeHandle at runtime')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user