Compare commits

..

19 Commits

Author SHA1 Message Date
xuewenjie
8673426d5c fix(core): use current chunk for shell output update instead of cumulative 2025-12-16 10:26:20 +08:00
xuewenjie
b272ac0119 Fix: Make cleanup strategy dynamic to support testing mocks 2025-12-12 17:47:03 +08:00
xuewenjie
574d89da14 Refactor ShellExecutionService cleanup to use strategy pattern 2025-12-12 17:03:04 +08:00
xuewenjie
16939c0bc8 Refactor ShellTool: remove ping hack and timeout, optimize cleanup 2025-12-10 13:49:51 +08:00
xuewenjie
6fc09a82fb fix: use && for windows background keep-alive ping and add test 2025-12-09 13:33:42 +08:00
xuewenjie
d622f8d1bf Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2025-12-09 11:32:17 +08:00
xuewenjie
28d178b5c1 fix: handle windows background execution errors and add tests 2025-12-09 11:24:30 +08:00
tanzhenxin
5fddcd509c pump versionm to 0.4.1 (#1177) 2025-12-09 10:25:07 +08:00
pomelo
a6a572336c Merge pull request #1157 from QwenLM/fix/windows-shell-output-garbled 2025-12-08 16:45:05 +08:00
xuewenjie
e5e1e6a3da Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-shell-output-garbled 2025-12-08 11:35:47 +08:00
Mingholy
b4be2c6c7f Merge pull request #1166 from QwenLM/mingholy/fix/unstable-e2e-test
test: skip unstable e2e test
2025-12-06 18:42:29 +08:00
mingholy.lmh
8b5b8d2b90 test: skip unstable e2e test 2025-12-06 18:41:19 +08:00
Mingholy
6e826b815e Merge pull request #1165 from QwenLM/mingholy/fix/sdk-timeout
fix: update timeout settings and default logging level in SDK
2025-12-06 18:18:23 +08:00
mingholy.lmh
86b166bb1d fix: adjust e2e tests via timeout option 2025-12-06 17:53:31 +08:00
mingholy.lmh
bf6abf7752 fix: update timeout settings and default logging level in SDK 2025-12-06 12:27:16 +08:00
xuewenjie
2c1a836f18 fix: prefer UTF-8 encoding for shell output on Windows when detected 2025-12-05 16:49:26 +08:00
xuewenjie
4c69d536ac test: fix shell tool tests by updating pid expectation and AbortSignal matching 2025-12-05 10:47:06 +08:00
xuewenjie
403fd06117 chore: update .gitignore 2025-12-04 15:55:17 +08:00
xuewenjie
d9928eab66 fix: improve windows background process handling and cleanup 2025-12-04 15:55:11 +08:00
19 changed files with 308 additions and 67 deletions

View File

@@ -1195,7 +1195,7 @@ describe('Permission Control (E2E)', () => {
});
describe('mode comparison tests', () => {
it(
it.skip(
'should demonstrate different behaviors across all modes for write operations',
async () => {
const modes: Array<'default' | 'auto-edit' | 'yolo'> = [

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.0",
"version": "0.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.4.0",
"version": "0.4.1",
"workspaces": [
"packages/*"
],
@@ -16422,7 +16422,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.4.0",
"version": "0.4.1",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -16537,7 +16537,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.4.0",
"version": "0.4.1",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -19106,7 +19106,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.4.0",
"version": "0.4.1",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -19118,7 +19118,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.4.0",
"version": "0.4.1",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.0",
"version": "0.4.1",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.4.0",
"version": "0.4.1",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.4.0",
"version": "0.4.1",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -589,7 +589,7 @@ describe('ShellExecutionService child_process fallback', () => {
expect(result.error).toBeNull();
expect(result.aborted).toBe(false);
expect(result.output).toBe('file1.txt\na warning');
expect(handle.pid).toBe(undefined);
expect(handle.pid).toBe(12345);
expect(onOutputEventMock).toHaveBeenCalledWith({
type: 'data',

View File

@@ -7,7 +7,7 @@
import stripAnsi from 'strip-ansi';
import type { PtyImplementation } from '../utils/getPty.js';
import { getPty } from '../utils/getPty.js';
import { spawn as cpSpawn } from 'node:child_process';
import { spawn as cpSpawn, spawnSync } from 'node:child_process';
import { TextDecoder } from 'node:util';
import os from 'node:os';
import type { IPty } from '@lydell/node-pty';
@@ -98,6 +98,48 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
return lines.join('\n').trimEnd();
};
interface ProcessCleanupStrategy {
killPty(pid: number, pty: ActivePty): void;
killChildProcesses(pids: Set<number>): void;
}
const windowsStrategy: ProcessCleanupStrategy = {
killPty: (_pid, pty) => {
pty.ptyProcess.kill();
},
killChildProcesses: (pids) => {
if (pids.size > 0) {
try {
const args = ['/f', '/t'];
for (const pid of pids) {
args.push('/pid', pid.toString());
}
spawnSync('taskkill', args);
} catch {
// ignore
}
}
},
};
const posixStrategy: ProcessCleanupStrategy = {
killPty: (pid, _pty) => {
process.kill(-pid, 'SIGKILL');
},
killChildProcesses: (pids) => {
for (const pid of pids) {
try {
process.kill(-pid, 'SIGKILL');
} catch {
// ignore
}
}
},
};
const getCleanupStrategy = () =>
os.platform() === 'win32' ? windowsStrategy : posixStrategy;
/**
* A centralized service for executing shell commands with robust process
* management, cross-platform compatibility, and streaming output capabilities.
@@ -106,6 +148,29 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
export class ShellExecutionService {
private static activePtys = new Map<number, ActivePty>();
private static activeChildProcesses = new Set<number>();
static cleanup() {
const strategy = getCleanupStrategy();
// Cleanup PTYs
for (const [pid, pty] of this.activePtys) {
try {
strategy.killPty(pid, pty);
} catch {
// ignore
}
}
// Cleanup child processes
strategy.killChildProcesses(this.activeChildProcesses);
}
static {
process.on('exit', () => {
ShellExecutionService.cleanup();
});
}
/**
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
*
@@ -281,9 +346,13 @@ export class ShellExecutionService {
abortSignal.addEventListener('abort', abortHandler, { once: true });
if (child.pid) {
this.activeChildProcesses.add(child.pid);
}
child.on('exit', (code, signal) => {
if (child.pid) {
this.activePtys.delete(child.pid);
this.activeChildProcesses.delete(child.pid);
}
handleExit(code, signal);
});
@@ -310,7 +379,7 @@ export class ShellExecutionService {
}
});
return { pid: undefined, result };
return { pid: child.pid, result };
} catch (e) {
const error = e as Error;
return {

View File

@@ -210,7 +210,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -237,7 +237,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -262,7 +262,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -287,7 +287,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -312,7 +312,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir/subdir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -340,7 +340,7 @@ describe('ShellTool', () => {
'dir',
'/test/dir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -433,7 +433,7 @@ describe('ShellTool', () => {
expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith(
expect.any(String),
mockConfig.getGeminiClient(),
mockAbortSignal,
expect.any(AbortSignal),
1000,
);
expect(result.llmContent).toBe('summarized output');
@@ -542,7 +542,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -572,7 +572,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -602,7 +602,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -631,7 +631,7 @@ describe('ShellTool', () => {
expect.stringContaining('npm install'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -660,7 +660,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -690,7 +690,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -726,7 +726,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit -m "Initial commit"'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -763,7 +763,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -831,4 +831,68 @@ describe('ShellTool', () => {
expect(shellTool.description).toMatchSnapshot();
});
});
describe('Windows background execution', () => {
it('should clean up trailing ampersand on Windows for background tasks', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const mockAbortSignal = new AbortController().signal;
const invocation = shellTool.build({
command: 'npm start &',
is_background: true,
});
const promise = invocation.execute(mockAbortSignal);
// Simulate immediate success (process started)
resolveExecutionPromise({
rawOutput: Buffer.from(''),
output: '',
exitCode: 0,
signal: null,
error: null,
aborted: false,
pid: 12345,
executionMethod: 'child_process',
});
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'npm start',
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
false,
{},
);
});
it('should detect immediate failure in Windows background task', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const mockAbortSignal = new AbortController().signal;
const invocation = shellTool.build({
command: 'invalid_command',
is_background: true,
});
const promise = invocation.execute(mockAbortSignal);
// Wait a tick to ensure mockShellOutputCallback is assigned
await new Promise((resolve) => setTimeout(resolve, 0));
if (mockShellOutputCallback) {
mockShellOutputCallback({
type: 'data',
chunk:
"'invalid_command' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n",
});
}
const result = await promise;
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('Command failed to start');
});
});
});

View File

@@ -30,6 +30,7 @@ import { summarizeToolOutput } from '../utils/summarizer.js';
import type {
ShellExecutionConfig,
ShellOutputEvent,
ShellExecutionResult,
} from '../services/shellExecutionService.js';
import { ShellExecutionService } from '../services/shellExecutionService.js';
import { formatMemoryUsage } from '../utils/formatters.js';
@@ -143,11 +144,29 @@ export class ShellToolInvocation extends BaseToolInvocation<
const shouldRunInBackground = this.params.is_background;
let finalCommand = processedCommand;
// If explicitly marked as background and doesn't already end with &, add it
if (shouldRunInBackground && !finalCommand.trim().endsWith('&')) {
// On non-Windows, use & to run in background.
// On Windows, we don't use start /B because it creates a detached process that
// doesn't die when the parent dies. Instead, we rely on the race logic below
// to return early while keeping the process attached (detached: false).
if (
!isWindows &&
shouldRunInBackground &&
!finalCommand.trim().endsWith('&')
) {
finalCommand = finalCommand.trim() + ' &';
}
// On Windows, we rely on the race logic below to handle background tasks.
// We just ensure the command string is clean.
if (isWindows && shouldRunInBackground) {
let cmd = finalCommand.trim();
// Remove trailing & (common Linux habit, invalid on Windows at end of line)
while (cmd.endsWith('&')) {
cmd = cmd.slice(0, -1).trim();
}
finalCommand = cmd;
}
// pgrep is not available on Windows, so we can't get background PIDs
const commandToExecute = isWindows
? finalCommand
@@ -169,10 +188,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
commandToExecute,
cwd,
(event: ShellOutputEvent) => {
if (!updateOutput) {
return;
}
let shouldUpdate = false;
switch (event.type) {
@@ -201,7 +216,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
}
if (shouldUpdate) {
if (shouldUpdate && updateOutput) {
updateOutput(
typeof cumulativeOutput === 'string'
? cumulativeOutput
@@ -219,7 +234,59 @@ export class ShellToolInvocation extends BaseToolInvocation<
setPidCallback(pid);
}
const result = await resultPromise;
let result: ShellExecutionResult;
if (shouldRunInBackground && isWindows) {
// For Windows background tasks, we wait a short time to catch immediate errors.
// If it's still running, we return early.
const startupDelay = 1000;
const raceResult = await Promise.race([
resultPromise,
new Promise<null>((resolve) =>
setTimeout(() => resolve(null), startupDelay),
),
]);
if (raceResult === null) {
// Timeout reached, process is still running.
// throw new Error(`DEBUG: raceResult is null. Output: ${JSON.stringify(cumulativeOutput)}`);
// Check for common Windows error messages in the output
const outputStr =
typeof cumulativeOutput === 'string'
? cumulativeOutput
: JSON.stringify(cumulativeOutput);
console.log('DEBUG: outputStr:', outputStr);
const errorPatterns = [
'is not recognized as an internal or external command',
'The system cannot find the path specified',
'Access is denied',
];
if (errorPatterns.some((pattern) => outputStr.includes(pattern))) {
return {
llmContent: `Command failed to start: ${outputStr}`,
returnDisplay: `Command failed to start: ${outputStr}`,
error: {
type: ToolErrorType.EXECUTION_FAILED,
message: `Command failed to start: ${outputStr}`,
},
};
}
const pidMsg = pid ? ` PID: ${pid}` : '';
const winHint = isWindows
? ' (Note: Use taskkill /F /T /PID <pid> to stop)'
: '';
return {
llmContent: `Background command started.${pidMsg}${winHint}`,
returnDisplay: `Background command started.${pidMsg}${winHint}`,
};
} else {
result = raceResult;
}
} else {
result = await resultPromise;
}
const backgroundPIDs: number[] = [];
if (os.platform() !== 'win32') {

View File

@@ -391,6 +391,19 @@ describe('Shell Command Processor - Encoding Functions', () => {
expect(result).toBe('windows-1252');
});
it('should prioritize UTF-8 detection over Windows system encoding', () => {
mockedOsPlatform.mockReturnValue('win32');
mockedExecSync.mockReturnValue('Active code page: 936'); // GBK
const buffer = Buffer.from('test');
// Mock chardet to return UTF-8
mockedChardetDetect.mockReturnValue('UTF-8');
const result = getCachedEncodingForBuffer(buffer);
expect(result).toBe('utf-8');
});
it('should cache null system encoding result', () => {
// Reset the cache specifically for this test
resetEncodingCache();

View File

@@ -34,6 +34,15 @@ export function getCachedEncodingForBuffer(buffer: Buffer): string {
// If we have a cached system encoding, use it
if (cachedSystemEncoding) {
// If the system encoding is not UTF-8 (e.g. Windows CP936), but the buffer
// is detected as UTF-8, prefer UTF-8. This handles tools like 'git' which
// often output UTF-8 regardless of the system code page.
if (cachedSystemEncoding !== 'utf-8') {
const detected = detectEncodingFromBuffer(buffer);
if (detected === 'utf-8') {
return 'utf-8';
}
}
return cachedSystemEncoding;
}

View File

@@ -59,7 +59,7 @@ Creates a new query session with the Qwen Code.
| `model` | `string` | - | The AI model to use (e.g., `'qwen-max'`, `'qwen-plus'`, `'qwen-turbo'`). Takes precedence over `OPENAI_MODEL` and `QWEN_MODEL` environment variables. |
| `pathToQwenExecutable` | `string` | Auto-detected | Path to the Qwen Code executable. Supports multiple formats: `'qwen'` (native binary from PATH), `'/path/to/qwen'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, auto-detects from: `QWEN_CODE_CLI_PATH` env var, `~/.volta/bin/qwen`, `~/.npm-global/bin/qwen`, `/usr/local/bin/qwen`, `~/.local/bin/qwen`, `~/node_modules/.bin/qwen`, `~/.yarn/bin/qwen`. |
| `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. |
| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 30 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). |
| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). |
| `env` | `Record<string, string>` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. |
| `mcpServers` | `Record<string, McpServerConfig>` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. |
| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
@@ -76,12 +76,12 @@ Creates a new query session with the Qwen Code.
The SDK enforces the following default timeouts:
| Timeout | Default | Description |
| ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `canUseTool` | 30 seconds | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. |
| `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. |
| `controlRequest` | 30 seconds | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. |
| `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. |
| Timeout | Default | Description |
| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `canUseTool` | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. |
| `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. |
| `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. |
| `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. |
You can customize these timeouts via the `timeout` option:

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/sdk",
"version": "0.1.0-preview.0",
"version": "0.1.0",
"description": "TypeScript SDK for programmatic access to qwen-code CLI",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",

View File

@@ -5,9 +5,9 @@
* Implements AsyncIterator protocol for message consumption.
*/
const DEFAULT_CAN_USE_TOOL_TIMEOUT = 30_000;
const DEFAULT_CAN_USE_TOOL_TIMEOUT = 60_000;
const DEFAULT_MCP_REQUEST_TIMEOUT = 60_000;
const DEFAULT_CONTROL_REQUEST_TIMEOUT = 30_000;
const DEFAULT_CONTROL_REQUEST_TIMEOUT = 60_000;
const DEFAULT_STREAM_CLOSE_TIMEOUT = 60_000;
import { randomUUID } from 'node:crypto';
@@ -434,8 +434,9 @@ export class Query implements AsyncIterable<SDKMessage> {
try {
const canUseToolTimeout =
this.options.timeout?.canUseTool ?? DEFAULT_CAN_USE_TOOL_TIMEOUT;
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(
timeoutId = setTimeout(
() => reject(new Error('Permission callback timeout')),
canUseToolTimeout,
);
@@ -451,6 +452,10 @@ export class Query implements AsyncIterable<SDKMessage> {
timeoutPromise,
]);
if (timeoutId) {
clearTimeout(timeoutId);
}
if (result.behavior === 'allow') {
return {
behavior: 'allow',
@@ -789,14 +794,20 @@ export class Query implements AsyncIterable<SDKMessage> {
) {
const streamCloseTimeout =
this.options.timeout?.streamClose ?? DEFAULT_STREAM_CLOSE_TIMEOUT;
await Promise.race([
this.firstResultReceivedPromise,
new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, streamCloseTimeout);
}),
]);
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<void>((resolve) => {
timeoutId = setTimeout(() => {
logger.info('streamCloseTimeout resolved');
resolve();
}, streamCloseTimeout);
});
await Promise.race([this.firstResultReceivedPromise, timeoutPromise]);
if (timeoutId) {
clearTimeout(timeoutId);
}
}
this.endInput();

View File

@@ -316,7 +316,7 @@ export interface QueryOptions {
/**
* Logging level for the SDK.
* Controls the verbosity of log messages output by the SDK.
* @default 'info'
* @default 'error'
*/
logLevel?: 'debug' | 'info' | 'warn' | 'error';

View File

@@ -22,7 +22,7 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
export class SdkLogger {
private static config: LoggerConfig = {};
private static effectiveLevel: LogLevel = 'info';
private static effectiveLevel: LogLevel = 'error';
static configure(config: LoggerConfig): void {
this.config = config;
@@ -47,7 +47,7 @@ export class SdkLogger {
return 'debug';
}
return 'info';
return 'error';
}
private static isValidLogLevel(level: string): boolean {

View File

@@ -542,13 +542,16 @@ describe('Query', () => {
const canUseTool = vi.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => resolve({ behavior: 'allow' }), 35000); // Exceeds 30s timeout
setTimeout(() => resolve({ behavior: 'allow' }), 15000);
}),
);
const query = new Query(transport, {
cwd: '/test',
canUseTool,
timeout: {
canUseTool: 10000,
},
});
const controlReq = createControlRequest('can_use_tool', 'perm-req-4');
@@ -567,7 +570,7 @@ describe('Query', () => {
});
}
},
{ timeout: 35000 },
{ timeout: 15000 },
);
await query.close();
@@ -1204,7 +1207,12 @@ describe('Query', () => {
});
it('should handle control request timeout', async () => {
const query = new Query(transport, { cwd: '/test' });
const query = new Query(transport, {
cwd: '/test',
timeout: {
controlRequest: 10000,
},
});
// Respond to initialize
await vi.waitFor(() => {
@@ -1224,7 +1232,7 @@ describe('Query', () => {
await expect(interruptPromise).rejects.toThrow(/timeout/i);
await query.close();
}, 35000);
}, 15000);
it('should handle malformed control responses', async () => {
const query = new Query(transport, { cwd: '/test' });

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.4.0",
"version": "0.4.1",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {