Files
qwen-code/integration-tests/sdk-typescript/configuration-options.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

641 lines
17 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* E2E tests for SDK configuration options:
* - logLevel: Controls SDK internal logging verbosity
* - env: Environment variables passed to CLI process
* - authType: Authentication type for AI service
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
query,
isSDKAssistantMessage,
isSDKSystemMessage,
type SDKMessage,
} from '@qwen-code/sdk-typescript';
import {
SDKTestHelper,
extractText,
createSharedTestOptions,
assertSuccessfulCompletion,
} from './test-helper.js';
const SHARED_TEST_OPTIONS = createSharedTestOptions();
describe('Configuration Options (E2E)', () => {
let helper: SDKTestHelper;
let testDir: string;
beforeEach(async () => {
helper = new SDKTestHelper();
testDir = await helper.setup('configuration-options');
});
afterEach(async () => {
await helper.cleanup();
});
describe('logLevel Option', () => {
it('should respect logLevel: debug and capture detailed logs', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'What is 1 + 1? Just answer the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'debug',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Debug level should produce verbose logging
expect(stderrMessages.length).toBeGreaterThan(0);
// Debug logs should contain detailed information like [DEBUG]
const hasDebugLogs = stderrMessages.some(
(msg) => msg.includes('[DEBUG]') || msg.includes('debug'),
);
expect(hasDebugLogs).toBe(true);
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should respect logLevel: info and filter out debug messages', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'What is 2 + 2? Just answer the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'info',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Info level should filter out debug messages
// Check that we don't have [DEBUG] level messages from the SDK logger
const sdkDebugLogs = stderrMessages.filter(
(msg) =>
msg.includes('[DEBUG]') && msg.includes('[ProcessTransport]'),
);
expect(sdkDebugLogs.length).toBe(0);
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should respect logLevel: warn and only show warnings and errors', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'Say hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'warn',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Warn level should filter out info and debug messages from SDK
const sdkInfoOrDebugLogs = stderrMessages.filter(
(msg) =>
(msg.includes('[DEBUG]') || msg.includes('[INFO]')) &&
(msg.includes('[ProcessTransport]') ||
msg.includes('[createQuery]') ||
msg.includes('[Query]')),
);
expect(sdkInfoOrDebugLogs.length).toBe(0);
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should respect logLevel: error and only show error messages', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'Hello world',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'error',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Error level should filter out all non-error messages from SDK
const sdkNonErrorLogs = stderrMessages.filter(
(msg) =>
(msg.includes('[DEBUG]') ||
msg.includes('[INFO]') ||
msg.includes('[WARN]')) &&
(msg.includes('[ProcessTransport]') ||
msg.includes('[createQuery]') ||
msg.includes('[Query]')),
);
expect(sdkNonErrorLogs.length).toBe(0);
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should use logLevel over debug flag when both are provided', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'What is 3 + 3?',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
debug: true, // Would normally enable debug logging
logLevel: 'error', // But logLevel should take precedence
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
try {
for await (const _message of q) {
// Consume all messages
}
// logLevel: error should suppress debug/info/warn even with debug: true
const sdkNonErrorLogs = stderrMessages.filter(
(msg) =>
(msg.includes('[DEBUG]') ||
msg.includes('[INFO]') ||
msg.includes('[WARN]')) &&
(msg.includes('[ProcessTransport]') ||
msg.includes('[createQuery]') ||
msg.includes('[Query]')),
);
expect(sdkNonErrorLogs.length).toBe(0);
} finally {
await q.close();
}
});
});
describe('env Option', () => {
it('should pass custom environment variables to CLI process', async () => {
const q = query({
prompt: 'What is 1 + 1? Just the number please.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {
CUSTOM_TEST_VAR: 'test_value_12345',
ANOTHER_VAR: 'another_value',
},
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// The query should complete successfully with custom env vars
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should allow overriding existing environment variables', async () => {
// Store original value for comparison
const originalPath = process.env['PATH'];
const q = query({
prompt: 'Say hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {
// Override an existing env var (not PATH as it might break things)
MY_TEST_OVERRIDE: 'overridden_value',
},
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Query should complete successfully
assertSuccessfulCompletion(messages);
// Verify original process env is not modified
expect(process.env['PATH']).toBe(originalPath);
} finally {
await q.close();
}
});
it('should work with empty env object', async () => {
const q = query({
prompt: 'What is 2 + 2?',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {},
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should support setting model-related environment variables', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {
// Common model-related env vars that CLI might respect
OPENAI_API_KEY: process.env['OPENAI_API_KEY'] || 'test-key',
},
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Should complete (may succeed or fail based on API key validity)
expect(messages.length).toBeGreaterThan(0);
} finally {
await q.close();
}
});
it('should not leak env vars between query instances', async () => {
// First query with specific env var
const q1 = query({
prompt: 'Say one',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {
ISOLATED_VAR_1: 'value_1',
},
debug: false,
},
});
try {
for await (const _message of q1) {
// Consume messages
}
} finally {
await q1.close();
}
// Second query with different env var
const q2 = query({
prompt: 'Say two',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
env: {
ISOLATED_VAR_2: 'value_2',
},
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q2) {
messages.push(message);
}
// Second query should complete successfully
assertSuccessfulCompletion(messages);
// Verify process.env is not polluted by either query
expect(process.env['ISOLATED_VAR_1']).toBeUndefined();
expect(process.env['ISOLATED_VAR_2']).toBeUndefined();
} finally {
await q2.close();
}
});
});
describe('authType Option', () => {
it('should accept authType: openai', async () => {
const q = query({
prompt: 'What is 1 + 1? Just the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
authType: 'openai',
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Query should complete with openai auth type
assertSuccessfulCompletion(messages);
// Verify we got an assistant response
const assistantMessages = messages.filter(isSDKAssistantMessage);
expect(assistantMessages.length).toBeGreaterThan(0);
} finally {
await q.close();
}
});
// Skip in containerized sandbox environments - qwen-oauth requires user interaction
// which is not possible in Docker/Podman CI environments
it.skipIf(
process.env['SANDBOX'] === 'sandbox:docker' ||
process.env['SANDBOX'] === 'sandbox:podman',
)('should accept authType: qwen-oauth', async () => {
// Note: qwen-oauth requires credentials in ~/.qwen and user interaction
// Without credentials, the auth process will timeout waiting for user
// This test verifies the option is accepted and passed correctly to CLI
const stderrMessages: string[] = [];
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
authType: 'qwen-oauth',
debug: true,
logLevel: 'debug',
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
// Use a timeout to avoid hanging when credentials are not configured
const timeoutPromise = new Promise<'timeout'>((resolve) =>
setTimeout(() => resolve('timeout'), 20000),
);
const collectMessages = async () => {
for await (const message of q) {
messages.push(message);
}
return 'completed';
};
const result = await Promise.race([collectMessages(), timeoutPromise]);
if (result === 'timeout') {
// Timeout is expected when OAuth credentials are not configured
// Verify that CLI was spawned with correct --auth-type argument
const hasAuthTypeArg = stderrMessages.some((msg) =>
msg.includes('--auth-type'),
);
expect(hasAuthTypeArg).toBe(true);
} else {
// If credentials exist and auth completed, verify we got messages
expect(messages.length).toBeGreaterThan(0);
}
} finally {
await q.close();
}
});
it('should use default auth when authType is not specified', async () => {
const q = query({
prompt: 'What is 2 + 2? Just the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
// authType not specified - should use default
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Query should complete with default auth
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should properly pass authType to CLI process', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'Say hi',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
authType: 'openai',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// There should be spawn log containing auth-type
const hasAuthTypeArg = stderrMessages.some((msg) =>
msg.includes('--auth-type'),
);
expect(hasAuthTypeArg).toBe(true);
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
});
describe('Combined Options', () => {
it('should work with logLevel, env, and authType together', async () => {
const stderrMessages: string[] = [];
const q = query({
prompt: 'What is 3 + 3? Just the number.',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'debug',
env: {
COMBINED_TEST_VAR: 'combined_value',
},
authType: 'openai',
debug: true,
stderr: (msg: string) => {
stderrMessages.push(msg);
},
},
});
const messages: SDKMessage[] = [];
let assistantText = '';
try {
for await (const message of q) {
messages.push(message);
if (isSDKAssistantMessage(message)) {
assistantText += extractText(message.message.content);
}
}
// All three options should work together
expect(stderrMessages.length).toBeGreaterThan(0); // logLevel: debug produces logs
expect(assistantText).toMatch(/6/); // Query should work
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
it('should maintain system message consistency with all options', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
logLevel: 'info',
env: {
SYSTEM_MSG_TEST: 'test',
},
authType: 'openai',
debug: false,
},
});
const messages: SDKMessage[] = [];
try {
for await (const message of q) {
messages.push(message);
}
// Should have system init message
const systemMessages = messages.filter(isSDKSystemMessage);
const initMessage = systemMessages.find((m) => m.subtype === 'init');
expect(initMessage).toBeDefined();
expect(initMessage!.session_id).toBeDefined();
expect(initMessage!.tools).toBeDefined();
expect(initMessage!.permission_mode).toBeDefined();
assertSuccessfulCompletion(messages);
} finally {
await q.close();
}
});
});
});