Merge pull request #1288 from BradGroux/fix/acp-teardown-write-1287

fix: contain ACP teardown write failures
This commit is contained in:
Brad Groux 2026-08-30 07:25:58 -05:00 • committed by GitHub
commit 3f83da36bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 8 deletions

View file

@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { AcpJsonRpcPeer } from '@veritas-kanban/shared';
describe('ACP JSON-RPC peer writes', () => {
it('contains transport teardown failures for detached request replies', async () => {
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on('unhandledRejection', onUnhandled);
try {
const peer = new AcpJsonRpcPeer({
write: async () => {
throw new Error('stream destroyed');
},
onRequest: async () => ({}),
});
peer.acceptChunk('{"jsonrpc":"2.0","id":1,"method":"session/request_permission"}\n');
await new Promise<void>((resolve) => setImmediate(resolve));
expect(unhandled).toEqual([]);
} finally {
process.off('unhandledRejection', onUnhandled);
}
});
it('keeps caller-awaited write failures observable', async () => {
const peer = new AcpJsonRpcPeer({
write: async () => {
throw new Error('stream destroyed');
},
});
await expect(peer.notify('session/cancel', {})).rejects.toThrow('stream destroyed');
});
});

View file

@ -76,7 +76,7 @@ export class AcpJsonRpcPeer {
try {
record = JSON.parse(line);
} catch {
void this.send(jsonRpcError(null, -32700, 'Parse error'));
this.sendDetached(jsonRpcError(null, -32700, 'Parse error'));
continue;
}
this.acceptRecord(record);
@ -117,7 +117,7 @@ export class AcpJsonRpcPeer {
}
if (typeof value.method !== 'string' || !value.method.trim()) {
if (id !== undefined) void this.send(jsonRpcError(id, -32600, 'Invalid Request'));
if (id !== undefined) this.sendDetached(jsonRpcError(id, -32600, 'Invalid Request'));
return;
}
if (id === undefined) {
@ -127,14 +127,18 @@ export class AcpJsonRpcPeer {
return;
}
if (!this.options.onRequest) {
void this.send(jsonRpcError(id, -32601, 'Method not found'));
this.sendDetached(jsonRpcError(id, -32601, 'Method not found'));
return;
}
void Promise.resolve(this.options.onRequest(value.method, value.params, id))
.then((result) => this.send({ jsonrpc: '2.0', id, result: result ?? {} }))
.catch((error) =>
this.send(jsonRpcError(id, -32603, boundedMessage(error, 'Internal error')))
);
void Promise.resolve(this.options.onRequest(value.method, value.params, id)).then(
(result) => this.sendDetached({ jsonrpc: '2.0', id, result: result ?? {} }),
(error) =>
this.sendDetached(jsonRpcError(id, -32603, boundedMessage(error, 'Internal error')))
);
}
private sendDetached(record: AcpJsonRpcMessage): void {
void this.send(record).catch(() => undefined);
}
private send(record: AcpJsonRpcMessage): Promise<void> {