Files
qwen-code/integration-tests/sdk-typescript/single-turn.test.ts
mingholy.lmh 322ce80e2c feat: implement SDK MCP server support and enhance control request handling
- Added new `SdkMcpController` to manage communication between CLI MCP clients and SDK MCP servers.
- Introduced `createSdkMcpServer` function for creating SDK-embedded MCP servers.
- Updated configuration options to support both external and SDK MCP servers.
- Enhanced timeout settings for various SDK operations, including MCP requests.
- Refactored existing control request handling to accommodate new SDK MCP server functionality.
- Updated tests to cover new SDK MCP server features and ensure proper integration.
2025-12-05 13:14:54 +08:00

529 lines
14 KiB
TypeScript

/**
* E2E tests for single-turn query execution
* Tests basic query patterns with simple prompts and clear output expectations
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
query,
isSDKAssistantMessage,
isSDKSystemMessage,
isSDKResultMessage,
isSDKPartialAssistantMessage,
type SDKMessage,
type SDKSystemMessage,
type SDKAssistantMessage,
} from '@qwen-code/sdk-typescript';
import {
SDKTestHelper,
extractText,
createSharedTestOptions,
assertSuccessfulCompletion,
collectMessagesByType,
} from './test-helper.js';
const SHARED_TEST_OPTIONS = createSharedTestOptions();
describe('Single-Turn Query (E2E)', () => {
let helper: SDKTestHelper;
let testDir: string;
beforeEach(async () => {
helper = new SDKTestHelper();
testDir = await helper.setup('single-turn');
});
afterEach(async () => {
await helper.cleanup();
});
describe('Simple Text Queries', () => {
it('should answer basic arithmetic question', async () => {
const q = query({
prompt: 'What is 2 + 2? Just give me the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: true,
},
});
const messages: SDKMessage[] = [];
let assistantText = '';
try {
for await (const message of q) {
messages.push(message);
if (isSDKAssistantMessage(message)) {
assistantText += extractText(message.message.content);
}
}
// Validate we got messages
expect(messages.length).toBeGreaterThan(0);
// Validate assistant response content
expect(assistantText.length).toBeGreaterThan(0);
expect(assistantText).toMatch(/4/);
// Validate message flow ends with success
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should answer simple factual question', async () => {
const q = query({
prompt: 'What is the capital of France? One word answer.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
const messages: SDKMessage[] = [];
let assistantText = '';
try {
for await (const message of q) {
messages.push(message);
if (isSDKAssistantMessage(message)) {
assistantText += extractText(message.message.content);
}
}
// Validate content
expect(assistantText.length).toBeGreaterThan(0);
expect(assistantText.toLowerCase()).toContain('paris');
// Validate completion
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should handle greeting and self-description', async () => {
const q = query({
prompt: 'Say hello and tell me your name in one sentence.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
const messages: SDKMessage[] = [];
let assistantText = '';
try {
for await (const message of q) {
messages.push(message);
if (isSDKAssistantMessage(message)) {
assistantText += extractText(message.message.content);
}
}
// Validate content contains greeting
expect(assistantText.length).toBeGreaterThan(0);
expect(assistantText.toLowerCase()).toMatch(/hello|hi|greetings/);
// Validate message types
const assistantMessages = collectMessagesByType(
messages,
isSDKAssistantMessage,
);
expect(assistantMessages.length).toBeGreaterThan(0);
} finally {
await q.close();
}
});
});
describe('System Initialization', () => {
it('should receive system message with initialization info', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
const messages: SDKMessage[] = [];
let systemMessage: SDKSystemMessage | null = null;
try {
for await (const message of q) {
messages.push(message);
if (isSDKSystemMessage(message) && message.subtype === 'init') {
systemMessage = message;
}
}
// Validate system message exists and has required fields
expect(systemMessage).not.toBeNull();
expect(systemMessage!.type).toBe('system');
expect(systemMessage!.subtype).toBe('init');
expect(systemMessage!.uuid).toBeDefined();
expect(systemMessage!.session_id).toBeDefined();
expect(systemMessage!.cwd).toBeDefined();
expect(systemMessage!.tools).toBeDefined();
expect(Array.isArray(systemMessage!.tools)).toBe(true);
expect(systemMessage!.mcp_servers).toBeDefined();
expect(Array.isArray(systemMessage!.mcp_servers)).toBe(true);
expect(systemMessage!.model).toBeDefined();
expect(systemMessage!.permission_mode).toBeDefined();
expect(systemMessage!.qwen_code_version).toBeDefined();
// Validate system message appears early in sequence
const systemMessageIndex = messages.findIndex(
(msg) => isSDKSystemMessage(msg) && msg.subtype === 'init',
);
expect(systemMessageIndex).toBeGreaterThanOrEqual(0);
expect(systemMessageIndex).toBeLessThan(3);
} finally {
await q.close();
}
});
it('should maintain session ID consistency', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
let systemMessage: SDKSystemMessage | null = null;
const sessionId = q.getSessionId();
try {
for await (const message of q) {
if (isSDKSystemMessage(message) && message.subtype === 'init') {
systemMessage = message;
}
}
// Validate session IDs are consistent
expect(sessionId).toBeDefined();
expect(systemMessage).not.toBeNull();
expect(systemMessage!.session_id).toBeDefined();
expect(systemMessage!.uuid).toBeDefined();
expect(systemMessage!.session_id).toBe(systemMessage!.uuid);
} finally {
await q.close();
}
});
});
describe('Message Flow', () => {
it('should follow expected message sequence', async () => {
const q = query({
prompt: 'Say hi',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
const messageTypes: string[] = [];
try {
for await (const message of q) {
messageTypes.push(message.type);
}
// Validate message sequence
expect(messageTypes.length).toBeGreaterThan(0);
expect(messageTypes).toContain('assistant');
expect(messageTypes[messageTypes.length - 1]).toBe('result');
} finally {
await q.close();
}
});
it('should complete iteration naturally', async () => {
const q = query({
prompt: 'Say goodbye',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
let completedNaturally = false;
let messageCount = 0;
try {
for await (const message of q) {
messageCount++;
if (isSDKResultMessage(message)) {
completedNaturally = true;
expect(message.subtype).toBe('success');
}
}
expect(messageCount).toBeGreaterThan(0);
expect(completedNaturally).toBe(true);
} finally {
await q.close();
}
});
});
describe('Configuration Options', () => {
it('should respect debug option and capture stderr', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
try {
for await (const _message of q) {
// Consume all messages
}
// Debug mode should produce stderr output
expect(stderrMessages.length).toBeGreaterThan(0);
} finally {
await q.close();
}
});
it('should respect cwd option', async () => {
const q = query({
prompt: 'What is 1 + 1?',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
let hasResponse = false;
try {
for await (const message of q) {
if (isSDKAssistantMessage(message)) {
hasResponse = true;
}
}
expect(hasResponse).toBe(true);
} finally {
await q.close();
}
});
it('should receive partial messages when includePartialMessages is enabled', async () => {
const q = query({
prompt: 'Count from 1 to 5',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
includePartialMessages: true,
debug: false,
},
});
const messages: SDKMessage[] = [];
let partialMessageCount = 0;
let assistantMessageCount = 0;
try {
for await (const message of q) {
messages.push(message);
if (isSDKPartialAssistantMessage(message)) {
partialMessageCount++;
}
if (isSDKAssistantMessage(message)) {
assistantMessageCount++;
}
}
expect(messages.length).toBeGreaterThan(0);
expect(partialMessageCount).toBeGreaterThan(0);
expect(assistantMessageCount).toBeGreaterThan(0);
} finally {
await q.close();
}
});
});
describe('Message Type Recognition', () => {
it('should correctly identify all message types', async () => {
const q = query({
prompt: 'What is 5 + 5?',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Validate type guards work correctly
const assistantMessages = collectMessagesByType(
messages,
isSDKAssistantMessage,
);
const resultMessages = collectMessagesByType(
messages,
isSDKResultMessage,
);
const systemMessages = collectMessagesByType(
messages,
isSDKSystemMessage,
);
expect(assistantMessages.length).toBeGreaterThan(0);
expect(resultMessages.length).toBeGreaterThan(0);
expect(systemMessages.length).toBeGreaterThan(0);
// Validate assistant message structure
const firstAssistant = assistantMessages[0];
expect(firstAssistant.message.content).toBeDefined();
expect(Array.isArray(firstAssistant.message.content)).toBe(true);
// Validate result message structure
const resultMessage = resultMessages[0];
expect(resultMessage.subtype).toBe('success');
} finally {
await q.close();
}
});
it('should extract text content from assistant messages', async () => {
const q = query({
prompt: 'Count from 1 to 3',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
let assistantMessage: SDKAssistantMessage | null = null;
try {
for await (const message of q) {
if (isSDKAssistantMessage(message)) {
assistantMessage = message;
}
}
expect(assistantMessage).not.toBeNull();
expect(assistantMessage!.message.content).toBeDefined();
// Validate content contains expected numbers
const text = extractText(assistantMessage!.message.content);
expect(text.length).toBeGreaterThan(0);
expect(text).toMatch(/1/);
expect(text).toMatch(/2/);
expect(text).toMatch(/3/);
} finally {
await q.close();
}
});
});
describe('Error Handling', () => {
it('should throw if CLI not found', async () => {
try {
const q = query({
prompt: 'Hello',
options: {
pathToQwenExecutable: '/nonexistent/path/to/cli',
debug: false,
},
});
for await (const _message of q) {
// Should not reach here
}
expect(false).toBe(true); // Should have thrown
} catch (error) {
expect(error).toBeDefined();
expect(error instanceof Error).toBe(true);
expect((error as Error).message).toContain(
'Invalid pathToQwenExecutable',
);
}
});
});
describe('Resource Management', () => {
it('should cleanup subprocess on close()', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
// Start and immediately close
const iterator = q[Symbol.asyncIterator]();
await iterator.next();
// Should close without error
await q.close();
expect(true).toBe(true); // Cleanup completed
});
it('should handle close() called multiple times', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: false,
},
});
// Start the query
const iterator = q[Symbol.asyncIterator]();
await iterator.next();
// Close multiple times
await q.close();
await q.close();
await q.close();
// Should not throw
expect(true).toBe(true);
});
});
});