fix: prevent sending control request when query is closed

This commit is contained in:
mingholy.lmh
2025-12-05 18:46:51 +08:00
parent 46478e5dd3
commit c218048551
3 changed files with 20 additions and 24 deletions

View File

@@ -555,6 +555,15 @@ describe('Permission Control (E2E)', () => {
...SHARED_TEST_OPTIONS, ...SHARED_TEST_OPTIONS,
cwd: testDir, cwd: testDir,
permissionMode: 'default', permissionMode: 'default',
timeout: {
/**
* We use a short control request timeout and
* wait till the time exceeded to test if
* an immediate close() will raise an query close
* error and no other uncaught timeout error
*/
controlRequest: 5000,
},
}, },
}); });
@@ -563,7 +572,9 @@ describe('Permission Control (E2E)', () => {
await expect(q.setPermissionMode('yolo')).rejects.toThrow( await expect(q.setPermissionMode('yolo')).rejects.toThrow(
'Query is closed', 'Query is closed',
); );
});
await new Promise((resolve) => setTimeout(resolve, 8000));
}, 10_000);
}); });
describe('canUseTool and setPermissionMode integration', () => { describe('canUseTool and setPermissionMode integration', () => {

View File

@@ -13,7 +13,7 @@ npm install @qwen-code/sdk-typescript
## Requirements ## Requirements
- Node.js >= 20.0.0 - Node.js >= 20.0.0
- [Qwen Code](https://github.com/QwenLM/qwen-code) installed and accessible in PATH - [Qwen Code](https://github.com/QwenLM/qwen-code) >= 0.4.0 (stable) installed and accessible in PATH
> **Note for nvm users**: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the `pathToQwenExecutable` option to the full path of the `qwen` binary. > **Note for nvm users**: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the `pathToQwenExecutable` option to the full path of the `qwen` binary.

View File

@@ -620,6 +620,10 @@ export class Query implements AsyncIterable<SDKMessage> {
subtype: string, subtype: string,
data: Record<string, unknown> = {}, data: Record<string, unknown> = {},
): Promise<Record<string, unknown> | null> { ): Promise<Record<string, unknown> | null> {
if (this.closed) {
return Promise.reject(new Error('Query is closed'));
}
const requestId = randomUUID(); const requestId = randomUUID();
const request: CLIControlRequest = { const request: CLIControlRequest = {
@@ -688,12 +692,13 @@ export class Query implements AsyncIterable<SDKMessage> {
for (const pending of this.pendingControlRequests.values()) { for (const pending of this.pendingControlRequests.values()) {
pending.abortController.abort(); pending.abortController.abort();
clearTimeout(pending.timeout); clearTimeout(pending.timeout);
pending.reject(new Error('Query is closed'));
} }
this.pendingControlRequests.clear(); this.pendingControlRequests.clear();
// Clean up pending MCP responses // Clean up pending MCP responses
for (const pending of this.pendingMcpResponses.values()) { for (const pending of this.pendingMcpResponses.values()) {
pending.reject(new Error('Query closed')); pending.reject(new Error('Query is closed'));
} }
this.pendingMcpResponses.clear(); this.pendingMcpResponses.clear();
@@ -719,7 +724,7 @@ export class Query implements AsyncIterable<SDKMessage> {
} }
} }
this.sdkMcpTransports.clear(); this.sdkMcpTransports.clear();
logger.info('Query closed'); logger.info('Query is closed');
} }
private async *readSdkMessages(): AsyncGenerator<SDKMessage> { private async *readSdkMessages(): AsyncGenerator<SDKMessage> {
@@ -821,28 +826,16 @@ export class Query implements AsyncIterable<SDKMessage> {
} }
async interrupt(): Promise<void> { async interrupt(): Promise<void> {
if (this.closed) {
throw new Error('Query is closed');
}
await this.sendControlRequest(ControlRequestType.INTERRUPT); await this.sendControlRequest(ControlRequestType.INTERRUPT);
} }
async setPermissionMode(mode: string): Promise<void> { async setPermissionMode(mode: string): Promise<void> {
if (this.closed) {
throw new Error('Query is closed');
}
await this.sendControlRequest(ControlRequestType.SET_PERMISSION_MODE, { await this.sendControlRequest(ControlRequestType.SET_PERMISSION_MODE, {
mode, mode,
}); });
} }
async setModel(model: string): Promise<void> { async setModel(model: string): Promise<void> {
if (this.closed) {
throw new Error('Query is closed');
}
await this.sendControlRequest(ControlRequestType.SET_MODEL, { model }); await this.sendControlRequest(ControlRequestType.SET_MODEL, { model });
} }
@@ -853,10 +846,6 @@ export class Query implements AsyncIterable<SDKMessage> {
* @throws Error if query is closed * @throws Error if query is closed
*/ */
async supportedCommands(): Promise<Record<string, unknown> | null> { async supportedCommands(): Promise<Record<string, unknown> | null> {
if (this.closed) {
throw new Error('Query is closed');
}
return this.sendControlRequest(ControlRequestType.SUPPORTED_COMMANDS); return this.sendControlRequest(ControlRequestType.SUPPORTED_COMMANDS);
} }
@@ -867,10 +856,6 @@ export class Query implements AsyncIterable<SDKMessage> {
* @throws Error if query is closed * @throws Error if query is closed
*/ */
async mcpServerStatus(): Promise<Record<string, unknown> | null> { async mcpServerStatus(): Promise<Record<string, unknown> | null> {
if (this.closed) {
throw new Error('Query is closed');
}
return this.sendControlRequest(ControlRequestType.MCP_SERVER_STATUS); return this.sendControlRequest(ControlRequestType.MCP_SERVER_STATUS);
} }