qwen-code/packages/cli/src/acp-integration/acpAgent.test.ts
顾盼 97926a07fe
fix(acp): support SSE and HTTP MCP servers in ACP mode (#3574)
In ACP mode, the Mcp server list sent by the IDE client can include
SSE (type: "sse") and HTTP (type: "http") transports, but the previous
implementation only handled stdio servers via toStdioServer(). Non-stdio
servers were silently skipped (continue), so any SSE/HTTP-configured
MCP server would never be registered.

Changes:
- Add toSseServer() helper: detects type=="sse" servers and maps them
  to MCPServerConfig(url=..., headers=...)
- Add toHttpServer() helper: detects type=="http" servers and maps them
  to MCPServerConfig(httpUrl=..., headers=...)
- Refactor newSessionConfig() loop to handle all three transport types
- Declare mcpCapabilities: { sse: true, http: true } in agentCapabilities
  so IDE clients know this agent supports these transports without needing
  a transparent proxy
- Export the three helper functions for unit testing

Tests:
- Unit tests for toStdioServer / toSseServer / toHttpServer helpers
  (type discrimination, mutual exclusion)
- Integration-style tests for QwenAgent.initialize() mcpCapabilities
- Integration-style tests for newSession() with SSE/HTTP MCP servers,
  verifying MCPServerConfig is constructed with the correct arguments
  (url vs httpUrl, headers passthrough, empty-headers → undefined)

Fixes #3472
2026-04-24 14:53:01 +08:00

896 lines
26 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
afterAll,
type MockInstance,
} from 'vitest';
// Mock cleanup module before importing anything else
const { mockRunExitCleanup } = vi.hoisted(() => ({
mockRunExitCleanup: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('../utils/cleanup.js', () => ({
runExitCleanup: mockRunExitCleanup,
}));
// Mock the ACP SDK
const { mockConnectionState } = vi.hoisted(() => {
const state = {
resolve: () => {},
promise: null as unknown as Promise<void>,
reset() {
state.promise = new Promise<void>((r) => {
state.resolve = r;
});
},
};
state.reset();
return { mockConnectionState: state };
});
vi.mock('@agentclientprotocol/sdk', () => ({
AgentSideConnection: vi.fn().mockImplementation(() => ({
get closed() {
return mockConnectionState.promise;
},
})),
ndJsonStream: vi.fn().mockReturnValue({}),
RequestError: class RequestError extends Error {
static authRequired = vi
.fn()
.mockImplementation((data: unknown, msg: string) => {
const err = new Error(msg);
Object.assign(err, data);
return err;
});
static invalidParams = vi
.fn()
.mockImplementation((data: unknown, msg: string) => {
const err = new Error(msg);
Object.assign(err, data);
return err;
});
},
PROTOCOL_VERSION: '1.0.0',
}));
// Mock stream conversion
vi.mock('node:stream', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:stream')>();
return {
...actual,
Writable: { ...actual.Writable, toWeb: vi.fn().mockReturnValue({}) },
Readable: { ...actual.Readable, toWeb: vi.fn().mockReturnValue({}) },
};
});
// Mock core dependencies
vi.mock('@qwen-code/qwen-code-core', () => ({
createDebugLogger: () => ({
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
}),
APPROVAL_MODE_INFO: {},
APPROVAL_MODES: [],
AuthType: {},
clearCachedCredentialFile: vi.fn(),
QwenOAuth2Event: {},
qwenOAuth2Events: { on: vi.fn(), off: vi.fn() },
MCPServerConfig: vi.fn().mockImplementation((...args: unknown[]) => ({
_args: args,
})),
SessionService: vi.fn(),
tokenLimit: vi.fn(),
SessionStartSource: {
Startup: 'startup',
Resume: 'resume',
},
SessionEndReason: {
PromptInputExit: 'prompt_input_exit',
Other: 'other',
},
}));
vi.mock('./authMethods.js', () => ({ buildAuthMethods: vi.fn() }));
vi.mock('./service/filesystem.js', () => ({
AcpFileSystemService: vi.fn(),
}));
vi.mock('../config/settings.js', () => ({
SettingScope: {},
loadSettings: vi.fn(),
}));
vi.mock('../config/config.js', () => ({ loadCliConfig: vi.fn() }));
vi.mock('./session/Session.js', () => ({ Session: vi.fn() }));
vi.mock('../utils/acpModelUtils.js', () => ({
formatAcpModelId: vi.fn(),
}));
import {
runAcpAgent,
toStdioServer,
toSseServer,
toHttpServer,
} from './acpAgent.js';
import type { Config } from '@qwen-code/qwen-code-core';
import type { LoadedSettings } from '../config/settings.js';
import type { CliArgs } from '../config/config.js';
import { SessionEndReason, MCPServerConfig } from '@qwen-code/qwen-code-core';
import type { McpServer } from '@agentclientprotocol/sdk';
import { AgentSideConnection } from '@agentclientprotocol/sdk';
import { loadSettings } from '../config/settings.js';
import { loadCliConfig } from '../config/config.js';
import { Session } from './session/Session.js';
describe('runAcpAgent shutdown cleanup', () => {
let processExitSpy: MockInstance<typeof process.exit>;
let processOnSpy: MockInstance<typeof process.on>;
let processOffSpy: MockInstance<typeof process.off>;
let stdinDestroySpy: MockInstance<typeof process.stdin.destroy>;
let stdoutDestroySpy: MockInstance<typeof process.stdout.destroy>;
let sigTermListeners: NodeJS.SignalsListener[];
let sigIntListeners: NodeJS.SignalsListener[];
let mockConfig: Config;
const mockSettings = { merged: {} } as LoadedSettings;
const mockArgv = {} as CliArgs;
beforeEach(() => {
vi.clearAllMocks();
// Reset mockConfig after clearAllMocks
mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getHookSystem: vi.fn().mockReturnValue(undefined),
getDisableAllHooks: vi.fn().mockReturnValue(false),
hasHooksForEvent: vi.fn().mockReturnValue(false),
getModel: vi.fn().mockReturnValue('test-model'),
} as unknown as Config;
mockRunExitCleanup.mockResolvedValue(undefined);
mockConnectionState.reset();
sigTermListeners = [];
sigIntListeners = [];
// Intercept signal handler registration
processOnSpy = vi.spyOn(process, 'on').mockImplementation(((
event: string,
listener: (...args: unknown[]) => void,
) => {
if (event === 'SIGTERM')
sigTermListeners.push(listener as NodeJS.SignalsListener);
if (event === 'SIGINT')
sigIntListeners.push(listener as NodeJS.SignalsListener);
return process;
}) as typeof process.on);
processOffSpy = vi.spyOn(process, 'off').mockImplementation(((
event: string,
listener: (...args: unknown[]) => void,
) => {
if (event === 'SIGTERM') {
sigTermListeners = sigTermListeners.filter((l) => l !== listener);
}
if (event === 'SIGINT') {
sigIntListeners = sigIntListeners.filter((l) => l !== listener);
}
return process;
}) as typeof process.off);
// Mock process.exit to prevent actually exiting
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as unknown as typeof process.exit);
// Mock stdin/stdout destroy
stdinDestroySpy = vi
.spyOn(process.stdin, 'destroy')
.mockImplementation(() => process.stdin);
stdoutDestroySpy = vi
.spyOn(process.stdout, 'destroy')
.mockImplementation(() => process.stdout);
});
afterEach(() => {
processExitSpy.mockRestore();
stdinDestroySpy.mockRestore();
stdoutDestroySpy.mockRestore();
vi.clearAllMocks();
});
afterAll(() => {
processOnSpy.mockRestore();
processOffSpy.mockRestore();
});
it('calls runExitCleanup and process.exit on SIGTERM', async () => {
// Start runAcpAgent (it will await connection.closed)
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
// Wait for signal handlers to be registered
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
// Simulate SIGTERM from IDE
sigTermListeners[0]('SIGTERM');
// runExitCleanup is async, wait for it
await vi.waitFor(() => {
expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
});
await vi.waitFor(() => {
expect(processExitSpy).toHaveBeenCalledWith(0);
});
// Resolve connection.closed so the promise settles
mockConnectionState.resolve();
await agentPromise;
});
it('calls runExitCleanup and process.exit on SIGINT', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
// Wait for signal handlers to be registered
await vi.waitFor(() => {
expect(sigIntListeners.length).toBeGreaterThan(0);
});
sigIntListeners[0]('SIGINT');
await vi.waitFor(() => {
expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
});
await vi.waitFor(() => {
expect(processExitSpy).toHaveBeenCalledWith(0);
});
mockConnectionState.resolve();
await agentPromise;
});
it('only runs shutdown once even if multiple signals arrive', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
// Wait for signal handlers to be registered
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
// Send SIGTERM twice
sigTermListeners[0]('SIGTERM');
sigTermListeners[0]('SIGTERM');
await vi.waitFor(() => {
expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
});
mockConnectionState.resolve();
await agentPromise;
});
it('still exits even if runExitCleanup throws', async () => {
mockRunExitCleanup.mockRejectedValueOnce(new Error('cleanup failed'));
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
// Wait for signal handlers to be registered
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
sigTermListeners[0]('SIGTERM');
// process.exit should still be called via .finally()
await vi.waitFor(() => {
expect(processExitSpy).toHaveBeenCalledWith(0);
});
mockConnectionState.resolve();
await agentPromise;
});
});
describe('runAcpAgent SessionEnd hooks', () => {
let processExitSpy: MockInstance<typeof process.exit>;
let processOnSpy: MockInstance<typeof process.on>;
let processOffSpy: MockInstance<typeof process.off>;
let stdinDestroySpy: MockInstance<typeof process.stdin.destroy>;
let stdoutDestroySpy: MockInstance<typeof process.stdout.destroy>;
let sigTermListeners: NodeJS.SignalsListener[];
let sigIntListeners: NodeJS.SignalsListener[];
let mockConfig: Config;
let mockHookSystem: {
fireSessionEndEvent: ReturnType<typeof vi.fn>;
fireSessionStartEvent: ReturnType<typeof vi.fn>;
};
const mockSettings = { merged: {} } as LoadedSettings;
const mockArgv = {} as CliArgs;
beforeEach(() => {
vi.clearAllMocks();
mockHookSystem = {
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
};
mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
getDisableAllHooks: vi.fn().mockReturnValue(false),
hasHooksForEvent: vi.fn().mockReturnValue(true),
getModel: vi.fn().mockReturnValue('test-model'),
} as unknown as Config;
mockRunExitCleanup.mockResolvedValue(undefined);
mockConnectionState.reset();
sigTermListeners = [];
sigIntListeners = [];
processOnSpy = vi.spyOn(process, 'on').mockImplementation(((
event: string,
listener: (...args: unknown[]) => void,
) => {
if (event === 'SIGTERM')
sigTermListeners.push(listener as NodeJS.SignalsListener);
if (event === 'SIGINT')
sigIntListeners.push(listener as NodeJS.SignalsListener);
return process;
}) as typeof process.on);
processOffSpy = vi.spyOn(process, 'off').mockImplementation(((
event: string,
listener: (...args: unknown[]) => void,
) => {
if (event === 'SIGTERM') {
sigTermListeners = sigTermListeners.filter((l) => l !== listener);
}
if (event === 'SIGINT') {
sigIntListeners = sigIntListeners.filter((l) => l !== listener);
}
return process;
}) as typeof process.off);
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as unknown as typeof process.exit);
stdinDestroySpy = vi
.spyOn(process.stdin, 'destroy')
.mockImplementation(() => process.stdin);
stdoutDestroySpy = vi
.spyOn(process.stdout, 'destroy')
.mockImplementation(() => process.stdout);
});
afterEach(() => {
processExitSpy.mockRestore();
stdinDestroySpy.mockRestore();
stdoutDestroySpy.mockRestore();
vi.clearAllMocks();
});
afterAll(() => {
processOnSpy.mockRestore();
processOffSpy.mockRestore();
});
it('fires SessionEnd hook with Other reason on SIGTERM', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
sigTermListeners[0]('SIGTERM');
await vi.waitFor(() => {
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(
SessionEndReason.Other,
);
});
mockConnectionState.resolve();
await agentPromise;
});
it('fires SessionEnd hook with Other reason on SIGINT', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => {
expect(sigIntListeners.length).toBeGreaterThan(0);
});
sigIntListeners[0]('SIGINT');
await vi.waitFor(() => {
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(
SessionEndReason.Other,
);
});
mockConnectionState.resolve();
await agentPromise;
});
it('fires SessionEnd hook with PromptInputExit on connection.closed', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
// Resolve connection to simulate IDE disconnect
mockConnectionState.resolve();
await vi.waitFor(() => {
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(
SessionEndReason.PromptInputExit,
);
});
await agentPromise;
});
it('does not fire SessionEnd hook when hooks are disabled', async () => {
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
sigTermListeners[0]('SIGTERM');
await vi.waitFor(() => {
expect(mockRunExitCleanup).toHaveBeenCalled();
});
// SessionEnd hook should NOT be called
expect(mockHookSystem.fireSessionEndEvent).not.toHaveBeenCalled();
mockConnectionState.resolve();
await agentPromise;
});
it('does not fire SessionEnd hook when event not registered', async () => {
mockConfig.hasHooksForEvent = vi.fn().mockReturnValue(false);
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
sigTermListeners[0]('SIGTERM');
await vi.waitFor(() => {
expect(mockRunExitCleanup).toHaveBeenCalled();
});
// SessionEnd hook should NOT be called
expect(mockHookSystem.fireSessionEndEvent).not.toHaveBeenCalled();
mockConnectionState.resolve();
await agentPromise;
});
it('fires SessionEnd hook only once when SIGTERM triggers before connection.closed', async () => {
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => {
expect(sigTermListeners.length).toBeGreaterThan(0);
});
// Trigger SIGTERM first
sigTermListeners[0]('SIGTERM');
await vi.waitFor(() => {
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(
SessionEndReason.Other,
);
});
// Now resolve connection.closed - this should NOT trigger another SessionEnd
mockConnectionState.resolve();
// Wait for the agent to complete
await agentPromise;
// SessionEnd should have been called exactly once
expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledTimes(1);
});
});
// ---------------------------------------------------------------------------
// Unit tests for toStdioServer / toSseServer / toHttpServer helpers
// ---------------------------------------------------------------------------
describe('toStdioServer', () => {
const stdioServer = {
name: 'my-stdio',
command: 'node',
args: ['server.js'],
env: [],
} as unknown as McpServer;
const sseServer = {
type: 'sse',
name: 'my-sse',
url: 'http://localhost:3000/sse',
headers: [],
} as unknown as McpServer;
it('returns the server when it is a stdio server', () => {
expect(toStdioServer(stdioServer)).toBe(stdioServer);
});
it('returns undefined for SSE server', () => {
expect(toStdioServer(sseServer)).toBeUndefined();
});
it('returns undefined for HTTP server', () => {
const httpServer = {
type: 'http',
name: 'my-http',
url: 'http://localhost:3000/mcp',
headers: [],
} as unknown as McpServer;
expect(toStdioServer(httpServer)).toBeUndefined();
});
});
describe('toSseServer', () => {
it('returns the server when type is sse', () => {
const sseServer = {
type: 'sse',
name: 'my-sse',
url: 'http://localhost:3000/sse',
headers: [],
} as unknown as McpServer;
const result = toSseServer(sseServer);
expect(result).toBe(sseServer);
expect(result?.type).toBe('sse');
});
it('returns undefined for stdio server', () => {
const stdioServer = {
name: 'my-stdio',
command: 'node',
args: [],
env: [],
} as unknown as McpServer;
expect(toSseServer(stdioServer)).toBeUndefined();
});
it('returns undefined for http server', () => {
const httpServer = {
type: 'http',
name: 'my-http',
url: 'http://localhost:3000/mcp',
headers: [],
} as unknown as McpServer;
expect(toSseServer(httpServer)).toBeUndefined();
});
});
describe('toHttpServer', () => {
it('returns the server when type is http', () => {
const httpServer = {
type: 'http',
name: 'my-http',
url: 'http://localhost:3000/mcp',
headers: [],
} as unknown as McpServer;
const result = toHttpServer(httpServer);
expect(result).toBe(httpServer);
expect(result?.type).toBe('http');
});
it('returns undefined for stdio server', () => {
const stdioServer = {
name: 'my-stdio',
command: 'node',
args: [],
env: [],
} as unknown as McpServer;
expect(toHttpServer(stdioServer)).toBeUndefined();
});
it('returns undefined for sse server', () => {
const sseServer = {
type: 'sse',
name: 'my-sse',
url: 'http://localhost:3000/sse',
headers: [],
} as unknown as McpServer;
expect(toHttpServer(sseServer)).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Tests for QwenAgent.initialize() mcpCapabilities + newSession SSE/HTTP
// ---------------------------------------------------------------------------
describe('QwenAgent MCP SSE/HTTP support', () => {
// We need to capture the agent factory from AgentSideConnection constructor
let capturedAgentFactory:
| ((conn: AgentSideConnectionLike) => AgentLike)
| undefined;
type AgentSideConnectionLike = { closed: Promise<void> };
type AgentLike = {
initialize: (args: Record<string, unknown>) => Promise<unknown>;
newSession: (args: Record<string, unknown>) => Promise<unknown>;
};
let mockConfig: Config;
let processExitSpy: MockInstance<typeof process.exit>;
let stdinDestroySpy: MockInstance<typeof process.stdin.destroy>;
let stdoutDestroySpy: MockInstance<typeof process.stdout.destroy>;
const mockArgv = {} as CliArgs;
beforeEach(() => {
vi.clearAllMocks();
mockConnectionState.reset();
capturedAgentFactory = undefined;
// Override AgentSideConnection mock to capture factory
vi.mocked(AgentSideConnection).mockImplementation((factory: unknown) => {
capturedAgentFactory = factory as typeof capturedAgentFactory;
return {
get closed() {
return mockConnectionState.promise;
},
} as unknown as InstanceType<typeof AgentSideConnection>;
});
mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getHookSystem: vi.fn().mockReturnValue(undefined),
getDisableAllHooks: vi.fn().mockReturnValue(false),
hasHooksForEvent: vi.fn().mockReturnValue(false),
getModel: vi.fn().mockReturnValue('test-model'),
getModelsConfig: vi.fn().mockReturnValue({
getCurrentAuthType: vi.fn().mockReturnValue('api-key'),
}),
refreshAuth: vi.fn().mockResolvedValue(undefined),
} as unknown as Config;
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as unknown as typeof process.exit);
stdinDestroySpy = vi
.spyOn(process.stdin, 'destroy')
.mockImplementation(() => process.stdin);
stdoutDestroySpy = vi
.spyOn(process.stdout, 'destroy')
.mockImplementation(() => process.stdout);
});
afterEach(() => {
processExitSpy.mockRestore();
stdinDestroySpy.mockRestore();
stdoutDestroySpy.mockRestore();
});
it('initialize response includes mcpCapabilities with sse and http', async () => {
const mockSettings = {
merged: { mcpServers: {} },
} as unknown as LoadedSettings;
const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
const fakeConn = {
get closed() {
return mockConnectionState.promise;
},
} as AgentSideConnectionLike;
const agent = capturedAgentFactory!(fakeConn) as AgentLike;
const response = await agent.initialize({ clientCapabilities: {} });
expect(response).toMatchObject({
agentCapabilities: {
mcpCapabilities: {
sse: true,
http: true,
},
},
});
mockConnectionState.resolve();
await agentPromise;
});
function makeInnerConfig() {
return {
initialize: vi.fn().mockResolvedValue(undefined),
getModelsConfig: vi.fn().mockReturnValue({
getCurrentAuthType: vi.fn().mockReturnValue('api-key'),
}),
refreshAuth: vi.fn().mockResolvedValue(undefined),
getModel: vi.fn().mockReturnValue('m'),
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
getAvailableModels: vi.fn().mockReturnValue([]),
getModes: vi.fn().mockReturnValue([]),
getApprovalMode: vi.fn().mockReturnValue('default'),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
}),
getFileSystemService: vi.fn().mockReturnValue(undefined),
setFileSystemService: vi.fn(),
getHookSystem: vi.fn().mockReturnValue(undefined),
getDisableAllHooks: vi.fn().mockReturnValue(true),
hasHooksForEvent: vi.fn().mockReturnValue(false),
};
}
function makeSessionSettings() {
return {
merged: { mcpServers: {} },
getUserHooks: vi.fn().mockReturnValue({}),
getProjectHooks: vi.fn().mockReturnValue({}),
} as unknown as LoadedSettings;
}
async function setupSessionMocks(sessionId: string) {
const innerConfig = makeInnerConfig();
vi.mocked(loadSettings).mockReturnValue(makeSessionSettings());
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
vi.mocked(Session).mockImplementation(
() =>
({
getId: vi.fn().mockReturnValue(sessionId),
getConfig: vi.fn().mockReturnValue(innerConfig),
sendAvailableCommandsUpdate: vi.fn().mockResolvedValue(undefined),
replayHistory: vi.fn().mockResolvedValue(undefined),
installRewriter: vi.fn(),
}) as unknown as InstanceType<typeof Session>,
);
return innerConfig;
}
it('newSession with SSE MCP server creates MCPServerConfig with url', async () => {
await setupSessionMocks('session-sse');
const agentPromise = runAcpAgent(
mockConfig,
makeSessionSettings(),
mockArgv,
);
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
const agent = capturedAgentFactory!({
get closed() {
return mockConnectionState.promise;
},
}) as AgentLike;
await agent.newSession({
cwd: '/tmp',
mcpServers: [
{
type: 'sse',
name: 'my-sse-server',
url: 'http://localhost:3001/sse',
headers: [{ name: 'Authorization', value: 'Bearer token123' }],
},
],
});
expect(MCPServerConfig).toHaveBeenCalledWith(
undefined,
undefined,
undefined,
undefined,
'http://localhost:3001/sse',
undefined,
{ Authorization: 'Bearer token123' },
);
mockConnectionState.resolve();
await agentPromise;
});
it('newSession with HTTP MCP server creates MCPServerConfig with httpUrl', async () => {
await setupSessionMocks('session-http');
const agentPromise = runAcpAgent(
mockConfig,
makeSessionSettings(),
mockArgv,
);
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
const agent = capturedAgentFactory!({
get closed() {
return mockConnectionState.promise;
},
}) as AgentLike;
await agent.newSession({
cwd: '/tmp',
mcpServers: [
{
type: 'http',
name: 'my-http-server',
url: 'http://localhost:3002/mcp',
headers: [],
},
],
});
expect(MCPServerConfig).toHaveBeenCalledWith(
undefined,
undefined,
undefined,
undefined,
undefined,
'http://localhost:3002/mcp',
undefined,
);
mockConnectionState.resolve();
await agentPromise;
});
it('newSession with SSE MCP server and empty headers passes undefined for headers', async () => {
await setupSessionMocks('session-sse-noheaders');
const agentPromise = runAcpAgent(
mockConfig,
makeSessionSettings(),
mockArgv,
);
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
const agent = capturedAgentFactory!({
get closed() {
return mockConnectionState.promise;
},
}) as AgentLike;
await agent.newSession({
cwd: '/tmp',
mcpServers: [
{
type: 'sse',
name: 'no-header-sse',
url: 'http://localhost:3003/sse',
headers: [],
},
],
});
expect(MCPServerConfig).toHaveBeenCalledWith(
undefined,
undefined,
undefined,
undefined,
'http://localhost:3003/sse',
undefined,
undefined,
);
mockConnectionState.resolve();
await agentPromise;
});
});