qwen-code/integration-tests/cli/acp-integration.test.ts
Shaojin Wen 9993c6f413
fix(test): restore openai model selection in ACP set_config_option test (#5721)
PR #5676 (an unrelated v5 settings-migration fix) inadvertently reverted
the model-selection assertion in the ACP integration test back to a flaky
form that asserts the session's current model is present in
availableModels. In CI (no real auth) the default current model is not in
availableModels, so the assertion fails with "expected false to be true",
breaking the Release workflow's "Integration Tests (No Sandbox)" job and
gating the Publish Release step.

Restore the robust form that explicitly picks an openai model from
availableModels (matching the version that shipped in every successful
release since March), which avoids the auth-dependent current-model
mismatch.
2026-06-23 06:53:41 +08:00

1016 lines
30 KiB
TypeScript

/**
* @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<number, PendingRequest>();
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<unknown>((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 });
}
return;
}
// JSON-RPC requires every request to get a response. Reject unknown
// agent->client requests (e.g. optional extension methods like
// craft/drainMidTurnQueue) with -32601 so the agent fails fast instead
// of awaiting a reply that never comes.
if (typeof msg.id === 'number' && typeof msg.method === 'string') {
send({
jsonrpc: '2.0',
id: msg.id,
error: { code: -32601, message: 'Method not found' },
});
}
};
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<void>((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();
}
});
},
);