mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
## Summary Preview as Text (`PreviewAny`) nodes were re-executing on every prompt submission because the rendered preview text was being echoed back to the backend as input values, mutating the cache signature. ## Changes - **What**: Set `widget.options.serialize = false` on the three widgets the `Comfy.PreviewAny` extension adds (`preview_markdown`, `preview_text`, `previewMode`) so they are excluded from the API prompt sent to the backend. ## Root cause The extension was setting `widget.serialize = false`, which only controls **workflow JSON** persistence (checked in `LGraphNode.serialize`). The **API prompt** serializer in `executionUtil.graphToPrompt` checks `widget.options.serialize` instead — a distinct property documented in litegraph's `WIDGET_SERIALIZATION` convention. After `onExecuted` writes the rendered text into the widget value, the next `graphToPrompt` call serialized that text into `inputs.preview_text` / `inputs.preview_markdown`. The backend cache signature in `comfy_execution/caching.py` hashes all keys in `node["inputs"]`, so the changing text invalidated the cache and forced a redundant execution every time. ## Review Focus Two commits, red-green TDD: 1. `test:` failing unit + e2e tests asserting the desired behavior. 2. `fix:` adds `options.serialize = false` to make them pass. Tests verify the widgets are excluded from the API prompt; e2e additionally simulates a prior execution populating widget values to mirror the real bug condition. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12010-fix-stop-PreviewAny-widgets-from-triggering-re-execution-3586d73d3650810585cdd077f3ac64f5) by [Unito](https://www.unito.io)
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { ComfyExtension } from '@/types/comfy'
|
|
|
|
const capturedExtensions: ComfyExtension[] = []
|
|
|
|
vi.mock('@/services/extensionService', () => ({
|
|
useExtensionService: () => ({
|
|
registerExtension: (ext: ComfyExtension) => {
|
|
capturedExtensions.push(ext)
|
|
}
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/scripts/app', () => ({ app: {} }))
|
|
|
|
interface MockWidget {
|
|
name: string
|
|
options: Record<string, unknown>
|
|
element: { readOnly: boolean }
|
|
callback?: (value: unknown) => void
|
|
value: unknown
|
|
hidden: boolean
|
|
label: string
|
|
serialize?: boolean
|
|
}
|
|
|
|
const createdWidgets: MockWidget[] = []
|
|
|
|
vi.mock('@/scripts/widgets', () => {
|
|
const create =
|
|
(kind: string) =>
|
|
(
|
|
node: { widgets?: MockWidget[] },
|
|
name: string,
|
|
_info: unknown,
|
|
_app: unknown
|
|
) => {
|
|
const widget: MockWidget = {
|
|
name,
|
|
options: {},
|
|
element: { readOnly: false },
|
|
value: kind === 'BOOLEAN' ? false : '',
|
|
hidden: false,
|
|
label: ''
|
|
}
|
|
node.widgets = node.widgets ?? []
|
|
node.widgets.push(widget)
|
|
createdWidgets.push(widget)
|
|
return { widget }
|
|
}
|
|
return {
|
|
ComfyWidgets: {
|
|
MARKDOWN: create('MARKDOWN'),
|
|
STRING: create('STRING'),
|
|
BOOLEAN: create('BOOLEAN')
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('PreviewAny extension', () => {
|
|
beforeEach(async () => {
|
|
capturedExtensions.length = 0
|
|
createdWidgets.length = 0
|
|
vi.resetModules()
|
|
await import('./previewAny')
|
|
})
|
|
|
|
async function setupNode() {
|
|
const ext = capturedExtensions.find((e) => e.name === 'Comfy.PreviewAny')
|
|
expect(ext).toBeDefined()
|
|
|
|
const nodeType = { prototype: {} } as unknown as Parameters<
|
|
NonNullable<ComfyExtension['beforeRegisterNodeDef']>
|
|
>[0]
|
|
const nodeData = { name: 'PreviewAny' } as Parameters<
|
|
NonNullable<ComfyExtension['beforeRegisterNodeDef']>
|
|
>[1]
|
|
|
|
await ext!.beforeRegisterNodeDef!(
|
|
nodeType,
|
|
nodeData,
|
|
{} as Parameters<NonNullable<ComfyExtension['beforeRegisterNodeDef']>>[2]
|
|
)
|
|
|
|
const node: { widgets?: MockWidget[] } = {}
|
|
const proto = nodeType.prototype as { onNodeCreated?: () => void }
|
|
proto.onNodeCreated!.call(node)
|
|
return node
|
|
}
|
|
|
|
it('excludes preview widgets from the API prompt to prevent re-execution', async () => {
|
|
await setupNode()
|
|
|
|
const previewMarkdown = createdWidgets.find(
|
|
(w) => w.name === 'preview_markdown'
|
|
)
|
|
const previewText = createdWidgets.find((w) => w.name === 'preview_text')
|
|
const previewMode = createdWidgets.find((w) => w.name === 'previewMode')
|
|
|
|
expect(previewMarkdown).toBeDefined()
|
|
expect(previewText).toBeDefined()
|
|
expect(previewMode).toBeDefined()
|
|
|
|
// widget.options.serialize === false is what executionUtil.graphToPrompt
|
|
// checks to exclude a widget from the API prompt sent to the backend.
|
|
// Without this, post-execution widget value updates (the rendered preview
|
|
// text) get serialized as inputs, change the cache signature, and cause
|
|
// the node to re-execute on the next prompt.
|
|
expect(previewMarkdown!.options.serialize).toBe(false)
|
|
expect(previewText!.options.serialize).toBe(false)
|
|
expect(previewMode!.options.serialize).toBe(false)
|
|
})
|
|
})
|