mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
621 lines
16 KiB
TypeScript
621 lines
16 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();
|
|
}
|
|
});
|
|
|
|
it('should accept authType: qwen-oauth', async () => {
|
|
// Note: qwen-oauth requires credentials in ~/.qwen
|
|
// This test may fail if credentials are not configured
|
|
// The test verifies the option is accepted and passed correctly
|
|
|
|
const stderrMessages: string[] = [];
|
|
|
|
const q = query({
|
|
prompt: 'Hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testDir,
|
|
authType: 'qwen-oauth',
|
|
debug: true,
|
|
stderr: (msg: string) => {
|
|
stderrMessages.push(msg);
|
|
},
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// The query should at least start (may fail due to missing credentials)
|
|
expect(messages.length).toBeGreaterThan(0);
|
|
} catch (error) {
|
|
// qwen-oauth may fail if credentials are not configured
|
|
// This is acceptable - we're testing that the option is passed correctly
|
|
expect(error).toBeDefined();
|
|
} 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,
|
|
logLevel: 'debug',
|
|
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();
|
|
}
|
|
});
|
|
});
|
|
});
|