From 2524846f5c29c32eafe559c399efafb3234477db Mon Sep 17 00:00:00 2001 From: Dante Date: Tue, 14 Apr 2026 08:47:14 +0900 Subject: [PATCH] fix: guard progress_text before canvas init (#11174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Prevent early `progress_text` websocket events from throwing before the graph canvas is initialized. ## Changes - **What**: Guard `handleProgressText()` until `canvasStore.canvas` exists, and add a regression test for a startup-time `progress_text` event arriving before `GraphCanvas` finishes initialization. ## Review Focus Confirm this is the right guard point for the startup race between `GraphView` websocket binding and `GraphCanvas` async setup, and that progress text behavior is unchanged once the canvas is ready. ## Validation - `pnpm exec eslint src/stores/executionStore.ts src/stores/executionStore.test.ts` - `pnpm exec vitest run src/stores/executionStore.test.ts -t "should ignore progress_text before the canvas is initialized"` - `pnpm test:unit -- --run src/stores/executionStore.test.ts` still reports one unrelated isolated-file failure in `nodeLocatorIdToExecutionId` on current `main` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11174-fix-guard-progress_text-before-canvas-init-3406d73d3650813dad23d511fb51add5) by [Unito](https://www.unito.io) --- src/stores/executionStore.test.ts | 67 +++++++++++++++++++++++++++++-- src/stores/executionStore.ts | 2 +- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/stores/executionStore.test.ts b/src/stores/executionStore.test.ts index cf531463d4..535667cf9c 100644 --- a/src/stores/executionStore.test.ts +++ b/src/stores/executionStore.test.ts @@ -7,12 +7,21 @@ import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNod import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil' // Create mock functions that will be shared -const mockNodeExecutionIdToNodeLocatorId = vi.fn() -const mockNodeIdToNodeLocatorId = vi.fn() -const mockNodeLocatorIdToNodeExecutionId = vi.fn() +const { + mockNodeExecutionIdToNodeLocatorId, + mockNodeIdToNodeLocatorId, + mockNodeLocatorIdToNodeExecutionId, + mockShowTextPreview +} = vi.hoisted(() => ({ + mockNodeExecutionIdToNodeLocatorId: vi.fn(), + mockNodeIdToNodeLocatorId: vi.fn(), + mockNodeLocatorIdToNodeExecutionId: vi.fn(), + mockShowTextPreview: vi.fn() +})) import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore' import type { NodeProgressState } from '@/schemas/apiSchema' +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils' import { createTestingPinia } from '@pinia/testing' @@ -38,7 +47,7 @@ declare global { vi.mock('@/composables/node/useNodeProgressText', () => ({ useNodeProgressText: () => ({ - showTextPreview: vi.fn() + showTextPreview: mockShowTextPreview }) })) @@ -431,6 +440,56 @@ describe('useExecutionStore - reconcileInitializingJobs', () => { }) }) +describe('useExecutionStore - progress_text startup guard', () => { + let store: ReturnType + + function fireProgressText(detail: { + nodeId: string + text: string + prompt_id?: string + }) { + const handler = apiEventHandlers.get('progress_text') + if (!handler) throw new Error('progress_text handler not bound') + handler(new CustomEvent('progress_text', { detail })) + } + + beforeEach(() => { + vi.clearAllMocks() + apiEventHandlers.clear() + setActivePinia(createTestingPinia({ stubActions: false })) + store = useExecutionStore() + store.bindExecutionEvents() + }) + + it('should ignore progress_text before the canvas is initialized', async () => { + const { useCanvasStore } = + await import('@/renderer/core/canvas/canvasStore') + useCanvasStore().canvas = null + + expect(() => + fireProgressText({ + nodeId: '1', + text: 'warming up' + }) + ).not.toThrow() + + expect(mockShowTextPreview).not.toHaveBeenCalled() + }) + + it('should call showTextPreview when canvas is available', async () => { + const mockNode = createMockLGraphNode({ id: 1 }) + const { useCanvasStore } = + await import('@/renderer/core/canvas/canvasStore') + useCanvasStore().canvas = { + graph: { getNodeById: vi.fn(() => mockNode) } + } as unknown as LGraphCanvas + + fireProgressText({ nodeId: '1', text: 'warming up' }) + + expect(mockShowTextPreview).toHaveBeenCalledWith(mockNode, 'warming up') + }) +}) + describe('useExecutionErrorStore - Node Error Lookups', () => { let store: ReturnType diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index bbe42f227a..f69490c7a6 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -527,7 +527,7 @@ export const useExecutionStore = defineStore('execution', () => { // Handle execution node IDs for subgraphs const currentId = getNodeIdIfExecuting(nodeId) if (!currentId) return - const node = canvasStore.getCanvas().graph?.getNodeById(currentId) + const node = canvasStore.canvas?.graph?.getNodeById(currentId) if (!node) return useNodeProgressText().showTextPreview(node, text)