Compare commits

...

4 Commits

Author SHA1 Message Date
GitHub Action
def709cd7d [automated] Apply ESLint and Oxfmt fixes 2026-03-24 02:13:53 +00:00
Matt Miller
e6a423c36e fix: unexport FILE_INPUT_FIELDS and fix lint
- Remove export from FILE_INPUT_FIELDS (flagged by knip as unused export)
- Extract EmptyFileInputNode interface to fix oxfmt line length

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:11:01 -07:00
Matt Miller
1b6c2f0add fix: address review findings for empty file input validation
- Broaden check to catch null, undefined, and whitespace-only values
- Skip linked inputs (array refs to upstream nodes) to avoid false positives
- Scope validation to target nodes during partial execution
- Use `as const` for FILE_INPUT_FIELDS
- Add test coverage for edge cases (null, undefined, whitespace, linked
  inputs, partial execution filtering, missing fields)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:41:04 -07:00
Matt Miller
68c6a9d7e2 fix: block workflow queue when file input nodes have empty selections
Prevents submitting workflows where LoadImage, LoadAudio, Load3D, or
LoadVideo nodes have no file selected. Shows a localized error dialog
listing the affected nodes instead of sending an invalid request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:29:53 -07:00
3 changed files with 181 additions and 1 deletions

View File

@@ -1889,7 +1889,9 @@
"extensionFileHint": "This may be due to the following script",
"promptExecutionError": "Prompt execution failed",
"accessRestrictedTitle": "Access Restricted",
"accessRestrictedMessage": "Your account is not authorized for this feature."
"accessRestrictedMessage": "Your account is not authorized for this feature.",
"emptyFileInputTitle": "Missing File Inputs",
"emptyFileInputMessage": "The following nodes require a file to be selected: {nodeList}. Please upload or select files before running."
},
"apiNodesSignInDialog": {
"title": "Sign In Required to Use API Nodes",

View File

@@ -146,6 +146,45 @@ import {
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
const FILE_INPUT_FIELDS = {
LoadImage: 'image',
LoadAudio: 'audio',
Load3D: 'model_file',
LoadVideo: 'video'
} as const
function isEmptyFileValue(value: unknown): boolean {
if (Array.isArray(value)) return false // linked input from another node
if (typeof value === 'string') return value.trim() === ''
return value == null
}
interface EmptyFileInputNode {
nodeId: string
classType: string
title: string
}
export function findEmptyFileInputNodes(
output: ComfyApiWorkflow,
nodeIds?: Set<string>
): EmptyFileInputNode[] {
const result: EmptyFileInputNode[] = []
for (const [nodeId, node] of Object.entries(output)) {
if (nodeIds && !nodeIds.has(nodeId)) continue
const field =
FILE_INPUT_FIELDS[node.class_type as keyof typeof FILE_INPUT_FIELDS]
if (field && isEmptyFileValue(node.inputs[field])) {
result.push({
nodeId,
classType: node.class_type,
title: node._meta?.title ?? node.class_type
})
}
}
return result
}
export function sanitizeNodeName(string: string) {
let entityMap = {
'&': '',
@@ -1615,6 +1654,25 @@ export class ComfyApp {
const queuedWorkflow = useWorkspaceStore().workflow
.activeWorkflow as ComfyWorkflow
const p = await this.graphToPrompt(this.rootGraph)
const targetNodeIds = isPartialExecution
? new Set(queueNodeIds!)
: undefined
const emptyFileInputNodes = findEmptyFileInputNodes(
p.output,
targetNodeIds
)
if (emptyFileInputNodes.length) {
const nodeList = emptyFileInputNodes
.map((n) => `#${n.nodeId} ${n.title}`)
.join(', ')
useDialogService().showErrorDialog(
new Error(t('errorDialog.emptyFileInputMessage', { nodeList })),
{ title: t('errorDialog.emptyFileInputTitle') }
)
break
}
const queuedNodes = collectAllNodes(this.rootGraph)
try {
api.authToken = comfyOrgAuthToken

View File

@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest'
import type { ComfyApiWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
import { findEmptyFileInputNodes } from './app'
function makeNode(
classType: string,
inputs: Record<string, unknown>,
title?: string
) {
return {
class_type: classType,
inputs,
_meta: { title: title ?? classType }
}
}
describe('findEmptyFileInputNodes', () => {
it('detects LoadImage with empty image field', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: '' }),
'2': makeNode('KSampler', { seed: 42 })
}
expect(findEmptyFileInputNodes(output)).toEqual([
{ nodeId: '1', classType: 'LoadImage', title: 'LoadImage' }
])
})
it('detects multiple empty file input nodes', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: '' }, 'My Image'),
'2': makeNode('LoadAudio', { audio: '' }),
'3': makeNode('LoadVideo', { video: 'file.mp4' })
}
const result = findEmptyFileInputNodes(output)
expect(result).toHaveLength(2)
expect(result[0]).toEqual({
nodeId: '1',
classType: 'LoadImage',
title: 'My Image'
})
expect(result[1]).toEqual({
nodeId: '2',
classType: 'LoadAudio',
title: 'LoadAudio'
})
})
it('returns empty array when all file inputs are populated', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: 'photo.png' }),
'2': makeNode('Load3D', { model_file: 'model.glb' })
}
expect(findEmptyFileInputNodes(output)).toEqual([])
})
it('returns empty array when no file input nodes exist', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('KSampler', { seed: 42 }),
'2': makeNode('CLIPTextEncode', { text: 'hello' })
}
expect(findEmptyFileInputNodes(output)).toEqual([])
})
it('detects Load3D with empty model_file', () => {
const output: ComfyApiWorkflow = {
'5': makeNode('Load3D', { model_file: '' })
}
expect(findEmptyFileInputNodes(output)).toEqual([
{ nodeId: '5', classType: 'Load3D', title: 'Load3D' }
])
})
it('detects null file input values', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: null })
}
expect(findEmptyFileInputNodes(output)).toHaveLength(1)
})
it('detects undefined file input values', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: undefined })
}
expect(findEmptyFileInputNodes(output)).toHaveLength(1)
})
it('detects whitespace-only file input values', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: ' ' })
}
expect(findEmptyFileInputNodes(output)).toHaveLength(1)
})
it('skips linked inputs (array references to other nodes)', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: ['5', 0] })
}
expect(findEmptyFileInputNodes(output)).toEqual([])
})
it('filters to only specified node IDs when provided', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', { image: '' }),
'2': makeNode('LoadAudio', { audio: '' }),
'3': makeNode('KSampler', { seed: 42 })
}
const result = findEmptyFileInputNodes(output, new Set(['2', '3']))
expect(result).toEqual([
{ nodeId: '2', classType: 'LoadAudio', title: 'LoadAudio' }
])
})
it('detects missing file input field entirely', () => {
const output: ComfyApiWorkflow = {
'1': makeNode('LoadImage', {})
}
expect(findEmptyFileInputNodes(output)).toHaveLength(1)
})
})