From 02bc2bf533364acbcb47099c36ee109541a813c0 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Mon, 17 Aug 2026 10:57:15 +0000 Subject: [PATCH] test(editor): Stop AgentBuilderView tests from leaking async work across tests (#36412) Co-authored-by: Claude Fable 5 --- .../agents/__tests__/AgentBuilderView.test.ts | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts index 52abd46b2e8..0c59da8b988 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts @@ -1,5 +1,5 @@ /* eslint-disable import-x/no-extraneous-dependencies, @typescript-eslint/no-unsafe-assignment -- test-only patterns: @vue/test-utils is a transitive devDep and private-state reads */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { nextTick, ref, computed, reactive } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; @@ -393,10 +393,25 @@ async function renderView({ stubs: commonStubs, }, }); + mountedWrappers.push(wrapper); if (waitForAsyncSetup) await flushPromises(); return wrapper; } +const mountedWrappers: Array> = []; + +// Unmount every view mounted by the test and drain its async tail. Without +// this, a still-mounted view's pending debounce timers and in-flight save +// chains keep running into later tests, where they consume shared mock +// `*Once` queues and emit bus events against the next test's component. +// A second unmount after a test's own `wrapper.unmount()` is a no-op. +afterEach(async () => { + for (const wrapper of mountedWrappers.splice(0)) { + wrapper.unmount(); + } + await flushPromises(); +}); + async function startArtifactPreviewSend( wrapper: Awaited>, sessionId: string, @@ -1584,11 +1599,17 @@ describe('AgentBuilderView — preview routing', { timeout: 60_000 }, () => { .spyOn(useMCPStore(), 'toggleAgentMcpAccess') .mockReturnValueOnce(mcpSave.promise); + // A failed initialize() only shows a toast, then renders the editor with a + // null agent — which silently drops the toggle below and surfaces three + // asserts later as an autosave spy with zero calls. Fail here instead, + // with the swallowed error visible in the spy's recorded calls. + expect(showErrorMock).not.toHaveBeenCalled(); + const editorColumn = wrapper.findComponent({ name: 'AgentBuilderEditorColumn' }); + expect(editorColumn.props('agent')).toBeTruthy(); + vi.useFakeTimers(); try { - wrapper - .findComponent({ name: 'AgentBuilderEditorColumn' }) - .vm.$emit('toggle-mcp-access', true); + editorColumn.vm.$emit('toggle-mcp-access', true); await nextTick(); await vi.advanceTimersByTimeAsync(500); expect(toggleAgentMcpAccess).toHaveBeenCalledExactlyOnceWith('a2', true); @@ -2071,11 +2092,15 @@ describe('AgentBuilderView — three-column shell', () => { unchangedIds: [], }); + // See the sibling MCP test above: a failed initialize() drops the toggle + // silently, so guard the mount before emitting. + expect(showErrorMock).not.toHaveBeenCalled(); + const editorColumn = wrapper.findComponent({ name: 'AgentBuilderEditorColumn' }); + expect(editorColumn.props('agent')).toBeTruthy(); + vi.useFakeTimers(); try { - wrapper - .findComponent({ name: 'AgentBuilderEditorColumn' }) - .vm.$emit('toggle-mcp-access', true); + editorColumn.vm.$emit('toggle-mcp-access', true); await nextTick(); await wrapper.setProps({ artifactAgentId: 'a2' }); @@ -2942,17 +2967,11 @@ describe('AgentBuilderView — three-column shell', () => { }); it('shows the loading spinner while initialize() is in flight and hides it after', async () => { - const { default: AgentBuilderView } = await import('../views/AgentBuilderView.vue'); - const pinia = createPinia(); - setActivePinia(pinia); - // A promise that we control — lets us capture the intermediate loading state. let resolveAgent!: (v: unknown) => void; getAgentMock.mockReturnValueOnce(new Promise((r) => (resolveAgent = r))); - const wrapper = mount(AgentBuilderView, { - global: { plugins: [pinia], stubs: commonStubs }, - }); + const wrapper = await renderView({ waitForAsyncSetup: false }); // initialize() hasn't resolved yet → spinner visible, content hidden. await nextTick(); @@ -2969,15 +2988,9 @@ describe('AgentBuilderView — three-column shell', () => { }); it('clears the loading spinner and shows an error when initialize() throws (finally path)', async () => { - const { default: AgentBuilderView } = await import('../views/AgentBuilderView.vue'); - const pinia = createPinia(); - setActivePinia(pinia); - getAgentMock.mockRejectedValueOnce(new Error('network error')); - const wrapper = mount(AgentBuilderView, { - global: { plugins: [pinia], stubs: commonStubs }, - }); + const wrapper = await renderView({ waitForAsyncSetup: false }); await flushPromises();