diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 89ad4df17..1acc90778 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -70,6 +70,10 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy' import { type ExtensionManager } from '@/types/extensionTypes' import type { NodeExecutionId } from '@/types/nodeIdentification' import { graphToPrompt } from '@/utils/executionUtil' +import { + createMissingNodeTypeFromError, + type MissingNodeTypeExtraInfo +} from '@/utils/missingNodeUtil' import { anyItemOverlapsRect } from '@/utils/mathUtil' import { collectAllNodes, forEachNode } from '@/utils/graphTraversalUtil' import { @@ -1399,28 +1403,9 @@ export class ComfyApp { typeof error.response.error === 'object' && error.response.error?.type === 'missing_node_type' ) { - const extraInfo = (error.response.error.extra_info ?? {}) as { - class_type?: string | null - node_title?: string | null - node_id?: string - } - const classType = extraInfo.class_type ?? 'Unknown' - const nodeTitle = extraInfo.node_title ?? classType - const nodeId = extraInfo.node_id - const hint = (() => { - if (nodeTitle !== classType && nodeId) { - return `"${nodeTitle}" (Node ID #${nodeId})` - } else if (nodeTitle !== classType) { - return `"${nodeTitle}"` - } else if (nodeId) { - return `Node ID #${nodeId}` - } - return undefined - })() - const missingNodeType: MissingNodeType = { - type: classType, - ...(hint && { hint }) - } + const extraInfo = (error.response.error.extra_info ?? + {}) as MissingNodeTypeExtraInfo + const missingNodeType = createMissingNodeTypeFromError(extraInfo) this.showMissingNodesError([missingNodeType]) } else { useDialogService().showErrorDialog(error, { diff --git a/src/utils/missingNodeUtil.test.ts b/src/utils/missingNodeUtil.test.ts new file mode 100644 index 000000000..a2966551a --- /dev/null +++ b/src/utils/missingNodeUtil.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' + +import { + buildMissingNodeHint, + createMissingNodeTypeFromError +} from './missingNodeUtil' + +describe('buildMissingNodeHint', () => { + it('returns hint with title and node ID when both available', () => { + expect(buildMissingNodeHint('My Node', 'MyNodeClass', '42')).toBe( + '"My Node" (Node ID #42)' + ) + }) + + it('returns hint with title only when no node ID', () => { + expect(buildMissingNodeHint('My Node', 'MyNodeClass', undefined)).toBe( + '"My Node"' + ) + }) + + it('returns hint with node ID only when title matches class type', () => { + expect(buildMissingNodeHint('MyNodeClass', 'MyNodeClass', '42')).toBe( + 'Node ID #42' + ) + }) + + it('returns undefined when title matches class type and no node ID', () => { + expect( + buildMissingNodeHint('MyNodeClass', 'MyNodeClass', undefined) + ).toBeUndefined() + }) + + it('returns undefined when title is null and no node ID', () => { + expect(buildMissingNodeHint(null, 'MyNodeClass', undefined)).toBeUndefined() + }) + + it('returns node ID hint when title is null but node ID exists', () => { + expect(buildMissingNodeHint(null, 'MyNodeClass', '42')).toBe('Node ID #42') + }) +}) + +describe('createMissingNodeTypeFromError', () => { + it('returns string type when no hint is generated', () => { + const result = createMissingNodeTypeFromError({ + class_type: 'MyNodeClass', + node_title: 'MyNodeClass' + }) + expect(result).toBe('MyNodeClass') + }) + + it('returns object with hint when title differs from class type', () => { + const result = createMissingNodeTypeFromError({ + class_type: 'MyNodeClass', + node_title: 'My Custom Title', + node_id: '42' + }) + expect(result).toEqual({ + type: 'MyNodeClass', + hint: '"My Custom Title" (Node ID #42)' + }) + }) + + it('handles null class_type by defaulting to Unknown', () => { + const result = createMissingNodeTypeFromError({ + class_type: null, + node_title: 'Some Title', + node_id: '42' + }) + expect(result).toEqual({ + type: 'Unknown', + hint: '"Some Title" (Node ID #42)' + }) + }) + + it('handles empty extra_info', () => { + const result = createMissingNodeTypeFromError({}) + expect(result).toBe('Unknown') + }) + + it('returns object with node ID hint when only node_id is available', () => { + const result = createMissingNodeTypeFromError({ + class_type: 'MyNodeClass', + node_id: '123' + }) + expect(result).toEqual({ + type: 'MyNodeClass', + hint: 'Node ID #123' + }) + }) +}) diff --git a/src/utils/missingNodeUtil.ts b/src/utils/missingNodeUtil.ts new file mode 100644 index 000000000..fcaae1416 --- /dev/null +++ b/src/utils/missingNodeUtil.ts @@ -0,0 +1,44 @@ +import type { MissingNodeType } from '@/types/comfy' + +/** + * Extra info returned by the backend for missing_node_type errors + */ +export interface MissingNodeTypeExtraInfo { + class_type?: string | null + node_title?: string | null + node_id?: string +} + +/** + * Builds a hint string from missing node metadata. + * Provides context about which node is missing (title, ID) when available. + */ +export function buildMissingNodeHint( + nodeTitle: string | null | undefined, + classType: string, + nodeId: string | undefined +): string | undefined { + const hasTitle = nodeTitle && nodeTitle !== classType + if (hasTitle && nodeId) { + return `"${nodeTitle}" (Node ID #${nodeId})` + } else if (hasTitle) { + return `"${nodeTitle}"` + } else if (nodeId) { + return `Node ID #${nodeId}` + } + return undefined +} + +/** + * Creates a MissingNodeType from backend error extra_info. + * Used when the /prompt endpoint returns a missing_node_type error. + */ +export function createMissingNodeTypeFromError( + extraInfo: MissingNodeTypeExtraInfo +): MissingNodeType { + const classType = extraInfo.class_type ?? 'Unknown' + const nodeTitle = extraInfo.node_title ?? classType + const hint = buildMissingNodeHint(nodeTitle, classType, extraInfo.node_id) + + return hint ? { type: classType, hint } : classType +}