Merge pull request #709: fix(agent): COD-549 preserve worker IDs after readiness failures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-27 02:11:29 +01:00
commit bd3320ff90
5 files changed
+120 -17

No files matched your search

+70
View File
@@ -30,6 +30,7 @@ import {
buildSendBody,
deleteRefusal,
describeFailure,
describeRequestError,
httpRequest,
inboxCell,
inputRefusal,
@@ -548,6 +549,75 @@ describe('agent spawn', () => {
expect(deps.calls).toHaveLength(1);
expect(deps.err.join('')).toMatch(/SESSION_BUSY/);
});
it.each([false, true])('preserves the created id when readiness throws (json=%s)', async (json) => {
const deps = fakeDeps((options) => {
if (options.path === '/api/v1/quick-start') return ok({ sessionId: OTHER, caseName: 'c' });
throw new Error('readiness connection reset');
}, json);
const exit = await agentSpawn(deps, { caseName: 'c', mode: 'claude', ready: true, timeoutMs: 1000 }).catch(
() => undefined
);
if (json) {
expect(deps.out).toHaveLength(1);
expect(JSON.parse(deps.out[0])).toEqual({
sessionId: OTHER,
caseName: 'c',
readinessError: 'readiness connection reset',
});
expect(deps.err).toEqual([]);
} else {
expect(deps.out).toEqual([OTHER]);
expect(deps.err.join('')).toContain('readiness check failed: readiness connection reset');
expect(deps.err.join('')).not.toMatch(/composer not seen|startup dialog/);
}
expect(exit).toBe(EXIT.error);
expect(deps.calls.map(({ method, path }) => ({ method, path }))).toEqual([
{ method: 'POST', path: '/api/v1/quick-start' },
{ method: 'GET', path: `/api/v1/sessions/${OTHER}/wait-output` },
]);
});
it.each([
{ error: new Error(''), kind: 'Error' },
{ error: '', kind: 'string' },
])('reports a readiness failure when the thrown $kind has no message', async ({ error }) => {
const deps = fakeDeps((options) => {
if (options.path === '/api/v1/quick-start') return ok({ sessionId: OTHER });
throw error;
}, true);
expect(await agentSpawn(deps, { caseName: 'c', mode: 'claude', ready: true, timeoutMs: 1000 })).toBe(EXIT.error);
expect(deps.out).toHaveLength(1);
expect(JSON.parse(deps.out[0])).toEqual({ sessionId: OTHER, readinessError: 'request failed without a message' });
expect(deps.calls).toHaveLength(2);
});
it('keeps the error code when a refused connection rejects with an empty AggregateError', async () => {
// Node 22 on a host resolving to ::1 and 127.0.0.1: message '', code ECONNREFUSED.
const refused = Object.assign(new AggregateError([new Error('connect ECONNREFUSED ::1:3000')], ''), {
code: 'ECONNREFUSED',
});
const deps = fakeDeps((options) => {
if (options.path === '/api/v1/quick-start') return ok({ sessionId: OTHER });
throw refused;
}, true);
expect(await agentSpawn(deps, { caseName: 'c', mode: 'claude', ready: true, timeoutMs: 1000 })).toBe(EXIT.error);
expect(JSON.parse(deps.out[0])).toEqual({ sessionId: OTHER, readinessError: 'ECONNREFUSED' });
});
});
describe('describeRequestError', () => {
it('prefers the message, then the code, then a generic line', () => {
expect(describeRequestError(new Error('socket hang up'))).toBe('socket hang up');
expect(describeRequestError(Object.assign(new AggregateError([], ''), { code: 'ECONNREFUSED' }))).toBe(
'ECONNREFUSED'
);
expect(describeRequestError(new Error(''))).toBe('request failed without a message');
expect(describeRequestError('')).toBe('request failed without a message');
});
});
describe('session id prefixes', () => {
+11
View File
@@ -248,6 +248,17 @@ describe('protocol: both eras', () => {
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('CODEMAN_API_URL');
});
it('a refused connection with an empty AggregateError still names the error code', async () => {
const refused: ApiRequest = async () => {
throw Object.assign(new AggregateError([new Error('connect ECONNREFUSED ::1:9')], ''), { code: 'ECONNREFUSED' });
};
const { out, send } = server(config(), { request: refused, calls: [] });
await send({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'list_sessions', arguments: {} } });
const result = (out[0] as { result: { isError: boolean; content: Array<{ text: string }> } }).result;
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Cannot reach Codeman at http://127.0.0.1:9: ECONNREFUSED.');
});
});
describe('tool definitions', () => {