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:
Christian Byrne
2026-05-10 20:09:59 -07:00
committed by Connor Byrne
parent 0f5e0303d5
commit cdb61c16cd
3 changed files with 32 additions and 163 deletions

View File

@@ -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] ?? ''
}

View File

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

View File

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