mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
Merge branch 'main' into feat/extension
This commit is contained in:
commit
140e8c926d
12 changed files with 200 additions and 60 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { z } from 'zod';
|
||||
import * as schema from './schema.js';
|
||||
import { ACP_ERROR_CODES } from './errorCodes.js';
|
||||
export * from './schema.js';
|
||||
|
||||
import type { WritableStream, ReadableStream } from 'node:stream/web';
|
||||
|
|
@ -349,27 +350,51 @@ export class RequestError extends Error {
|
|||
}
|
||||
|
||||
static parseError(details?: string): RequestError {
|
||||
return new RequestError(-32700, 'Parse error', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.PARSE_ERROR,
|
||||
'Parse error',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
static invalidRequest(details?: string): RequestError {
|
||||
return new RequestError(-32600, 'Invalid request', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.INVALID_REQUEST,
|
||||
'Invalid request',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
static methodNotFound(details?: string): RequestError {
|
||||
return new RequestError(-32601, 'Method not found', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.METHOD_NOT_FOUND,
|
||||
'Method not found',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
static invalidParams(details?: string): RequestError {
|
||||
return new RequestError(-32602, 'Invalid params', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.INVALID_PARAMS,
|
||||
'Invalid params',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
static internalError(details?: string): RequestError {
|
||||
return new RequestError(-32603, 'Internal error', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.INTERNAL_ERROR,
|
||||
'Internal error',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
static authRequired(details?: string): RequestError {
|
||||
return new RequestError(-32000, 'Authentication required', details);
|
||||
return new RequestError(
|
||||
ACP_ERROR_CODES.AUTH_REQUIRED,
|
||||
'Authentication required',
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
toResult<T>(): Result<T> {
|
||||
|
|
|
|||
25
packages/cli/src/acp-integration/errorCodes.ts
Normal file
25
packages/cli/src/acp-integration/errorCodes.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const ACP_ERROR_CODES = {
|
||||
// Parse error: invalid JSON received by server.
|
||||
PARSE_ERROR: -32700,
|
||||
// Invalid request: JSON is not a valid Request object.
|
||||
INVALID_REQUEST: -32600,
|
||||
// Method not found: method does not exist or is unavailable.
|
||||
METHOD_NOT_FOUND: -32601,
|
||||
// Invalid params: invalid method parameter(s).
|
||||
INVALID_PARAMS: -32602,
|
||||
// Internal error: implementation-defined server error.
|
||||
INTERNAL_ERROR: -32603,
|
||||
// Authentication required: must authenticate before operation.
|
||||
AUTH_REQUIRED: -32000,
|
||||
// Resource not found: e.g. missing file.
|
||||
RESOURCE_NOT_FOUND: -32002,
|
||||
} as const;
|
||||
|
||||
export type AcpErrorCode =
|
||||
(typeof ACP_ERROR_CODES)[keyof typeof ACP_ERROR_CODES];
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { FileSystemService } from '@qwen-code/qwen-code-core';
|
||||
import { AcpFileSystemService } from './filesystem.js';
|
||||
import { ACP_ERROR_CODES } from '../errorCodes.js';
|
||||
|
||||
const createFallback = (): FileSystemService => ({
|
||||
readTextFile: vi.fn(),
|
||||
|
|
@ -16,11 +17,13 @@ const createFallback = (): FileSystemService => ({
|
|||
|
||||
describe('AcpFileSystemService', () => {
|
||||
describe('readTextFile ENOENT handling', () => {
|
||||
it('parses path from ACP ENOENT message (quoted)', async () => {
|
||||
it('converts RESOURCE_NOT_FOUND error to ENOENT', async () => {
|
||||
const resourceNotFoundError = {
|
||||
code: ACP_ERROR_CODES.RESOURCE_NOT_FOUND,
|
||||
message: 'File not found',
|
||||
};
|
||||
const client = {
|
||||
readTextFile: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ content: 'ERROR: ENOENT: "/remote/file.txt"' }),
|
||||
readTextFile: vi.fn().mockRejectedValue(resourceNotFoundError),
|
||||
} as unknown as import('../acp.js').Client;
|
||||
|
||||
const svc = new AcpFileSystemService(
|
||||
|
|
@ -30,15 +33,20 @@ describe('AcpFileSystemService', () => {
|
|||
createFallback(),
|
||||
);
|
||||
|
||||
await expect(svc.readTextFile('/local/file.txt')).rejects.toMatchObject({
|
||||
await expect(svc.readTextFile('/some/file.txt')).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
path: '/remote/file.txt',
|
||||
errno: -2,
|
||||
path: '/some/file.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to requested path when none provided', async () => {
|
||||
it('re-throws other errors unchanged', async () => {
|
||||
const otherError = {
|
||||
code: ACP_ERROR_CODES.INTERNAL_ERROR,
|
||||
message: 'Internal error',
|
||||
};
|
||||
const client = {
|
||||
readTextFile: vi.fn().mockResolvedValue({ content: 'ERROR: ENOENT:' }),
|
||||
readTextFile: vi.fn().mockRejectedValue(otherError),
|
||||
} as unknown as import('../acp.js').Client;
|
||||
|
||||
const svc = new AcpFileSystemService(
|
||||
|
|
@ -48,12 +56,34 @@ describe('AcpFileSystemService', () => {
|
|||
createFallback(),
|
||||
);
|
||||
|
||||
await expect(
|
||||
svc.readTextFile('/fallback/path.txt'),
|
||||
).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
path: '/fallback/path.txt',
|
||||
await expect(svc.readTextFile('/some/file.txt')).rejects.toMatchObject({
|
||||
code: ACP_ERROR_CODES.INTERNAL_ERROR,
|
||||
message: 'Internal error',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses fallback when readTextFile capability is disabled', async () => {
|
||||
const client = {
|
||||
readTextFile: vi.fn(),
|
||||
} as unknown as import('../acp.js').Client;
|
||||
|
||||
const fallback = createFallback();
|
||||
(fallback.readTextFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
'fallback content',
|
||||
);
|
||||
|
||||
const svc = new AcpFileSystemService(
|
||||
client,
|
||||
'session-3',
|
||||
{ readTextFile: false, writeTextFile: true },
|
||||
fallback,
|
||||
);
|
||||
|
||||
const result = await svc.readTextFile('/some/file.txt');
|
||||
|
||||
expect(result).toBe('fallback content');
|
||||
expect(fallback.readTextFile).toHaveBeenCalledWith('/some/file.txt');
|
||||
expect(client.readTextFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import type { FileSystemService } from '@qwen-code/qwen-code-core';
|
||||
import type * as acp from '../acp.js';
|
||||
import { ACP_ERROR_CODES } from '../errorCodes.js';
|
||||
|
||||
/**
|
||||
* ACP client-based implementation of FileSystemService
|
||||
|
|
@ -23,25 +24,31 @@ export class AcpFileSystemService implements FileSystemService {
|
|||
return this.fallback.readTextFile(filePath);
|
||||
}
|
||||
|
||||
const response = await this.client.readTextFile({
|
||||
path: filePath,
|
||||
sessionId: this.sessionId,
|
||||
line: null,
|
||||
limit: null,
|
||||
});
|
||||
let response: { content: string };
|
||||
try {
|
||||
response = await this.client.readTextFile({
|
||||
path: filePath,
|
||||
sessionId: this.sessionId,
|
||||
line: null,
|
||||
limit: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorCode =
|
||||
typeof error === 'object' && error !== null && 'code' in error
|
||||
? (error as { code?: unknown }).code
|
||||
: undefined;
|
||||
|
||||
if (response.content.startsWith('ERROR: ENOENT:')) {
|
||||
// Treat ACP error strings as structured ENOENT errors without
|
||||
// assuming a specific platform format.
|
||||
const match = /^ERROR:\s*ENOENT:\s*(?<path>.*)$/i.exec(response.content);
|
||||
const err = new Error(response.content) as NodeJS.ErrnoException;
|
||||
err.code = 'ENOENT';
|
||||
err.errno = -2;
|
||||
const rawPath = match?.groups?.['path']?.trim();
|
||||
err['path'] = rawPath
|
||||
? rawPath.replace(/^['"]|['"]$/g, '') || filePath
|
||||
: filePath;
|
||||
throw err;
|
||||
if (errorCode === ACP_ERROR_CODES.RESOURCE_NOT_FOUND) {
|
||||
const err = new Error(
|
||||
`File not found: ${filePath}`,
|
||||
) as NodeJS.ErrnoException;
|
||||
err.code = 'ENOENT';
|
||||
err.errno = -2;
|
||||
err.path = filePath;
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.content;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue