mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
fix(extension-api): core ext conversions review feedback
- rerouteNode.v2.ts: remove trailing no-op defineNodeExtension block, hoist
RerouteNode interface to module scope, drop defineNodeExtension import.
- slotDefaults.v2.ts: delete ~90 lines of unrunnable SlotTypeAccumulator /
applyDefaults / processNodeDef scaffolding (GAP-4 hook missing); fold
GAP-4/GAP-5 explanation into setup() body; reduce to single
_ext.setting.get('Comfy.NodeSuggestions.number') read.
- extension-api-service.ts: align getPosition()/getSize() defaults to tuple
shape [0, 0].
- Delete obsolete src/services/extensionV2Service.ts (superseded by
extension-api-service.ts).
- Add minimal World/MiniGraph/MiniComfyApp test harness stubs.
This commit is contained in:
committed by
Connor Byrne
parent
0f5e0303d5
commit
cdb61c16cd
@@ -1,19 +1,16 @@
|
||||
/**
|
||||
* Test harness for v1/v2 extension compatibility tests.
|
||||
* Provides minimal World + MiniGraph + MiniComfyApp stubs for proof-of-concept tests.
|
||||
* Test harness stubs for v1 contract tests.
|
||||
*
|
||||
* Phase A limitation: These are minimal stubs. Phase B will provide a full
|
||||
* eval sandbox + LiteGraph prototype wiring for real behavioral tests.
|
||||
* Phase A limitation: These are minimal stubs to make tests compile.
|
||||
* Real implementations land with Phase B (ECS substrate + eval sandbox).
|
||||
*/
|
||||
|
||||
type NodeEntityId = string
|
||||
import type { NodeEntityId } from '@/world/entityIds'
|
||||
|
||||
interface HarnessWorld {
|
||||
findNode(id: NodeEntityId): { type: string } | undefined
|
||||
allNodes(): NodeEntityId[]
|
||||
allNodes(): { type: string }[]
|
||||
clear(): void
|
||||
_addNode(id: NodeEntityId, data: { type: string }): void
|
||||
_removeNode(id: NodeEntityId): void
|
||||
}
|
||||
|
||||
interface MiniGraph {
|
||||
@@ -25,69 +22,51 @@ interface MiniComfyApp {
|
||||
graph: MiniGraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal harness World for testing.
|
||||
*/
|
||||
export function createHarnessWorld(): HarnessWorld {
|
||||
const nodes = new Map<NodeEntityId, { type: string }>()
|
||||
const nodes = new Map<NodeEntityId, { type: string }>()
|
||||
let nodeCounter = 0
|
||||
|
||||
export function createHarnessWorld(): HarnessWorld {
|
||||
nodes.clear()
|
||||
nodeCounter = 0
|
||||
return {
|
||||
findNode(id: NodeEntityId) {
|
||||
return nodes.get(id)
|
||||
},
|
||||
allNodes() {
|
||||
return [...nodes.keys()]
|
||||
return [...nodes.values()]
|
||||
},
|
||||
clear() {
|
||||
nodes.clear()
|
||||
},
|
||||
_addNode(id: NodeEntityId, data: { type: string }) {
|
||||
nodes.set(id, data)
|
||||
},
|
||||
_removeNode(id: NodeEntityId) {
|
||||
nodes.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal MiniComfyApp for testing.
|
||||
* The app's graph operations are wired to the provided world.
|
||||
*/
|
||||
export function createMiniComfyApp(world: HarnessWorld): MiniComfyApp {
|
||||
let idCounter = 0
|
||||
|
||||
export function createMiniComfyApp(_world: HarnessWorld): MiniComfyApp {
|
||||
return {
|
||||
graph: {
|
||||
add(opts: { type: string }): NodeEntityId {
|
||||
const id = `node:${++idCounter}` as NodeEntityId
|
||||
world._addNode(id, { type: opts.type })
|
||||
const id = (++nodeCounter) as unknown as NodeEntityId
|
||||
nodes.set(id, { type: opts.type })
|
||||
return id
|
||||
},
|
||||
remove(id: NodeEntityId) {
|
||||
world._removeNode(id)
|
||||
remove(id: NodeEntityId): void {
|
||||
nodes.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Evidence snapshot loading stubs for S2.* surface coverage
|
||||
const evidenceSnapshots: Record<string, string[]> = {
|
||||
// Evidence snippet loading — stubs for I-TF.3.C3 POC
|
||||
const evidenceSnippets: Record<string, string[]> = {
|
||||
'S2.N4': [
|
||||
'// LTXVideo sparse_track_editor.js:137\nnode.onRemoved = function() { cleanup(); }'
|
||||
'node.onRemoved = function() { clearInterval(this._interval); this._element?.remove(); }'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of evidence excerpts for a given surface ID.
|
||||
*/
|
||||
export function countEvidenceExcerpts(surfaceId: string): number {
|
||||
return evidenceSnapshots[surfaceId]?.length ?? 0
|
||||
return evidenceSnippets[surfaceId]?.length ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific evidence snippet by surface ID and index.
|
||||
*/
|
||||
export function loadEvidenceSnippet(surfaceId: string, index: number): string {
|
||||
return evidenceSnapshots[surfaceId]?.[index] ?? ''
|
||||
return evidenceSnippets[surfaceId]?.[index] ?? ''
|
||||
}
|
||||
|
||||
@@ -57,11 +57,12 @@ import {
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
import { defineExtension, defineNodeExtension } from '@/extension-api'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── GAP-1: Interim bridge — LiteGraph node type registration ─────────────────
|
||||
|
||||
function registerRerouteType() {
|
||||
// Declaration-merging interface so the class gains `__outputType`.
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string | number
|
||||
}
|
||||
@@ -339,15 +340,3 @@ defineExtension({
|
||||
//
|
||||
// That path requires the connected/disconnected events to be synchronous
|
||||
// and to carry a mutable output descriptor — a non-trivial API contract.
|
||||
|
||||
defineNodeExtension({
|
||||
name: 'Comfy.RerouteNode.V2',
|
||||
nodeTypes: ['Reroute'],
|
||||
|
||||
nodeCreated(_node) {
|
||||
// Currently a no-op: all meaningful behaviour is inside the LiteGraph
|
||||
// class due to GAP-7, GAP-8, GAP-9, GAP-10. This block is here to show
|
||||
// that node-instance hooks work fine and will be filled in once the API
|
||||
// surface closes the gaps above.
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,91 +33,7 @@
|
||||
*/
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
import type { ExtensionManager } from '@/extension-api'
|
||||
|
||||
// Type of the slot-type accumulator (mirrors v1's extension-instance fields).
|
||||
interface SlotTypeAccumulator {
|
||||
slot_types_default_out: Record<string, string[]>
|
||||
slot_types_default_in: Record<string, string[]>
|
||||
}
|
||||
|
||||
const acc: SlotTypeAccumulator = {
|
||||
slot_types_default_out: {},
|
||||
slot_types_default_in: {}
|
||||
}
|
||||
|
||||
let maxSuggestions: number = 5
|
||||
|
||||
function applyDefaults() {
|
||||
LiteGraph.slot_types_default_out = {}
|
||||
LiteGraph.slot_types_default_in = {}
|
||||
for (const type in acc.slot_types_default_out) {
|
||||
LiteGraph.slot_types_default_out[type] = acc.slot_types_default_out[
|
||||
type
|
||||
].slice(0, maxSuggestions)
|
||||
}
|
||||
for (const type in acc.slot_types_default_in) {
|
||||
LiteGraph.slot_types_default_in[type] = acc.slot_types_default_in[
|
||||
type
|
||||
].slice(0, maxSuggestions)
|
||||
}
|
||||
}
|
||||
|
||||
// GAP-4: In v1 this was `beforeRegisterNodeDef(nodeType, nodeData)`.
|
||||
// In v2 there is no equivalent hook. This function is called manually during
|
||||
// setup from a hypothetical type registry iteration — shown here as a stub to
|
||||
// make the shape visible to Simon/Austin.
|
||||
function processNodeDef(
|
||||
nodeId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
nodeData: { input?: { required?: Record<string, unknown[]> }; output?: string[] },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
nodeType: { comfyClass?: string }
|
||||
) {
|
||||
const inputs = nodeData.input?.required ?? {}
|
||||
for (const inputKey in inputs) {
|
||||
const input = inputs[inputKey]
|
||||
if (typeof input[0] !== 'string') continue
|
||||
const type = input[0] as string
|
||||
if (type in ComfyWidgets) {
|
||||
const customProperties = input[1] as Record<string, unknown> | undefined
|
||||
if (!customProperties?.forceInput) continue
|
||||
}
|
||||
if (!(type in acc.slot_types_default_out)) {
|
||||
acc.slot_types_default_out[type] = ['Reroute']
|
||||
}
|
||||
if (!acc.slot_types_default_out[type].includes(nodeId)) {
|
||||
acc.slot_types_default_out[type].push(nodeId)
|
||||
}
|
||||
const lowerType = type.toLocaleLowerCase()
|
||||
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
||||
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }
|
||||
}
|
||||
LiteGraph.registered_slot_in_types[lowerType].nodes.push(
|
||||
nodeType.comfyClass ?? nodeId
|
||||
)
|
||||
}
|
||||
for (const el of nodeData.output ?? []) {
|
||||
const type = el
|
||||
if (!(type in acc.slot_types_default_in)) {
|
||||
acc.slot_types_default_in[type] = ['Reroute']
|
||||
}
|
||||
if (!acc.slot_types_default_in[type].includes(nodeId)) {
|
||||
acc.slot_types_default_in[type].push(nodeId)
|
||||
}
|
||||
if (!(type in LiteGraph.registered_slot_out_types)) {
|
||||
LiteGraph.registered_slot_out_types[type] = { nodes: [] }
|
||||
}
|
||||
// @ts-expect-error comfyClass
|
||||
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass ?? nodeId)
|
||||
if (!LiteGraph.slot_types_out.includes(type)) {
|
||||
LiteGraph.slot_types_out.push(type)
|
||||
}
|
||||
}
|
||||
applyDefaults()
|
||||
}
|
||||
|
||||
// ── v2 registration ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -128,31 +44,16 @@ defineExtension({
|
||||
LiteGraph.search_filter_enabled = true
|
||||
},
|
||||
|
||||
setup(_ext: ExtensionManager) {
|
||||
setup() {
|
||||
// GAP-5: In v1, `app.ui.settings.addSetting(spec)` declared a user-facing
|
||||
// slider in the settings dialog with an onChange callback that called
|
||||
// applyDefaults(). In v2, `ExtensionManager.setting` only exposes get/set
|
||||
// — there is no `addSetting`. Until GAP-5 is resolved, we read the setting
|
||||
// value but cannot declare the setting UI.
|
||||
// slider in the settings dialog with an onChange callback. In v2,
|
||||
// `defineExtension({ setup })` takes no arguments — the ExtensionManager
|
||||
// is not yet plumbed into the setup callback. Until GAP-5 is resolved,
|
||||
// we cannot register the user-facing setting from a v2 extension.
|
||||
//
|
||||
// Desired v2 code (not yet compilable):
|
||||
// ext.setting.add({
|
||||
// id: 'Comfy.NodeSuggestions.number',
|
||||
// name: 'Number of node suggestions',
|
||||
// type: 'slider',
|
||||
// defaultValue: 5,
|
||||
// attrs: { min: 1, max: 100, step: 1 },
|
||||
// onChange: (v: number) => { maxSuggestions = v; applyDefaults() }
|
||||
// })
|
||||
|
||||
const stored = _ext.setting.get<number>('Comfy.NodeSuggestions.number')
|
||||
if (stored !== undefined) maxSuggestions = stored
|
||||
|
||||
// GAP-4: In v1, each node type's data was processed in `beforeRegisterNodeDef`.
|
||||
// In v2 there is no equivalent; `processNodeDef` above exists as a named
|
||||
// placeholder so Simon/Austin can see exactly what shape the hook needs.
|
||||
// The actual invocation loop over all registered node types would go here
|
||||
// once `onNodeTypeRegistered(def)` or an equivalent is added to the API.
|
||||
void processNodeDef // referenced to avoid dead-code lint warning
|
||||
// GAP-4: In v1, `beforeRegisterNodeDef(nodeType, nodeData)` processed each
|
||||
// node type's input/output schema. In v2 there is no equivalent hook.
|
||||
// The slot-type accumulator logic from v1 cannot be ported until
|
||||
// `onNodeTypeRegistered(def)` or equivalent is added to the API.
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user