/** * @license * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ import { spawn } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { createInterface } from 'node:readline'; import { setTimeout as delay } from 'node:timers/promises'; import { describe, expect, it } from 'vitest'; import { TestRig } from '../test-helper.js'; const REQUEST_TIMEOUT_MS = 60_000; const INITIAL_PROMPT = 'Create a quick note (smoke test).'; const IS_SANDBOX = process.env['QWEN_SANDBOX'] && process.env['QWEN_SANDBOX']!.toLowerCase() !== 'false'; type PendingRequest = { resolve: (value: unknown) => void; reject: (reason: Error) => void; timeout: NodeJS.Timeout; }; type UsageMetadata = { promptTokens?: number | null; completionTokens?: number | null; thoughtsTokens?: number | null; totalTokens?: number | null; cachedTokens?: number | null; }; type SessionUpdateNotification = { sessionId?: string; update?: { sessionUpdate?: string; availableCommands?: Array<{ name: string; description: string; input?: { hint: string } | null; }>; content?: { type: string; text?: string; }; modeId?: string; currentModeId?: string; _meta?: { usage?: UsageMetadata; }; }; }; type PermissionRequest = { id: number; sessionId?: string; toolCall?: { toolCallId: string; title: string; kind: string; status: string; content?: Array<{ type: string; text?: string; path?: string; oldText?: string; newText?: string; }>; }; options?: Array<{ optionId: string; name: string; kind: string; }>; }; type PermissionHandler = ( request: PermissionRequest, ) => { optionId: string } | { outcome: 'cancelled' }; /** * Sets up an ACP test environment with all necessary utilities. * @param useNewFlag - If true, uses --acp; if false, uses --experimental-acp (for backward compatibility testing) */ function setupAcpTest( rig: TestRig, options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean }, ) { const pending = new Map(); let nextRequestId = 1; const sessionUpdates: SessionUpdateNotification[] = []; const permissionRequests: PermissionRequest[] = []; const stderr: string[] = []; // Default permission handler: auto-approve all const permissionHandler = options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' })); // Use --acp by default, but allow testing with --experimental-acp for backward compatibility const acpFlag = options?.useNewFlag !== false ? '--acp' : '--experimental-acp'; const agent = spawn( 'node', [rig.bundlePath, acpFlag, '--no-chat-recording'], { cwd: rig.testDir!, stdio: ['pipe', 'pipe', 'pipe'], }, ); agent.stderr?.on('data', (chunk) => { stderr.push(chunk.toString()); }); const rl = createInterface({ input: agent.stdout }); const send = (json: unknown) => { agent.stdin.write(`${JSON.stringify(json)}\n`); }; const sendResponse = (id: number, result: unknown) => { send({ jsonrpc: '2.0', id, result }); }; const sendRequest = (method: string, params?: unknown) => new Promise((resolve, reject) => { const id = nextRequestId++; const timeout = setTimeout(() => { pending.delete(id); reject(new Error(`Request ${id} (${method}) timed out`)); }, REQUEST_TIMEOUT_MS); pending.set(id, { resolve, reject, timeout }); send({ jsonrpc: '2.0', id, method, params }); }); const handleResponse = (msg: { id: number; result?: unknown; error?: { message?: string }; }) => { const waiter = pending.get(msg.id); if (!waiter) { return; } clearTimeout(waiter.timeout); pending.delete(msg.id); if (msg.error) { const error = new Error(msg.error.message ?? 'Unknown error'); (error as Error & { response?: unknown }).response = msg.error; waiter.reject(error); } else { waiter.resolve(msg.result); } }; const handleMessage = (msg: { id?: number; method?: string; params?: SessionUpdateNotification & { path?: string; content?: string; sessionId?: string; toolCall?: PermissionRequest['toolCall']; options?: PermissionRequest['options']; }; result?: unknown; error?: { message?: string }; }) => { if (typeof msg.id !== 'undefined' && ('result' in msg || 'error' in msg)) { handleResponse( msg as { id: number; result?: unknown; error?: { message?: string }; }, ); return; } if (msg.method === 'session/update') { sessionUpdates.push({ sessionId: msg.params?.sessionId, update: msg.params?.update, }); return; } if ( msg.method === 'session/request_permission' && typeof msg.id === 'number' ) { // Track permission request const permRequest: PermissionRequest = { id: msg.id, sessionId: msg.params?.sessionId, toolCall: msg.params?.toolCall, options: msg.params?.options, }; permissionRequests.push(permRequest); // Use custom handler or default const response = permissionHandler(permRequest); if ('outcome' in response) { sendResponse(msg.id, { outcome: response }); } else { sendResponse(msg.id, { outcome: { optionId: response.optionId, outcome: 'selected' }, }); } return; } if (msg.method === 'fs/read_text_file' && typeof msg.id === 'number') { try { const content = readFileSync(msg.params?.path ?? '', 'utf8'); sendResponse(msg.id, { content }); } catch (e) { sendResponse(msg.id, { content: `ERROR: ${(e as Error).message}` }); } return; } if (msg.method === 'fs/write_text_file' && typeof msg.id === 'number') { try { writeFileSync( msg.params?.path ?? '', msg.params?.content ?? '', 'utf8', ); sendResponse(msg.id, null); } catch (e) { sendResponse(msg.id, { message: (e as Error).message }); } } }; rl.on('line', (line) => { if (!line.trim()) return; try { const msg = JSON.parse(line); handleMessage(msg); } catch { // Ignore non-JSON output from the agent. } }); const waitForExit = () => new Promise((resolve) => { if (agent.exitCode !== null || agent.signalCode) { resolve(); return; } agent.once('exit', () => resolve()); }); const cleanup = async () => { rl.close(); agent.kill(); pending.forEach(({ timeout }) => clearTimeout(timeout)); pending.clear(); await waitForExit(); }; return { sendRequest, sendResponse, cleanup, stderr, sessionUpdates, permissionRequests, }; } (IS_SANDBOX ? describe.skip : describe)('acp integration', () => { it('basic smoke test', async () => { const rig = new TestRig(); rig.setup('acp load session'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { const initResult = await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); expect(initResult).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((initResult as any).agentInfo.version).toBeDefined(); await sendRequest('authenticate', { methodId: 'openai' }); const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); const promptResult = await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [{ type: 'text', text: INITIAL_PROMPT }], }); expect(promptResult).toBeDefined(); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('initializes and allows setting mode', async () => { const rig = new TestRig(); rig.setup('acp mode and model'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { // Test 1: Initialize and verify modes are returned const initResult = (await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, })) as { protocolVersion: number }; expect(initResult).toBeDefined(); expect(initResult.protocolVersion).toBe(1); // Test 2: Authenticate await sendRequest('authenticate', { methodId: 'openai' }); // Test 3: Create a new session const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string; models: { availableModels: Array<{ modelId: string }>; }; }; expect(newSession.sessionId).toBeTruthy(); expect(newSession.models.availableModels.length).toBeGreaterThan(0); // Test 4: Set approval mode to 'yolo' const setModeResult = (await sendRequest('session/set_mode', { sessionId: newSession.sessionId, modeId: 'yolo', })) as unknown; expect(setModeResult).toEqual({}); // Test 5: Set approval mode to 'auto-edit' const setModeResult2 = (await sendRequest('session/set_mode', { sessionId: newSession.sessionId, modeId: 'auto-edit', })) as unknown; expect(setModeResult2).toEqual({}); // Test 6: Set approval mode back to 'default' const setModeResult3 = (await sendRequest('session/set_mode', { sessionId: newSession.sessionId, modeId: 'default', })) as unknown; expect(setModeResult3).toEqual({}); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('returns internal error details when model auth is required', async () => { const rig = new TestRig(); rig.setup('acp auth methods in error data'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); // Create a new session first const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string; models: { availableModels: Array<{ modelId: string }>; }; }; // Choose a qwen-oauth model to trigger auth-required path deterministically. const qwenOauthModel = newSession.models.availableModels.find((model) => model.modelId.includes('qwen-oauth'), ); expect(qwenOauthModel).toBeDefined(); await expect( sendRequest('session/set_config_option', { sessionId: newSession.sessionId, configId: 'model', value: qwenOauthModel!.modelId, }), ).rejects.toMatchObject({ response: { code: -32603, message: 'Internal error', data: { details: expect.any(String), }, }, }); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('supports session/set_config_option for mode and model', async () => { const rig = new TestRig(); rig.setup('acp set config option'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { // Initialize await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); await sendRequest('authenticate', { methodId: 'openai' }); // Create a new session const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string; models: { availableModels: Array<{ modelId: string }>; }; }; expect(newSession.sessionId).toBeTruthy(); // Test: Set mode using set_config_option const setModeResult = (await sendRequest('session/set_config_option', { sessionId: newSession.sessionId, configId: 'mode', value: 'yolo', })) as { configOptions: Array<{ id: string; currentValue: string; options: Array<{ value: string; name: string; description: string }>; }>; }; expect(setModeResult).toBeDefined(); expect(Array.isArray(setModeResult.configOptions)).toBe(true); expect(setModeResult.configOptions.length).toBeGreaterThanOrEqual(2); // Find mode option const modeOption = setModeResult.configOptions.find( (opt) => opt.id === 'mode', ); expect(modeOption).toBeDefined(); expect(modeOption!.currentValue).toBe('yolo'); expect(Array.isArray(modeOption!.options)).toBe(true); expect(modeOption!.options.some((o) => o.value === 'yolo')).toBe(true); // Find model option const modelOption = setModeResult.configOptions.find( (opt) => opt.id === 'model', ); expect(modelOption).toBeDefined(); expect(modelOption!.currentValue).toBeTruthy(); // Test: Set model using set_config_option // Use openai model to avoid auth issues const openaiModel = newSession.models.availableModels.find((model) => model.modelId.includes('openai'), ); expect(openaiModel).toBeDefined(); const setModelResult = (await sendRequest('session/set_config_option', { sessionId: newSession.sessionId, configId: 'model', value: openaiModel!.modelId, })) as { configOptions: Array<{ id: string; currentValue: string; options: Array<{ value: string; name: string; description: string }>; }>; }; expect(setModelResult).toBeDefined(); expect(Array.isArray(setModelResult.configOptions)).toBe(true); // Verify model was updated const updatedModelOption = setModelResult.configOptions.find( (opt) => opt.id === 'model', ); expect(updatedModelOption).toBeDefined(); expect(updatedModelOption!.currentValue).toBe(openaiModel!.modelId); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('returns error for invalid configId in set_config_option', async () => { const rig = new TestRig(); rig.setup('acp set config option error'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { // Initialize await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); await sendRequest('authenticate', { methodId: 'openai' }); // Create a new session const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); // Test: Invalid configId should return error await expect( sendRequest('session/set_config_option', { sessionId: newSession.sessionId, configId: 'invalid_config', value: 'some_value', }), ).rejects.toMatchObject({ response: { code: -32602, message: 'Invalid params: Unsupported configId: invalid_config', }, }); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('receives available_commands_update with slash commands after session creation', async () => { const rig = new TestRig(); rig.setup('acp slash commands'); const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig); try { // Initialize await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); await sendRequest('authenticate', { methodId: 'openai' }); // Create a new session const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); // Wait for available_commands_update to be received await delay(1000); // Verify available_commands_update is received const commandsUpdate = sessionUpdates.find( (update) => update.update?.sessionUpdate === 'available_commands_update', ); expect(commandsUpdate).toBeDefined(); expect(commandsUpdate?.update?.availableCommands).toBeDefined(); expect(Array.isArray(commandsUpdate?.update?.availableCommands)).toBe( true, ); // Verify that the 'init' command is present (the only allowed built-in command for ACP) const initCommand = commandsUpdate?.update?.availableCommands?.find( (cmd) => cmd.name === 'init', ); expect(initCommand).toBeDefined(); expect(initCommand?.description).toBeTruthy(); // Note: We don't test /init execution here because it triggers a complex // multi-step process (listing files, reading up to 10 files, generating QWEN.md) // that can take 30-60+ seconds, exceeding the request timeout. // The slash command execution path is tested via simpler prompts in other tests. } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('handles exit plan mode with permission request and mode update notification', async () => { const rig = new TestRig(); rig.setup('acp exit plan mode'); // Track which permission requests we've seen const planModeRequests: PermissionRequest[] = []; const { sendRequest, cleanup, stderr, sessionUpdates, permissionRequests } = setupAcpTest(rig, { permissionHandler: (request) => { // Track all permission requests for later verification // Auto-approve exit plan mode requests with "proceed_always" to trigger auto-edit mode if (request.toolCall?.kind === 'switch_mode') { planModeRequests.push(request); // Return proceed_always to switch to auto-edit mode return { optionId: 'proceed_always' }; } // Auto-approve all other requests return { optionId: 'proceed_once' }; }, }); try { // Initialize await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); await sendRequest('authenticate', { methodId: 'openai' }); // Create a new session const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); // Set mode to 'plan' to enable plan mode const setModeResult = (await sendRequest('session/set_mode', { sessionId: newSession.sessionId, modeId: 'plan', })) as unknown; expect(setModeResult).toEqual({}); // Send a prompt that should trigger the LLM to call exit_plan_mode // The prompt is designed to trigger planning behavior const promptResult = await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [ { type: 'text', text: 'Create a simple hello world function in Python. Make a brief plan and when ready, use the exit_plan_mode tool to present it for approval.', }, ], }); expect(promptResult).toBeDefined(); // Give time for all notifications to be processed await delay(1000); // Verify: If exit_plan_mode was called, we should have received: // 1. A permission request with kind: "switch_mode" // 2. A current_mode_update notification after approval // Check for switch_mode permission requests const switchModeRequests = permissionRequests.filter( (req) => req.toolCall?.kind === 'switch_mode', ); // Check for current_mode_update notifications const modeUpdateNotifications = sessionUpdates.filter( (update) => update.update?.sessionUpdate === 'current_mode_update', ); // If the LLM called exit_plan_mode, verify the flow if (switchModeRequests.length > 0) { // Verify permission request structure const permReq = switchModeRequests[0]; expect(permReq.toolCall).toBeDefined(); expect(permReq.toolCall?.kind).toBe('switch_mode'); expect(permReq.toolCall?.status).toBe('pending'); expect(permReq.options).toBeDefined(); expect(Array.isArray(permReq.options)).toBe(true); // Verify options include appropriate choices const optionKinds = permReq.options?.map((opt) => opt.kind) ?? []; expect(optionKinds).toContain('allow_once'); expect(optionKinds).toContain('allow_always'); // After approval, should have received current_mode_update expect(modeUpdateNotifications.length).toBeGreaterThan(0); // Verify mode update structure const modeUpdate = modeUpdateNotifications[0]; expect(modeUpdate.sessionId).toBe(newSession.sessionId); expect(modeUpdate.update?.currentModeId).toBeDefined(); // Mode should be auto-edit since we approved with proceed_always expect(modeUpdate.update?.currentModeId).toBe('auto-edit'); } // Note: If the LLM didn't call exit_plan_mode, that's acceptable // since LLM behavior is non-deterministic. The test setup and structure // is verified regardless. } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('blocks write tools in plan mode (issue #1806)', async () => { const rig = new TestRig(); rig.setup('acp plan mode enforcement'); const toolCallEvents: Array<{ toolName: string; status: string; error?: string; }> = []; const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig, { permissionHandler: (request) => { // Cancel exit_plan_mode to keep plan mode active if (request.toolCall?.kind === 'switch_mode') { return { outcome: 'cancelled' }; } return { optionId: 'proceed_once' }; }, }); try { await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, }); await sendRequest('authenticate', { methodId: 'openai' }); const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; // Set mode to 'plan' const setModeResult = (await sendRequest('session/set_mode', { sessionId: newSession.sessionId, modeId: 'plan', })) as unknown; expect(setModeResult).toEqual({}); // Try to create a file - this should be blocked by plan mode const promptResult = await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [ { type: 'text', text: 'Create a file called test.txt with content "Hello World"', }, ], }); expect(promptResult).toBeDefined(); // Give time for tool calls to be processed await delay(2000); // Collect tool call events from session updates sessionUpdates.forEach((update) => { if (update.update?.sessionUpdate === 'tool_call_update') { const toolUpdate = update.update as { sessionUpdate: string; toolName?: string; status?: string; error?: { message?: string }; }; if (toolUpdate.toolName) { toolCallEvents.push({ toolName: toolUpdate.toolName, status: toolUpdate.status ?? 'unknown', error: toolUpdate.error?.message, }); } } }); // Verify that if write_file was attempted, it was blocked const writeFileEvents = toolCallEvents.filter( (e) => e.toolName === 'write_file', ); // If the LLM tried to call write_file in plan mode, it should have been blocked if (writeFileEvents.length > 0) { const blockedEvent = writeFileEvents.find( (e) => e.status === 'error' && e.error?.includes('Plan mode'), ); expect(blockedEvent).toBeDefined(); expect(blockedEvent?.error).toContain('Plan mode is active'); } // Verify the file was NOT created const fs = await import('fs'); const path = await import('path'); const testFilePath = path.join(rig.testDir!, 'test.txt'); const fileExists = fs.existsSync(testFilePath); expect(fileExists).toBe(false); } catch (e) { if (stderr.length) console.error('Agent stderr:', stderr.join('')); throw e; } finally { await cleanup(); } }); it('receives usage metadata in agent_message_chunk updates', async () => { const rig = new TestRig(); rig.setup('acp usage metadata'); const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig); try { await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, }); await sendRequest('authenticate', { methodId: 'openai' }); const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [{ type: 'text', text: 'Say "hello".' }], }); await delay(500); // Find updates with usage metadata const updatesWithUsage = sessionUpdates.filter( (u) => u.update?.sessionUpdate === 'agent_message_chunk' && u.update?._meta?.usage, ); expect(updatesWithUsage.length).toBeGreaterThan(0); const usage = updatesWithUsage[0].update?._meta?.usage; expect(usage).toBeDefined(); expect( typeof usage?.promptTokens === 'number' || typeof usage?.totalTokens === 'number', ).toBe(true); } catch (e) { if (stderr.length) console.error('Agent stderr:', stderr.join('')); throw e; } finally { await cleanup(); } }); }); (IS_SANDBOX ? describe.skip : describe)( 'acp flag backward compatibility', () => { it('should work with deprecated --experimental-acp flag and show warning', async () => { const rig = new TestRig(); rig.setup('acp backward compatibility'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig, { useNewFlag: false, }); try { const initResult = await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); expect(initResult).toBeDefined(); // Verify deprecation warning is shown const stderrOutput = stderr.join(''); expect(stderrOutput).toContain('--experimental-acp is deprecated'); expect(stderrOutput).toContain('Please use --acp instead'); await sendRequest('authenticate', { methodId: 'openai' }); const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); // Verify functionality still works const promptResult = await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [{ type: 'text', text: 'Say hello.' }], }); expect(promptResult).toBeDefined(); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); it('should work with new --acp flag without warnings', async () => { const rig = new TestRig(); rig.setup('acp new flag'); const { sendRequest, cleanup, stderr } = setupAcpTest(rig, { useNewFlag: true, }); try { const initResult = await sendRequest('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, }, }); expect(initResult).toBeDefined(); // Verify no deprecation warning is shown const stderrOutput = stderr.join(''); expect(stderrOutput).not.toContain('--experimental-acp is deprecated'); await sendRequest('authenticate', { methodId: 'openai' }); const newSession = (await sendRequest('session/new', { cwd: rig.testDir!, mcpServers: [], })) as { sessionId: string }; expect(newSession.sessionId).toBeTruthy(); // Verify functionality works const promptResult = await sendRequest('session/prompt', { sessionId: newSession.sessionId, prompt: [{ type: 'text', text: 'Say hello.' }], }); expect(promptResult).toBeDefined(); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); } throw e; } finally { await cleanup(); } }); }, );