Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/cli/src/acp/acpFileSystemService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,57 @@ describe('AcpFileSystemService', () => {
message: 'Resource not found for document',
});
});

it.each([
{
name: 'snake_case "not_found"',
message: 'fs/read_text_file not_found: missing path',
},
{
name: 'phrase "file not found"',
message: 'agent: file not found at /tmp/x',
},
])(
'should throw normalized ENOENT for $name message variants',
async ({ message }) => {
service = new AcpFileSystemService(
mockConnection,
'session-1',
{ readTextFile: true, writeTextFile: true },
mockFallback,
'/path/to',
);
mockConnection.readTextFile.mockRejectedValue(new Error(message));

await expect(
service.readTextFile('/path/to/missing'),
).rejects.toMatchObject({
code: 'ENOENT',
message,
});
},
);

it('should throw normalized ENOENT when the ACP error carries a structured `code: "ENOENT"` field', async () => {
service = new AcpFileSystemService(
mockConnection,
'session-1',
{ readTextFile: true, writeTextFile: true },
mockFallback,
'/path/to',
);
const structured = Object.assign(new Error('opaque server message'), {
code: 'ENOENT',
});
mockConnection.readTextFile.mockRejectedValue(structured);

await expect(
service.readTextFile('/path/to/missing'),
).rejects.toMatchObject({
code: 'ENOENT',
message: 'opaque server message',
});
});
});

describe('writeTextFile', () => {
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/acp/acpFileSystemService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,27 @@ export class AcpFileSystemService implements FileSystemService {

private normalizeFileSystemError(err: unknown): never {
const errorMessage = err instanceof Error ? err.message : String(err);

// Structured signal first: a JSON-RPC error object's `code` field is the
// authoritative not-found indicator when the ACP server emits it. Falls
// back to substring matching below for servers that haven't migrated to
// structured error codes yet.
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
if (code === 'ENOENT') {
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
newErr.code = 'ENOENT';
throw newErr;
}
}
Comment on lines +54 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If err is a plain object, errorMessage will be initialized as '[object Object]' at line 37. When a structured error is detected, we should extract the message property. Additionally, per repository rules, we should log the detailed error for debugging and throw acp.RequestError with an appropriate JSON-RPC error code instead of a generic Error.

Suggested change
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
if (code === 'ENOENT') {
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
newErr.code = 'ENOENT';
throw newErr;
}
}
if (err && typeof err === 'object' && 'code' in err) {
const errObj = err as { code?: unknown; message?: unknown };
console.error('Detailed error:', err);
if (errObj.code === 'ENOENT') {
const msg = typeof errObj.message === 'string' ? errObj.message : errorMessage;
throw new acp.RequestError(-32602, msg);
}
}
References
  1. When catching exceptions, log the detailed error for debugging instead of providing only a generic error message.
  2. When handling JSON-RPC requests, throw acp.RequestError with an appropriate JSON-RPC error code instead of a generic Error for invalid parameters.


if (
errorMessage.includes('Resource not found') ||
errorMessage.includes('ENOENT') ||
errorMessage.includes('does not exist') ||
errorMessage.includes('No such file')
errorMessage.includes('No such file') ||
errorMessage.includes('not_found') ||
errorMessage.includes('file not found')
) {
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
newErr.code = 'ENOENT';
Expand Down