mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 17:28:58 +00:00
Compare commits
5 Commits
main
...
jaeone/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5975ddccb | ||
|
|
9b140ca405 | ||
|
|
56f5e7c0a7 | ||
|
|
e9841e6564 | ||
|
|
23e2882f21 |
@@ -152,9 +152,9 @@ describe('ErrorOverlay', () => {
|
||||
renderOverlay()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Only error'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -189,9 +189,9 @@ describe('ErrorOverlay', () => {
|
||||
renderOverlay({ appMode: true })
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Only error'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -131,9 +131,9 @@ describe('useErrorOverlayState', () => {
|
||||
mountOverlayState()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Only error'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -168,9 +168,9 @@ describe('useErrorOverlayState', () => {
|
||||
mountOverlayState()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Required input is missing'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -207,9 +207,9 @@ describe('useErrorOverlayState', () => {
|
||||
mountOverlayState()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Raw validation error'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('useErrorOverlayState', () => {
|
||||
mountOverlayState()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastExecutionError = {
|
||||
executionErrorStore.recordExecutionError({
|
||||
prompt_id: 'prompt',
|
||||
node_id: 1,
|
||||
node_type: 'KSampler',
|
||||
@@ -257,7 +257,7 @@ describe('useErrorOverlayState', () => {
|
||||
exception_type: 'torch.OutOfMemoryError',
|
||||
traceback: [],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -474,9 +474,9 @@ describe('useErrorOverlayState', () => {
|
||||
mountOverlayState()
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': makeNodeError(['Only error'])
|
||||
}
|
||||
})
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -63,10 +63,7 @@ const LOADER_NODE = { id: '2', title: 'LoaderNode' }
|
||||
|
||||
function seedTwoErrorGroups(pinia: TestingPinia) {
|
||||
const executionErrorStore = useExecutionErrorStore(pinia)
|
||||
executionErrorStore.lastNodeErrors = fromAny<
|
||||
typeof executionErrorStore.lastNodeErrors,
|
||||
unknown
|
||||
>({
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -83,7 +80,11 @@ function seedTwoErrorGroups(pinia: TestingPinia) {
|
||||
class_type: 'CLIPLoader',
|
||||
dependent_outputs: [],
|
||||
errors: [
|
||||
{ type: 'weird_error', message: 'Something odd happened', details: '' }
|
||||
{
|
||||
type: 'weird_error',
|
||||
message: 'Something odd happened',
|
||||
details: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import type { TestingPinia } from '@pinia/testing'
|
||||
import { render, screen, within } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import TabErrors from './TabErrors.vue'
|
||||
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
|
||||
import type { MissingModelCandidate } from '@/platform/missingModel/types'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { nodeError, validationError } from '@/utils/__tests__/nodeErrorHelpers'
|
||||
|
||||
const mockFocusNode = vi.hoisted(() => vi.fn())
|
||||
const { mockFocusNode, mockRefreshMissingModels } = vi.hoisted(() => ({
|
||||
mockFocusNode: vi.fn(),
|
||||
mockRefreshMissingModels: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
refreshMissingModels: mockRefreshMissingModels,
|
||||
rootGraph: {
|
||||
serialize: vi.fn(() => ({})),
|
||||
getNodeById: vi.fn()
|
||||
@@ -97,18 +106,16 @@ describe('TabErrors.vue', () => {
|
||||
})
|
||||
})
|
||||
|
||||
function renderComponent(initialState = {}) {
|
||||
function renderComponent(seed?: (pinia: TestingPinia) => void) {
|
||||
const user = userEvent.setup()
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false
|
||||
})
|
||||
seed?.(pinia)
|
||||
render(TabErrors, {
|
||||
global: {
|
||||
plugins: [
|
||||
PrimeVue,
|
||||
i18n,
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
initialState
|
||||
})
|
||||
],
|
||||
plugins: [PrimeVue, i18n, pinia],
|
||||
stubs: {
|
||||
AsyncSearchInput: {
|
||||
template:
|
||||
@@ -129,14 +136,12 @@ describe('TabErrors.vue', () => {
|
||||
})
|
||||
|
||||
it('renders prompt-level errors with resolved display message', async () => {
|
||||
renderComponent({
|
||||
executionError: {
|
||||
lastPromptError: {
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'Server Error: No outputs',
|
||||
details: 'Error details'
|
||||
}
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordPromptError({
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'Server Error: No outputs',
|
||||
details: 'Error details'
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getAllByText('Prompt has no outputs').length).toBeGreaterThan(
|
||||
@@ -162,45 +167,40 @@ describe('TabErrors.vue', () => {
|
||||
} as ReturnType<typeof getNodeByExecutionId>
|
||||
})
|
||||
|
||||
const { user } = renderComponent({
|
||||
executionError: {
|
||||
lastNodeErrors: {
|
||||
'2': {
|
||||
class_type: 'CLIPTextEncode',
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: clip',
|
||||
extra_info: {
|
||||
input_name: 'clip'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: positive',
|
||||
extra_info: {
|
||||
input_name: 'positive'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: model',
|
||||
extra_info: {
|
||||
input_name: 'model'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordNodeErrors({
|
||||
'2': nodeError(
|
||||
[
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'clip',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: clip'
|
||||
)
|
||||
],
|
||||
'CLIPTextEncode'
|
||||
),
|
||||
'1': nodeError(
|
||||
[
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'positive',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: positive'
|
||||
),
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'model',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: model'
|
||||
)
|
||||
],
|
||||
'KSampler'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getByText('Missing connection')).toBeInTheDocument()
|
||||
@@ -269,18 +269,17 @@ describe('TabErrors.vue', () => {
|
||||
title: 'KSampler'
|
||||
} as ReturnType<typeof getNodeByExecutionId>)
|
||||
|
||||
const { user } = renderComponent({
|
||||
executionError: {
|
||||
lastExecutionError: {
|
||||
prompt_id: 'abc',
|
||||
node_id: '10',
|
||||
node_type: 'KSampler',
|
||||
exception_message: 'Out of memory',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Line 1', 'Line 2'],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordExecutionError({
|
||||
prompt_id: 'abc',
|
||||
node_id: '10',
|
||||
node_type: 'KSampler',
|
||||
executed: [],
|
||||
exception_message: 'Out of memory',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Line 1', 'Line 2'],
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getAllByText('KSampler').length).toBeGreaterThanOrEqual(1)
|
||||
@@ -300,19 +299,17 @@ describe('TabErrors.vue', () => {
|
||||
const { getNodeByExecutionId } = await import('@/utils/graphTraversalUtil')
|
||||
vi.mocked(getNodeByExecutionId).mockReturnValue(null)
|
||||
|
||||
const { user } = renderComponent({
|
||||
executionError: {
|
||||
lastNodeErrors: {
|
||||
'1': {
|
||||
class_type: 'CLIPTextEncode',
|
||||
errors: [{ message: 'Missing text input' }]
|
||||
},
|
||||
'2': {
|
||||
class_type: 'KSampler',
|
||||
errors: [{ message: 'Out of memory' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordNodeErrors({
|
||||
'1': nodeError(
|
||||
[validationError('unknown', undefined, {}, 'Missing text input', '')],
|
||||
'CLIPTextEncode'
|
||||
),
|
||||
'2': nodeError(
|
||||
[validationError('unknown', undefined, {}, 'Out of memory', '')],
|
||||
'KSampler'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getAllByText('CLIPTextEncode').length).toBeGreaterThanOrEqual(
|
||||
@@ -337,18 +334,17 @@ describe('TabErrors.vue', () => {
|
||||
const mockCopy = vi.fn()
|
||||
vi.mocked(useCopyToClipboard).mockReturnValue({ copyToClipboard: mockCopy })
|
||||
|
||||
const { user } = renderComponent({
|
||||
executionError: {
|
||||
lastExecutionError: {
|
||||
prompt_id: 'abc',
|
||||
node_id: '1',
|
||||
node_type: 'TestNode',
|
||||
exception_message: 'Test message',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Test details'],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordExecutionError({
|
||||
prompt_id: 'abc',
|
||||
node_id: '1',
|
||||
node_type: 'TestNode',
|
||||
executed: [],
|
||||
exception_message: 'Test message',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Test details'],
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('error-card-copy'))
|
||||
@@ -364,18 +360,17 @@ describe('TabErrors.vue', () => {
|
||||
title: 'KSampler'
|
||||
} as ReturnType<typeof getNodeByExecutionId>)
|
||||
|
||||
renderComponent({
|
||||
executionError: {
|
||||
lastExecutionError: {
|
||||
prompt_id: 'abc',
|
||||
node_id: '10',
|
||||
node_type: 'KSampler',
|
||||
exception_message: 'Out of memory',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Line 1', 'Line 2'],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordExecutionError({
|
||||
prompt_id: 'abc',
|
||||
node_id: '10',
|
||||
node_type: 'KSampler',
|
||||
executed: [],
|
||||
exception_message: 'Out of memory',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: ['Line 1', 'Line 2'],
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getAllByText('KSampler').length).toBeGreaterThanOrEqual(1)
|
||||
@@ -399,12 +394,9 @@ describe('TabErrors.vue', () => {
|
||||
isAssetSupported: true
|
||||
} satisfies MissingModelCandidate
|
||||
|
||||
const { user } = renderComponent({
|
||||
missingModel: {
|
||||
missingModelCandidates: [missingModel]
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useMissingModelStore(pinia).setMissingModels([missingModel])
|
||||
})
|
||||
const missingModelStore = useMissingModelStore()
|
||||
|
||||
expect(screen.getByText('Missing Models')).toBeInTheDocument()
|
||||
expect(
|
||||
@@ -413,33 +405,31 @@ describe('TabErrors.vue', () => {
|
||||
|
||||
await user.click(screen.getByTestId('missing-model-header-refresh'))
|
||||
|
||||
expect(missingModelStore.refreshMissingModels).toHaveBeenCalled()
|
||||
expect(mockRefreshMissingModels).toHaveBeenCalledWith({ silent: true })
|
||||
})
|
||||
|
||||
it('counts missing models per file when several share one directory', () => {
|
||||
renderComponent({
|
||||
missingModel: {
|
||||
missingModelCandidates: [
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-a.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
},
|
||||
{
|
||||
nodeId: '2',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-b.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
}
|
||||
] satisfies MissingModelCandidate[]
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useMissingModelStore(pinia).setMissingModels([
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-a.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
},
|
||||
{
|
||||
nodeId: '2',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-b.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -461,10 +451,8 @@ describe('TabErrors.vue', () => {
|
||||
isAssetSupported: true
|
||||
} satisfies MissingModelCandidate
|
||||
|
||||
renderComponent({
|
||||
missingModel: {
|
||||
missingModelCandidates: [missingModel]
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useMissingModelStore(pinia).setMissingModels([missingModel])
|
||||
})
|
||||
|
||||
expect(screen.getByText('Missing Models')).toBeInTheDocument()
|
||||
@@ -483,10 +471,8 @@ describe('TabErrors.vue', () => {
|
||||
isMissing: true
|
||||
} satisfies MissingMediaCandidate
|
||||
|
||||
renderComponent({
|
||||
missingMedia: {
|
||||
missingMediaCandidates: [missingMedia]
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useMissingMediaStore(pinia).setMissingMedia([missingMedia])
|
||||
})
|
||||
|
||||
expect(screen.getByText('Missing Inputs')).toBeInTheDocument()
|
||||
@@ -507,27 +493,25 @@ describe('TabErrors.vue', () => {
|
||||
} as ReturnType<typeof getNodeByExecutionId>
|
||||
})
|
||||
|
||||
const { user } = renderComponent({
|
||||
missingMedia: {
|
||||
missingMediaCandidates: [
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'shared.png',
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeType: 'PreviewImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'shared.png',
|
||||
isMissing: true
|
||||
}
|
||||
] satisfies MissingMediaCandidate[]
|
||||
}
|
||||
const { user } = renderComponent((pinia) => {
|
||||
useMissingMediaStore(pinia).setMissingMedia([
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'shared.png',
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeType: 'PreviewImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'shared.png',
|
||||
isMissing: true
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
expect(screen.getAllByTestId('missing-media-row')).toHaveLength(2)
|
||||
@@ -551,59 +535,58 @@ describe('TabErrors.vue', () => {
|
||||
title: 'Node'
|
||||
} as ReturnType<typeof getNodeByExecutionId>)
|
||||
|
||||
renderComponent({
|
||||
executionError: {
|
||||
lastNodeErrors: {
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: model',
|
||||
extra_info: { input_name: 'model' }
|
||||
},
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: positive',
|
||||
extra_info: { input_name: 'positive' }
|
||||
}
|
||||
]
|
||||
},
|
||||
'2': {
|
||||
class_type: 'CLIPTextEncode',
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'Input: clip',
|
||||
extra_info: { input_name: 'clip' }
|
||||
}
|
||||
]
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useExecutionErrorStore(pinia).recordNodeErrors({
|
||||
'1': nodeError(
|
||||
[
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'model',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: model'
|
||||
),
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'positive',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: positive'
|
||||
)
|
||||
],
|
||||
'KSampler'
|
||||
),
|
||||
'2': nodeError(
|
||||
[
|
||||
validationError(
|
||||
'required_input_missing',
|
||||
'clip',
|
||||
{},
|
||||
'Required input is missing',
|
||||
'Input: clip'
|
||||
)
|
||||
],
|
||||
'CLIPTextEncode'
|
||||
)
|
||||
})
|
||||
useMissingMediaStore(pinia).setMissingMedia([
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'a.png',
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'b.png',
|
||||
isMissing: true
|
||||
}
|
||||
},
|
||||
missingMedia: {
|
||||
missingMediaCandidates: [
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'a.png',
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
name: 'b.png',
|
||||
isMissing: true
|
||||
}
|
||||
]
|
||||
} satisfies { missingMediaCandidates: MissingMediaCandidate[] }
|
||||
])
|
||||
})
|
||||
|
||||
// 3 validation items + 2 missing media references
|
||||
@@ -626,13 +609,8 @@ describe('TabErrors.vue', () => {
|
||||
}
|
||||
} satisfies MissingNodeType
|
||||
|
||||
renderComponent({
|
||||
missingNodesError: {
|
||||
missingNodesError: {
|
||||
message: 'Missing Node Packs',
|
||||
nodeTypes: [swapNode]
|
||||
}
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useMissingNodesErrorStore(pinia).setMissingNodeTypes([swapNode])
|
||||
})
|
||||
|
||||
expect(screen.getByText('Swap Nodes')).toBeInTheDocument()
|
||||
@@ -660,10 +638,8 @@ describe('TabErrors.vue', () => {
|
||||
isAssetSupported: true
|
||||
} satisfies MissingModelCandidate
|
||||
|
||||
renderComponent({
|
||||
missingModel: {
|
||||
missingModelCandidates: [missingModel]
|
||||
}
|
||||
renderComponent((pinia) => {
|
||||
useMissingModelStore(pinia).setMissingModels([missingModel])
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('missing-model-header-refresh')).toBeVisible()
|
||||
|
||||
@@ -428,7 +428,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('uses fallback catalog grouping for unknown node validation errors', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -440,7 +440,7 @@ describe('useErrorGroups', () => {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroups = groups.allErrorGroups.value.filter(
|
||||
@@ -453,7 +453,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('resolves required_input_missing item display copy', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -468,7 +468,7 @@ describe('useErrorGroups', () => {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -509,7 +509,7 @@ describe('useErrorGroups', () => {
|
||||
vi.mocked(getNodeByExecutionId).mockImplementation((_, nodeId) => {
|
||||
return actualGetNodeByExecutionId(rootGraph, String(nodeId))
|
||||
})
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError(
|
||||
[
|
||||
validationError(
|
||||
@@ -521,7 +521,7 @@ describe('useErrorGroups', () => {
|
||||
],
|
||||
'InteriorClass'
|
||||
)
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -540,7 +540,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('groups node validation errors by catalog id across node types', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -569,7 +569,7 @@ describe('useErrorGroups', () => {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroups = groups.allErrorGroups.value.filter(
|
||||
@@ -590,7 +590,7 @@ describe('useErrorGroups', () => {
|
||||
it('uses general execution_failed display fields for unrecognized runtime execution errors', async () => {
|
||||
mockIsCloud.value = true
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastExecutionError = {
|
||||
store.recordExecutionError({
|
||||
prompt_id: 'test-prompt',
|
||||
timestamp: Date.now(),
|
||||
node_id: 5,
|
||||
@@ -601,7 +601,7 @@ describe('useErrorGroups', () => {
|
||||
traceback: ['line 1', 'line 2'],
|
||||
current_inputs: {},
|
||||
current_outputs: {}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroups = groups.allErrorGroups.value.filter(
|
||||
@@ -627,7 +627,7 @@ describe('useErrorGroups', () => {
|
||||
it('adds display fields for targeted runtime execution errors', async () => {
|
||||
mockIsCloud.value = true
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastExecutionError = {
|
||||
store.recordExecutionError({
|
||||
prompt_id: 'test-prompt',
|
||||
timestamp: Date.now(),
|
||||
node_id: 5,
|
||||
@@ -639,7 +639,7 @@ describe('useErrorGroups', () => {
|
||||
traceback: ['line 1', 'line 2'],
|
||||
current_inputs: {},
|
||||
current_outputs: {}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -660,11 +660,11 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('includes prompt error when present', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastPromptError = {
|
||||
store.recordPromptError({
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'No outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const promptGroup = groups.allErrorGroups.value.find(
|
||||
@@ -682,11 +682,11 @@ describe('useErrorGroups', () => {
|
||||
typeof canvasStore.selectedItems,
|
||||
unknown
|
||||
>([{ id: '1' }])
|
||||
store.lastPromptError = {
|
||||
store.recordPromptError({
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'No outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const promptGroup = groups.allErrorGroups.value.find(
|
||||
@@ -698,7 +698,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('sorts cards within an execution group by nodeId numerically', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'10': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -714,7 +714,7 @@ describe('useErrorGroups', () => {
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -726,7 +726,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('sorts cards with subpath nodeIds before higher root IDs', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'2': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -742,7 +742,7 @@ describe('useErrorGroups', () => {
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -754,7 +754,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('sorts deeply nested nodeIds by each segment numerically', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'10:11:99': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -770,7 +770,7 @@ describe('useErrorGroups', () => {
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'err', message: 'Error', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
@@ -784,13 +784,13 @@ describe('useErrorGroups', () => {
|
||||
describe('filteredGroups', () => {
|
||||
it('returns all groups when search query is empty', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'value_error', message: 'Bad value', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(groups.filteredGroups.value.length).toBeGreaterThan(0)
|
||||
@@ -798,7 +798,7 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('filters groups based on search query', async () => {
|
||||
const { store, groups, searchQuery } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -821,7 +821,7 @@ describe('useErrorGroups', () => {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
searchQuery.value = 'sampler'
|
||||
@@ -1097,11 +1097,11 @@ describe('useErrorGroups', () => {
|
||||
typeof canvasStore.selectedItems,
|
||||
unknown
|
||||
>([{ id: '1' }])
|
||||
store.lastPromptError = {
|
||||
store.recordPromptError({
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'No outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const promptGroup = groups.allErrorGroups.value.find(
|
||||
@@ -1116,13 +1116,13 @@ describe('useErrorGroups', () => {
|
||||
|
||||
it('reports no selection state when nothing is selected', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'value_error', message: 'Bad value', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(groups.hasSelection.value).toBe(false)
|
||||
@@ -1145,7 +1145,7 @@ describe('useErrorGroups', () => {
|
||||
typeof canvasStore.selectedItems,
|
||||
unknown
|
||||
>([selectedNode])
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -1158,7 +1158,7 @@ describe('useErrorGroups', () => {
|
||||
{ type: 'file_not_found', message: 'File not found', details: '' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(groups.hasSelection.value).toBe(true)
|
||||
@@ -1254,13 +1254,13 @@ describe('useErrorGroups', () => {
|
||||
typeof canvasStore.selectedItems,
|
||||
unknown
|
||||
>([selectedNode])
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'2:5': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [{ type: 'value_error', message: 'Bad value', details: '' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(groups.selectionErrorCount.value).toBe(1)
|
||||
@@ -1284,7 +1284,7 @@ describe('useErrorGroups', () => {
|
||||
typeof canvasStore.selectedItems,
|
||||
unknown
|
||||
>([containerNode])
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'2:5': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
@@ -1297,7 +1297,7 @@ describe('useErrorGroups', () => {
|
||||
{ type: 'file_not_found', message: 'File not found', details: '' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(groups.selectionErrorCount.value).toBe(1)
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(node.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -182,7 +182,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
node.onWidgetChanged!.call(node, 'steps', 50, 20, node.widgets![0])
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(node.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -214,7 +214,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
node.onWidgetChanged!.call(node, 'steps', 150, 20, node.widgets![0])
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(
|
||||
fromAny<LGraph, unknown>(undefined)
|
||||
)
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(node.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -245,7 +245,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
node.onWidgetChanged!.call(node, 'steps', 50, 20, node.widgets![0])
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
it('sets has_errors on nodes referenced in lastNodeErrors', async () => {
|
||||
const { nodeA, nodeB, store } = setupGraphWithStore()
|
||||
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(nodeA.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -527,7 +527,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'KSampler'
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(nodeA.has_errors).toBe(true)
|
||||
@@ -537,7 +537,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
it('sets slot hasErrors for inputs matching error input_name', async () => {
|
||||
const { nodeA, store } = setupGraphWithStore()
|
||||
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(nodeA.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -550,7 +550,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'KSampler'
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(nodeA.inputs[0].hasErrors).toBe(true)
|
||||
@@ -560,7 +560,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
it('clears has_errors and slot hasErrors when errors are removed', async () => {
|
||||
const { nodeA, store } = setupGraphWithStore()
|
||||
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[String(nodeA.id)]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -573,12 +573,12 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'KSampler'
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
expect(nodeA.has_errors).toBe(true)
|
||||
expect(nodeA.inputs[1].hasErrors).toBe(true)
|
||||
|
||||
store.lastNodeErrors = null
|
||||
store.recordNodeErrors(null)
|
||||
await nextTick()
|
||||
|
||||
expect(nodeA.has_errors).toBeFalsy()
|
||||
@@ -603,7 +603,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
|
||||
// Error on interior node: execution ID = "50:<interiorNodeId>"
|
||||
const interiorExecId = `${subgraphNode.id}:${interiorNode.id}`
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[interiorExecId]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -616,7 +616,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'InnerNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
// Interior node should have the error
|
||||
@@ -626,6 +626,56 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
expect(subgraphNode.has_errors).toBe(true)
|
||||
})
|
||||
|
||||
it('merges slot errors when execution IDs resolve to the same node', async () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const interiorNode = new LGraphNode('InnerNode')
|
||||
interiorNode.addInput('first', 'INT')
|
||||
interiorNode.addInput('second', 'INT')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const firstInstance = createTestSubgraphNode(subgraph, { id: 50 })
|
||||
const secondInstance = createTestSubgraphNode(subgraph, { id: 51 })
|
||||
const graph = firstInstance.graph as LGraph
|
||||
graph.add(firstInstance)
|
||||
graph.add(secondInstance)
|
||||
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
vi.spyOn(app, 'isGraphReady', 'get').mockReturnValue(true)
|
||||
|
||||
useGraphNodeManager(graph)
|
||||
const store = useExecutionErrorStore()
|
||||
store.recordNodeErrors({
|
||||
[`${firstInstance.id}:${interiorNode.id}`]: {
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Missing first',
|
||||
details: '',
|
||||
extra_info: { input_name: 'first' }
|
||||
}
|
||||
],
|
||||
dependent_outputs: [],
|
||||
class_type: 'InnerNode'
|
||||
},
|
||||
[`${secondInstance.id}:${interiorNode.id}`]: {
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Missing second',
|
||||
details: '',
|
||||
extra_info: { input_name: 'second' }
|
||||
}
|
||||
],
|
||||
dependent_outputs: [],
|
||||
class_type: 'InnerNode'
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
expect(interiorNode.inputs[0].hasErrors).toBe(true)
|
||||
expect(interiorNode.inputs[1].hasErrors).toBe(true)
|
||||
})
|
||||
|
||||
it('sets has_errors on nodes with missing models', async () => {
|
||||
const { nodeA, nodeB } = setupGraphWithStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import { getParentExecutionIds } from '@/types/nodeIdentification'
|
||||
import { hasErrorForSlot } from '@/utils/executionErrorUtil'
|
||||
import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
function setNodeHasErrors(node: LGraphNode, hasErrors: boolean): void {
|
||||
@@ -39,7 +40,7 @@ function reconcileNodeErrorFlags(
|
||||
// Collect nodes and slot info that should be flagged
|
||||
// Includes both error-owning nodes and their ancestor containers
|
||||
const flaggedNodes = new Set<LGraphNode>()
|
||||
const errorSlots = new Map<LGraphNode, Set<string>>()
|
||||
const errorsByNode = new Map<LGraphNode, NodeError['errors']>()
|
||||
|
||||
if (nodeErrors) {
|
||||
for (const [executionId, nodeError] of Object.entries(nodeErrors)) {
|
||||
@@ -47,12 +48,10 @@ function reconcileNodeErrorFlags(
|
||||
if (!node) continue
|
||||
|
||||
flaggedNodes.add(node)
|
||||
const slotNames = new Set<string>()
|
||||
for (const error of nodeError.errors) {
|
||||
const name = error.extra_info?.input_name
|
||||
if (name) slotNames.add(name)
|
||||
}
|
||||
if (slotNames.size > 0) errorSlots.set(node, slotNames)
|
||||
errorsByNode.set(node, [
|
||||
...(errorsByNode.get(node) ?? []),
|
||||
...nodeError.errors
|
||||
])
|
||||
|
||||
for (const parentId of getParentExecutionIds(executionId)) {
|
||||
const parentNode = getNodeByExecutionId(rootGraph, parentId)
|
||||
@@ -75,9 +74,10 @@ function reconcileNodeErrorFlags(
|
||||
setNodeHasErrors(node, flaggedNodes.has(node))
|
||||
|
||||
if (node.inputs) {
|
||||
const nodeSlotNames = errorSlots.get(node)
|
||||
const ownErrors = errorsByNode.get(node)
|
||||
for (const slot of node.inputs) {
|
||||
slot.hasErrors = !!nodeSlotNames?.has(slot.name)
|
||||
slot.hasErrors =
|
||||
!!slot.name && !!ownErrors && hasErrorForSlot(ownErrors, slot.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -94,7 +94,7 @@ function renderControls({
|
||||
|
||||
useAppModeStore().selectedOutputs = [toNodeId(1)]
|
||||
if (hasError) {
|
||||
useExecutionErrorStore().lastNodeErrors = nodeErrors
|
||||
useExecutionErrorStore().recordNodeErrors(nodeErrors)
|
||||
}
|
||||
|
||||
const toastTarget = document.createElement('div')
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import { validationError } from '@/utils/__tests__/nodeErrorHelpers'
|
||||
|
||||
const GRAPH_ID = 'graph-test'
|
||||
|
||||
@@ -156,7 +157,7 @@ describe('hasWidgetError', () => {
|
||||
it('returns true when node has matching input error', () => {
|
||||
const widget = createMockWidget({ name: 'seed' })
|
||||
const nodeErrors = {
|
||||
errors: [{ extra_info: { input_name: 'seed' } }]
|
||||
errors: [validationError('required_input_missing', 'seed')]
|
||||
}
|
||||
expect(
|
||||
hasWidgetError(
|
||||
@@ -174,7 +175,7 @@ describe('hasWidgetError', () => {
|
||||
name: 'seed',
|
||||
sourceExecutionId: createNodeExecutionId([toNodeId(65), toNodeId(18)])
|
||||
})
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'65:18': {
|
||||
errors: [
|
||||
{
|
||||
@@ -187,7 +188,7 @@ describe('hasWidgetError', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(
|
||||
hasWidgetError(
|
||||
widget,
|
||||
@@ -219,7 +220,7 @@ describe('hasWidgetError', () => {
|
||||
sourceWidgetName: 'internal_name'
|
||||
})
|
||||
const nodeErrors = {
|
||||
errors: [{ extra_info: { input_name: 'display_slot' } }]
|
||||
errors: [validationError('required_input_missing', 'display_slot')]
|
||||
}
|
||||
expect(
|
||||
hasWidgetError(
|
||||
@@ -263,7 +264,7 @@ describe('hasWidgetError', () => {
|
||||
sourceExecutionId,
|
||||
sourceWidgetName: 'ckpt_name'
|
||||
})
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
[sourceExecutionId]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -276,7 +277,7 @@ describe('hasWidgetError', () => {
|
||||
class_type: 'CheckpointLoaderSimple',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(
|
||||
hasWidgetError(
|
||||
widget,
|
||||
@@ -711,7 +712,7 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
sourceWidgetName: 'ckpt_name'
|
||||
})
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
[sourceExecutionId]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -724,7 +725,7 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
class_type: 'CheckpointLoaderSimple',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const [processed] = processWidgets([widget])
|
||||
processed.updateHandler('real_model.safetensors')
|
||||
@@ -741,7 +742,7 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
executionErrorStore.recordNodeErrors({
|
||||
[NODE_ID]: {
|
||||
errors: [
|
||||
{
|
||||
@@ -754,7 +755,7 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const [processed] = processWidgets([widget])
|
||||
|
||||
@@ -762,7 +763,7 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
hasWidgetError(
|
||||
widget,
|
||||
createNodeExecutionId([NODE_ID]),
|
||||
executionErrorStore.lastNodeErrors[NODE_ID],
|
||||
executionErrorStore.lastNodeErrors?.[NODE_ID],
|
||||
executionErrorStore,
|
||||
missingModelStore
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||
@@ -39,6 +40,7 @@ import type { NodeId } from '@/types/nodeId'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import type { WidgetState } from '@/types/widgetState'
|
||||
import { hasErrorForSlot } from '@/utils/executionErrorUtil'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LinkedUpstreamInfo,
|
||||
@@ -121,9 +123,7 @@ function createWidgetUpdateHandler(
|
||||
export function hasWidgetError(
|
||||
widget: SafeWidgetData,
|
||||
nodeExecId: NodeExecutionId,
|
||||
nodeErrors:
|
||||
| { errors: { extra_info?: { input_name?: string } }[] }
|
||||
| undefined,
|
||||
nodeErrors: Pick<NodeError, 'errors'> | undefined,
|
||||
executionErrorStore: ReturnType<typeof useExecutionErrorStore>,
|
||||
missingModelStore: ReturnType<typeof useMissingModelStore>
|
||||
): boolean {
|
||||
@@ -135,7 +135,7 @@ export function hasWidgetError(
|
||||
? (widget.sourceWidgetName ?? widget.name)
|
||||
: widget.name
|
||||
return (
|
||||
!!errors?.some((e) => e.extra_info?.input_name === errorInputName) ||
|
||||
(!!errors && hasErrorForSlot(errors, errorInputName)) ||
|
||||
missingModelStore.isWidgetMissingModel(nodeExecId, widget.name)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
} from '@/composables/usePaste'
|
||||
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { PromptExecutionError, api } from '@/scripts/api'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
@@ -189,6 +190,21 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
|
||||
describe('queuePrompt', () => {
|
||||
function prepareEmptyPromptQueue() {
|
||||
const workflow = new ComfyWorkflow({
|
||||
path: 'workflows/review.json',
|
||||
modified: 0,
|
||||
size: 0
|
||||
})
|
||||
Reflect.set(app, 'rootGraphInternal', new LGraph())
|
||||
mockWorkspaceWorkflow.activeWorkflow = workflow
|
||||
vi.spyOn(app, 'graphToPrompt').mockResolvedValue({
|
||||
output: {},
|
||||
workflow: createWorkflowGraphData()
|
||||
})
|
||||
vi.spyOn(api, 'dispatchCustomEvent').mockImplementation(() => true)
|
||||
}
|
||||
|
||||
it('shows the error overlay for successful prompt responses with node errors', async () => {
|
||||
const graph = new LGraph()
|
||||
const workflow = new ComfyWorkflow({
|
||||
@@ -242,6 +258,77 @@ describe('ComfyApp', () => {
|
||||
)
|
||||
expect(mockCanvas.draw).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('preserves a failed result when prompt errors include an empty node error record', async () => {
|
||||
prepareEmptyPromptQueue()
|
||||
vi.spyOn(api, 'queuePrompt').mockRejectedValue(
|
||||
new PromptExecutionError({
|
||||
node_errors: {},
|
||||
error: {
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'Prompt has no outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(app.queuePrompt(0)).resolves.toBe(false)
|
||||
|
||||
const errorStore = useExecutionErrorStore()
|
||||
expect(errorStore.lastNodeErrors).toBeNull()
|
||||
expect(errorStore.lastPromptError).toMatchObject({
|
||||
type: 'prompt_no_outputs'
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves a successful result when prompt errors omit node errors', async () => {
|
||||
prepareEmptyPromptQueue()
|
||||
vi.spyOn(api, 'queuePrompt').mockRejectedValue(
|
||||
new PromptExecutionError({
|
||||
error: {
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'Prompt has no outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(app.queuePrompt(0)).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it('uses the last processed queue item result after an earlier failure', async () => {
|
||||
prepareEmptyPromptQueue()
|
||||
let rejectFirst!: (reason?: unknown) => void
|
||||
const firstResponse = new Promise<never>((_, reject) => {
|
||||
rejectFirst = reject
|
||||
})
|
||||
vi.spyOn(api, 'queuePrompt')
|
||||
.mockImplementationOnce(() => firstResponse)
|
||||
.mockResolvedValueOnce({
|
||||
prompt_id: 'job-2',
|
||||
error: ''
|
||||
})
|
||||
|
||||
const firstQueue = app.queuePrompt(0)
|
||||
await vi.waitFor(() => {
|
||||
expect(api.queuePrompt).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
await expect(app.queuePrompt(0)).resolves.toBe(false)
|
||||
|
||||
rejectFirst(
|
||||
new PromptExecutionError({
|
||||
node_errors: {},
|
||||
error: {
|
||||
type: 'prompt_no_outputs',
|
||||
message: 'Prompt has no outputs',
|
||||
details: ''
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(firstQueue).resolves.toBe(true)
|
||||
expect(useExecutionErrorStore().lastNodeErrors).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('refreshComboInNodes', () => {
|
||||
|
||||
@@ -95,6 +95,7 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
||||
import type { ExtensionManager } from '@/types/extensionTypes'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { normalizePromptError } from '@/utils/executionErrorUtil'
|
||||
import { graphToPrompt } from '@/utils/executionUtil'
|
||||
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
|
||||
import { getCnrIdFromProperties } from '@/platform/nodeReplacement/cnrIdUtil'
|
||||
@@ -1631,6 +1632,7 @@ export class ComfyApp {
|
||||
const executionStore = useExecutionStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.clearAllErrors()
|
||||
let queueResultOverride: boolean | null = null
|
||||
|
||||
// Get auth token for backend nodes - uses workspace token if enabled, otherwise Firebase token
|
||||
const comfyOrgAuthToken = await useAuthStore().getAuthToken()
|
||||
@@ -1673,12 +1675,8 @@ export class ComfyApp {
|
||||
})
|
||||
delete api.authToken
|
||||
delete api.apiKey
|
||||
const nodeErrors = res.node_errors
|
||||
const hasNodeErrors =
|
||||
nodeErrors && Object.keys(nodeErrors).length > 0
|
||||
executionErrorStore.lastNodeErrors = hasNodeErrors
|
||||
? nodeErrors
|
||||
: null
|
||||
executionErrorStore.recordNodeErrors(res.node_errors ?? null)
|
||||
queueResultOverride = null
|
||||
try {
|
||||
if (res.prompt_id) {
|
||||
executionStore.storeJob({
|
||||
@@ -1694,7 +1692,7 @@ export class ComfyApp {
|
||||
error
|
||||
})
|
||||
}
|
||||
if (hasNodeErrors) {
|
||||
if (executionErrorStore.hasNodeError) {
|
||||
if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||
executionErrorStore.showErrorOverlay()
|
||||
}
|
||||
@@ -1766,30 +1764,18 @@ export class ComfyApp {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof PromptExecutionError) {
|
||||
executionErrorStore.lastNodeErrors =
|
||||
error.response.node_errors ?? null
|
||||
// Keep the legacy result before empty node errors are normalized.
|
||||
const nodeErrors = error.response.node_errors
|
||||
queueResultOverride = !nodeErrors
|
||||
executionErrorStore.recordNodeErrors(nodeErrors ?? null)
|
||||
|
||||
// Store prompt-level error separately only when no node-specific errors exist,
|
||||
// because node errors already carry the full context. Prompt-level errors
|
||||
// (e.g. prompt_no_outputs, no_prompt) lack node IDs and need their own path.
|
||||
const nodeErrors = error.response.node_errors
|
||||
const hasNodeErrors =
|
||||
nodeErrors && Object.keys(nodeErrors).length > 0
|
||||
|
||||
if (!hasNodeErrors) {
|
||||
const respError = error.response.error
|
||||
if (respError && typeof respError === 'object') {
|
||||
executionErrorStore.lastPromptError = {
|
||||
type: respError.type,
|
||||
message: respError.message,
|
||||
details: respError.details ?? ''
|
||||
}
|
||||
} else if (typeof respError === 'string') {
|
||||
executionErrorStore.lastPromptError = {
|
||||
type: 'error',
|
||||
message: respError,
|
||||
details: ''
|
||||
}
|
||||
if (!executionErrorStore.hasNodeError) {
|
||||
const promptError = normalizePromptError(error.response.error)
|
||||
if (promptError) {
|
||||
executionErrorStore.recordPromptError(promptError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1828,7 +1814,7 @@ export class ComfyApp {
|
||||
} finally {
|
||||
this.processingQueue = false
|
||||
}
|
||||
return !executionErrorStore.lastNodeErrors
|
||||
return queueResultOverride ?? !executionErrorStore.lastNodeErrors
|
||||
}
|
||||
|
||||
showErrorOnFileLoad(file: File) {
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
describe('clearSimpleNodeErrors', () => {
|
||||
it('does nothing if lastNodeErrors is null', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = null
|
||||
store.recordNodeErrors(null)
|
||||
// Should not error
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -79,7 +79,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('clears entirely if there are only simple errors for the same slot', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -92,7 +92,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -105,7 +105,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('clears only the specific slot errors, leaving other errors alone', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -124,7 +124,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -141,7 +141,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('does nothing if executionId is not found in lastNodeErrors', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -154,7 +154,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(999)]),
|
||||
@@ -167,7 +167,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('preserves complex errors when slot has both simple and complex errors', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -186,7 +186,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -199,7 +199,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('clears one node while preserving another in multi-node errors', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -224,7 +224,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'LoadModel'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -238,7 +238,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('clears entire node when no slotName and all errors are simple', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -257,7 +257,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(createNodeExecutionId([toNodeId(123)]))
|
||||
|
||||
@@ -266,7 +266,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('does not clear when no slotName and some errors are not simple', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -285,7 +285,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(createNodeExecutionId([toNodeId(123)]))
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('does not clear if the error is not simple', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -307,7 +307,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearSimpleNodeErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -323,11 +323,11 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError([
|
||||
validationError('required_input_missing', 'seed_input')
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.surfacedNodeErrors).toHaveProperty('12')
|
||||
|
||||
@@ -342,7 +342,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError([
|
||||
validationError(
|
||||
'custom_validation_failed',
|
||||
@@ -351,7 +351,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
'Custom validation failed'
|
||||
)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.surfacedNodeErrors).toHaveProperty('12')
|
||||
|
||||
@@ -389,11 +389,11 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'1:2:3': nodeError([
|
||||
validationError('required_input_missing', 'seed_input')
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.surfacedNodeErrors).toHaveProperty('1')
|
||||
|
||||
@@ -413,7 +413,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
describe('clearWidgetRelatedErrors', () => {
|
||||
it('clears error if value is valid (isValueStillOutOfRange is false)', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -426,7 +426,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Valid value (5 < 10)
|
||||
store.clearWidgetRelatedErrors(
|
||||
@@ -444,7 +444,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('optimistically clears value_not_in_list error for string combo values', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -457,7 +457,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'KSampler'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.clearWidgetRelatedErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -471,7 +471,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('does not clear error if value is still out of range', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -484,7 +484,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'TestNode'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Invalid value (15 > 10)
|
||||
store.clearWidgetRelatedErrors(
|
||||
@@ -503,13 +503,13 @@ describe('executionErrorStore — node error operations', () => {
|
||||
|
||||
it('validates the base target against live widget bounds, not recorded ones', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': nodeError([
|
||||
validationError('value_bigger_than_max', 'testWidget', {
|
||||
input_config: ['INT', { max: 100 }]
|
||||
})
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
store.clearWidgetRelatedErrors(
|
||||
createNodeExecutionId([toNodeId(123)]),
|
||||
@@ -530,11 +530,11 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError([
|
||||
validationError('value_bigger_than_max', 'seed_input', {}, 'Too high')
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.surfacedNodeErrors).toHaveProperty('12')
|
||||
|
||||
@@ -570,7 +570,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError([
|
||||
validationError('value_bigger_than_max', 'seed_input', {
|
||||
input_config: ['INT', { max: 100 }]
|
||||
@@ -581,7 +581,7 @@ describe('executionErrorStore — node error operations', () => {
|
||||
input_config: ['INT', { max: 50 }]
|
||||
})
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
expect(store.surfacedNodeErrors?.['12'].errors).toHaveLength(2)
|
||||
|
||||
@@ -610,11 +610,11 @@ describe('executionErrorStore — node error operations', () => {
|
||||
mockGraphReady(rootGraph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'12:5': nodeError([
|
||||
validationError('required_input_missing', 'seed_input')
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
const hostLocatorId = createNodeLocatorId(null, toNodeId(12))
|
||||
|
||||
@@ -770,6 +770,28 @@ describe('surfaceMissingMedia — silent option', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('recordNodeErrors', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('normalizes an empty error record to null', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
|
||||
store.recordNodeErrors({})
|
||||
|
||||
expect(store.lastNodeErrors).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps a null error record as null', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
|
||||
store.recordNodeErrors(null)
|
||||
|
||||
expect(store.lastNodeErrors).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearAllErrors', () => {
|
||||
let executionErrorStore: ReturnType<typeof useExecutionErrorStore>
|
||||
let missingNodesStore: ReturnType<typeof useMissingNodesErrorStore>
|
||||
@@ -782,7 +804,7 @@ describe('clearAllErrors', () => {
|
||||
})
|
||||
|
||||
it('resets all error categories and closes error overlay', () => {
|
||||
executionErrorStore.lastExecutionError = {
|
||||
executionErrorStore.recordExecutionError({
|
||||
prompt_id: 'test',
|
||||
timestamp: 0,
|
||||
node_id: '1',
|
||||
@@ -791,13 +813,13 @@ describe('clearAllErrors', () => {
|
||||
exception_message: 'fail',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: []
|
||||
}
|
||||
executionErrorStore.lastPromptError = {
|
||||
})
|
||||
executionErrorStore.recordPromptError({
|
||||
type: 'execution',
|
||||
message: 'fail',
|
||||
details: ''
|
||||
}
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
})
|
||||
executionErrorStore.recordNodeErrors({
|
||||
'1': {
|
||||
errors: [
|
||||
{
|
||||
@@ -810,7 +832,7 @@ describe('clearAllErrors', () => {
|
||||
dependent_outputs: [],
|
||||
class_type: 'Test'
|
||||
}
|
||||
}
|
||||
})
|
||||
missingNodesStore.setMissingNodeTypes(
|
||||
fromAny<MissingNodeType[], unknown>([{ type: 'MissingNode', hint: '' }])
|
||||
)
|
||||
|
||||
@@ -33,7 +33,9 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
SIMPLE_ERROR_TYPES,
|
||||
errorsForSlot,
|
||||
getInputConfigBounds,
|
||||
hasErrorForSlot,
|
||||
isValueStillOutOfRange
|
||||
} from '@/utils/executionErrorUtil'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
@@ -59,6 +61,20 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
|
||||
const isErrorOverlayOpen = ref(false)
|
||||
|
||||
/** Replaces the full record; empty or null means the run produced no errors. */
|
||||
function recordNodeErrors(nodeErrors: Record<string, NodeError> | null) {
|
||||
lastNodeErrors.value =
|
||||
nodeErrors && Object.keys(nodeErrors).length > 0 ? nodeErrors : null
|
||||
}
|
||||
|
||||
function recordExecutionError(detail: ExecutionErrorWsMessage) {
|
||||
lastExecutionError.value = detail
|
||||
}
|
||||
|
||||
function recordPromptError(promptError: PromptError) {
|
||||
lastPromptError.value = promptError
|
||||
}
|
||||
|
||||
function showErrorOverlay() {
|
||||
isErrorOverlayOpen.value = true
|
||||
}
|
||||
@@ -82,10 +98,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
function clearExecutionStartErrors() {
|
||||
lastExecutionError.value = null
|
||||
lastPromptError.value = null
|
||||
if (
|
||||
!lastNodeErrors.value ||
|
||||
Object.keys(lastNodeErrors.value).length === 0
|
||||
) {
|
||||
if (!lastNodeErrors.value) {
|
||||
isErrorOverlayOpen.value = false
|
||||
}
|
||||
}
|
||||
@@ -105,7 +118,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
|
||||
const isSlotScoped = slotName !== undefined
|
||||
const relevantErrors = isSlotScoped
|
||||
? nodeError.errors.filter((e) => e.extra_info?.input_name === slotName)
|
||||
? errorsForSlot(nodeError.errors, slotName)
|
||||
: nodeError.errors
|
||||
|
||||
if (relevantErrors.length === 0) return null
|
||||
@@ -117,7 +130,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
|
||||
if (isSlotScoped) {
|
||||
const remainingErrors = nodeError.errors.filter(
|
||||
(e) => e.extra_info?.input_name !== slotName
|
||||
(error) => !relevantErrors.includes(error)
|
||||
)
|
||||
if (remainingErrors.length === 0) {
|
||||
delete updated[executionId]
|
||||
@@ -218,9 +231,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
const nodeError = nodeErrors[target.executionId]
|
||||
if (!nodeError) return false
|
||||
|
||||
const errors = nodeError.errors.filter(
|
||||
(error) => error.extra_info?.input_name === target.slotName
|
||||
)
|
||||
const errors = errorsForSlot(nodeError.errors, target.slotName)
|
||||
const options = target.useRecordedBounds
|
||||
? getTargetRangeOptions(errors, callerOptions)
|
||||
: callerOptions
|
||||
@@ -374,9 +385,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
|
||||
const hasPromptError = computed(() => !!lastPromptError.value)
|
||||
|
||||
const hasNodeError = computed(
|
||||
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
|
||||
)
|
||||
const hasNodeError = computed(() => lastNodeErrors.value !== null)
|
||||
|
||||
// Re-lifts only when the record changes; topology is assumed stable while errors are displayed.
|
||||
const surfacedNodeErrors = computed(() =>
|
||||
@@ -495,7 +504,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
const nodeError = getNodeErrors(nodeLocatorId)
|
||||
if (!nodeError) return false
|
||||
|
||||
return nodeError.errors.some((e) => e.extra_info?.input_name === slotName)
|
||||
return hasErrorForSlot(nodeError.errors, slotName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,10 +534,15 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
useNodeErrorFlagSync(surfacedNodeErrors, missingModelStore, missingMediaStore)
|
||||
|
||||
return {
|
||||
// Raw state
|
||||
lastNodeErrors,
|
||||
lastExecutionError,
|
||||
lastPromptError,
|
||||
// Read-only state
|
||||
lastNodeErrors: computed(() => lastNodeErrors.value),
|
||||
lastExecutionError: computed(() => lastExecutionError.value),
|
||||
lastPromptError: computed(() => lastPromptError.value),
|
||||
|
||||
// Recording
|
||||
recordNodeErrors,
|
||||
recordExecutionError,
|
||||
recordPromptError,
|
||||
|
||||
// Clearing
|
||||
clearAllErrors,
|
||||
|
||||
@@ -935,7 +935,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
|
||||
it('should return node error by locator ID for root graph node', () => {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -948,7 +948,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = store.getNodeErrors(
|
||||
createNodeLocatorId(null, toNodeId(123))
|
||||
@@ -974,7 +974,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
|
||||
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123:456': {
|
||||
errors: [
|
||||
{
|
||||
@@ -987,7 +987,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'SubgraphNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const locatorId = createNodeLocatorId(subgraphUuid, toNodeId(456))
|
||||
const result = store.getNodeErrors(locatorId)
|
||||
@@ -1006,7 +1006,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
|
||||
it('should return false when node has errors but slot is not mentioned', () => {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -1019,7 +1019,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = store.slotHasError(
|
||||
createNodeLocatorId(null, toNodeId(123)),
|
||||
@@ -1029,7 +1029,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
|
||||
it('should return true when slot has error', () => {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -1042,7 +1042,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = store.slotHasError(
|
||||
createNodeLocatorId(null, toNodeId(123)),
|
||||
@@ -1052,7 +1052,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
|
||||
it('should return true when multiple errors exist for the same slot', () => {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -1071,7 +1071,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = store.slotHasError(
|
||||
createNodeLocatorId(null, toNodeId(123)),
|
||||
@@ -1081,7 +1081,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
|
||||
it('should handle errors without extra_info', () => {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
'123': {
|
||||
errors: [
|
||||
{
|
||||
@@ -1093,7 +1093,7 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
class_type: 'TestNode',
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = store.slotHasError(
|
||||
createNodeLocatorId(null, toNodeId(123)),
|
||||
@@ -1339,7 +1339,7 @@ describe('useExecutionStore - WebSocket event handlers', () => {
|
||||
]
|
||||
}
|
||||
}
|
||||
errorStore.lastExecutionError = {
|
||||
errorStore.recordExecutionError({
|
||||
prompt_id: 'old-job',
|
||||
timestamp: 0,
|
||||
node_id: '1',
|
||||
@@ -1348,13 +1348,13 @@ describe('useExecutionStore - WebSocket event handlers', () => {
|
||||
exception_message: 'boom',
|
||||
exception_type: 'RuntimeError',
|
||||
traceback: []
|
||||
}
|
||||
errorStore.lastPromptError = {
|
||||
})
|
||||
errorStore.recordPromptError({
|
||||
type: 'old-error',
|
||||
message: 'old prompt error',
|
||||
details: ''
|
||||
}
|
||||
errorStore.lastNodeErrors = nodeErrors
|
||||
})
|
||||
errorStore.recordNodeErrors(nodeErrors)
|
||||
errorStore.showErrorOverlay()
|
||||
|
||||
fire('execution_start', { prompt_id: 'job-1', timestamp: 0 })
|
||||
|
||||
@@ -554,7 +554,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
|
||||
setWorkflowStatus(e.detail.prompt_id, 'failed')
|
||||
executionErrorStore.lastExecutionError = e.detail
|
||||
executionErrorStore.recordExecutionError(e.detail)
|
||||
clearInitializationByJobId(e.detail.prompt_id)
|
||||
resetExecutionState(e.detail.prompt_id)
|
||||
}
|
||||
@@ -580,13 +580,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
clearInitializationByJobId(detail.prompt_id)
|
||||
resetExecutionState(detail.prompt_id)
|
||||
executionErrorStore.lastPromptError = {
|
||||
executionErrorStore.recordPromptError({
|
||||
type: detail.exception_type ?? 'error',
|
||||
message: detail.exception_type
|
||||
? `${detail.exception_type}: ${detail.exception_message}`
|
||||
: (detail.exception_message ?? ''),
|
||||
details: detail.traceback?.join('\n') ?? ''
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -600,9 +600,9 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
resetExecutionState(detail.prompt_id)
|
||||
|
||||
if (result.kind === 'nodeErrors') {
|
||||
executionErrorStore.lastNodeErrors = result.nodeErrors
|
||||
executionErrorStore.recordNodeErrors(result.nodeErrors)
|
||||
} else {
|
||||
executionErrorStore.lastPromptError = result.promptError
|
||||
executionErrorStore.recordPromptError(result.promptError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
useExecutionErrorStore().lastNodeErrors = errors
|
||||
useExecutionErrorStore().recordNodeErrors(errors)
|
||||
useCanvasStore().getCanvas().draw(true, true)
|
||||
throw new Error(
|
||||
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
|
||||
|
||||
@@ -9,10 +9,10 @@ export function seedRequiredInputMissingNodeError(
|
||||
executionId: NodeExecutionId,
|
||||
inputName: string
|
||||
): void {
|
||||
store.lastNodeErrors = {
|
||||
store.recordNodeErrors({
|
||||
[executionId]: nodeError(
|
||||
[validationError('required_input_missing', inputName, {}, 'Missing', '')],
|
||||
'TestNode'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { NodeError, PromptError } from '@/schemas/apiSchema'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
|
||||
type RawPromptError =
|
||||
| string
|
||||
| { type?: string; message?: string; details?: string }
|
||||
|
||||
/**
|
||||
* The standard prompt validation response shape (`{ error, node_errors }`).
|
||||
* In cloud, this is embedded as JSON inside `execution_error.exception_message`
|
||||
@@ -8,7 +12,7 @@ import type { SerializedNodeId } from '@/types/nodeId'
|
||||
* rather than as direct HTTP responses.
|
||||
*/
|
||||
interface CloudValidationError {
|
||||
error?: { type?: string; message?: string; details?: string } | string
|
||||
error?: RawPromptError
|
||||
node_errors?: Record<SerializedNodeId, NodeError>
|
||||
}
|
||||
|
||||
@@ -51,6 +55,22 @@ type CloudValidationResult =
|
||||
| { kind: 'nodeErrors'; nodeErrors: Record<SerializedNodeId, NodeError> }
|
||||
| { kind: 'promptError'; promptError: PromptError }
|
||||
|
||||
export function normalizePromptError(
|
||||
error: RawPromptError | undefined
|
||||
): PromptError | null {
|
||||
if (error && typeof error === 'object') {
|
||||
return {
|
||||
type: error.type ?? 'error',
|
||||
message: error.message ?? '',
|
||||
details: error.details ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
return typeof error === 'string'
|
||||
? { type: 'error', message: error, details: '' }
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies an embedded cloud validation error from `exception_message`
|
||||
* as either node-level errors or a prompt-level error.
|
||||
@@ -70,25 +90,22 @@ export function classifyCloudValidationError(
|
||||
return { kind: 'nodeErrors', nodeErrors: node_errors }
|
||||
}
|
||||
|
||||
if (error && typeof error === 'object') {
|
||||
return {
|
||||
kind: 'promptError',
|
||||
promptError: {
|
||||
type: error.type ?? 'error',
|
||||
message: error.message ?? '',
|
||||
details: error.details ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
const promptError = normalizePromptError(error)
|
||||
return promptError ? { kind: 'promptError', promptError } : null
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return {
|
||||
kind: 'promptError',
|
||||
promptError: { type: 'error', message: error, details: '' }
|
||||
}
|
||||
}
|
||||
export function errorsForSlot(
|
||||
errors: NodeError['errors'],
|
||||
slotName: string
|
||||
): NodeError['errors'] {
|
||||
return errors.filter((error) => error.extra_info?.input_name === slotName)
|
||||
}
|
||||
|
||||
return null
|
||||
export function hasErrorForSlot(
|
||||
errors: NodeError['errors'],
|
||||
slotName: string
|
||||
): boolean {
|
||||
return errors.some((error) => error.extra_info?.input_name === slotName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user