fix(tests): resolve 4 post-phase test failures
- platformClients.integration: iOS/Android push tests lacked setStatus() call before listNodes(), so platform filter excluded nodes. Added publishHeartbeat() to set platform on connection state. - server.test: agent.send now emits run_state events before done (Phase 1). Added sendAndWaitForDone() helper and updated test to find done event rather than assuming index 0. - handlers.test: updated agent.send/cancel assertions to use find() and pass send arg to agent.cancel, consistent with run_state events. - httpBody: req.destroy() closed socket before 413 response could be sent. Removed socket destruction from body reader; 413 responses now send Connection: close so Node closes the connection cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,7 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
} catch (err) {
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
auditLogger?.webhookDenied(webhookName, `Payload too large (>${this.maxRequestBodyBytes} bytes)`);
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -396,6 +396,7 @@ describe('platform clients integration', () => {
|
||||
|
||||
try {
|
||||
await client.register();
|
||||
await client.publishHeartbeat();
|
||||
const push = await client.registerPushToken({
|
||||
token: 'd'.repeat(64),
|
||||
topic: 'dev.flynn.ios',
|
||||
@@ -424,6 +425,7 @@ describe('platform clients integration', () => {
|
||||
|
||||
try {
|
||||
await client.register();
|
||||
await client.publishHeartbeat();
|
||||
const push = await client.registerPushToken('e'.repeat(64));
|
||||
|
||||
expect(push.updated).toBe(true);
|
||||
|
||||
@@ -212,7 +212,8 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
|
||||
command: parsedCommand,
|
||||
});
|
||||
|
||||
const isCommand = Boolean(commandInput && deps.commandRegistry?.isCommand(commandInput));
|
||||
const commandRegistry = deps.commandRegistry;
|
||||
const isCommand = Boolean(commandInput && commandRegistry?.isCommand(commandInput));
|
||||
if (!isCommand) {
|
||||
activeRequestIds.set(connectionId, request.id);
|
||||
auditLogger?.runState?.({
|
||||
@@ -231,8 +232,11 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
|
||||
}
|
||||
|
||||
if (isCommand) {
|
||||
if (!commandRegistry) {
|
||||
throw new Error('Command registry is not available');
|
||||
}
|
||||
const sessionId = deps.sessionBridge.getSessionId(connectionId);
|
||||
const commandResult = await deps.commandRegistry.execute(commandInput, {
|
||||
const commandResult = await commandRegistry.execute(commandInput, {
|
||||
channel: 'ws',
|
||||
senderId: connectionId,
|
||||
sessionId: sessionId ?? `ws:${connectionId}`,
|
||||
|
||||
@@ -1146,8 +1146,11 @@ describe('agent handlers', () => {
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
expect(mockAgent.process).toHaveBeenCalledWith('hello', undefined);
|
||||
expect(sent).toHaveLength(1);
|
||||
const doneEvent = sent[0] as GatewayEvent;
|
||||
const doneEvent = sent.find((msg) => (msg as GatewayEvent).event === 'done') as GatewayEvent | undefined;
|
||||
expect(doneEvent).toBeTruthy();
|
||||
if (!doneEvent) {
|
||||
throw new Error('done event not emitted');
|
||||
}
|
||||
expect(doneEvent.event).toBe('done');
|
||||
expect(getPath(doneEvent.data, 'content')).toBe('response text');
|
||||
});
|
||||
@@ -1171,7 +1174,11 @@ describe('agent handlers', () => {
|
||||
{ mimeType: 'image/png', data: 'iVBOR...', url: undefined, filename: 'screenshot.png' },
|
||||
{ mimeType: 'application/pdf', data: undefined, url: 'https://example.com/doc.pdf', filename: undefined },
|
||||
]);
|
||||
const doneEvent = sent[0] as GatewayEvent;
|
||||
const doneEvent = sent.find((msg) => (msg as GatewayEvent).event === 'done') as GatewayEvent | undefined;
|
||||
expect(doneEvent).toBeTruthy();
|
||||
if (!doneEvent) {
|
||||
throw new Error('done event not emitted');
|
||||
}
|
||||
expect(doneEvent.event).toBe('done');
|
||||
});
|
||||
|
||||
@@ -1187,7 +1194,8 @@ describe('agent handlers', () => {
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
expect(mockAgent.process).toHaveBeenCalledWith('hi', []);
|
||||
expect(sent).toHaveLength(1);
|
||||
const doneEvent = sent.find((msg) => (msg as GatewayEvent).event === 'done');
|
||||
expect(doneEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('agent.send accepts attachment-only requests', async () => {
|
||||
@@ -1207,8 +1215,8 @@ describe('agent handlers', () => {
|
||||
expect(mockAgent.process).toHaveBeenCalledWith('', [
|
||||
{ mimeType: 'image/png', data: 'iVBOR...', url: undefined, filename: undefined },
|
||||
]);
|
||||
expect(sent).toHaveLength(1);
|
||||
expect((sent[0] as GatewayEvent).event).toBe('done');
|
||||
const doneEvent = sent.find((msg) => (msg as GatewayEvent).event === 'done');
|
||||
expect(doneEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('agent.send requires message or attachments', async () => {
|
||||
@@ -1247,10 +1255,8 @@ describe('agent handlers', () => {
|
||||
await Promise.all([p1, p2]);
|
||||
|
||||
// Both should have completed — no AgentBusy error
|
||||
expect(sent1).toHaveLength(1);
|
||||
expect((sent1[0] as GatewayEvent).event).toBe('done');
|
||||
expect(sent2).toHaveLength(1);
|
||||
expect((sent2[0] as GatewayEvent).event).toBe('done');
|
||||
expect(sent1.find((msg) => (msg as GatewayEvent).event === 'done')).toBeTruthy();
|
||||
expect(sent2.find((msg) => (msg as GatewayEvent).event === 'done')).toBeTruthy();
|
||||
expect(mockAgent.process).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -1262,7 +1268,11 @@ describe('agent handlers', () => {
|
||||
|
||||
await handlers['agent.send'](req, send);
|
||||
|
||||
const errorEvent = sent[0] as GatewayEvent;
|
||||
const errorEvent = sent.find((msg) => (msg as GatewayEvent).event === 'error') as GatewayEvent | undefined;
|
||||
expect(errorEvent).toBeTruthy();
|
||||
if (!errorEvent) {
|
||||
throw new Error('error event not emitted');
|
||||
}
|
||||
expect(errorEvent.event).toBe('error');
|
||||
expect(getPath(errorEvent.data, 'message')).toBe('model failed');
|
||||
});
|
||||
@@ -1291,7 +1301,7 @@ describe('agent handlers', () => {
|
||||
it('agent.cancel returns cancelled state', async () => {
|
||||
mockBridge.cancel.mockReturnValue(true);
|
||||
const req: GatewayRequest = { id: 7, method: 'agent.cancel', params: { connectionId: 'conn-1' } };
|
||||
const result = await handlers['agent.cancel'](req) as GatewayResponse;
|
||||
const result = await handlers['agent.cancel'](req, vi.fn()) as GatewayResponse;
|
||||
|
||||
expect(getPath(result.result, 'cancelled')).toBe(true);
|
||||
expect(getPath(result.result, 'message')).toContain('Cancellation requested');
|
||||
@@ -1301,7 +1311,7 @@ describe('agent handlers', () => {
|
||||
it('agent.cancel returns not-cancelled when no active operation exists', async () => {
|
||||
mockBridge.cancel.mockReturnValue(false);
|
||||
const req: GatewayRequest = { id: 8, method: 'agent.cancel', params: { connectionId: 'conn-1' } };
|
||||
const result = await handlers['agent.cancel'](req) as GatewayResponse;
|
||||
const result = await handlers['agent.cancel'](req, vi.fn()) as GatewayResponse;
|
||||
|
||||
expect(getPath(result.result, 'cancelled')).toBe(false);
|
||||
expect(getPath(result.result, 'message')).toContain('No active operation');
|
||||
|
||||
@@ -102,6 +102,24 @@ function sendAndReceiveAll(ws: WebSocket, msg: object, count: number): Promise<A
|
||||
});
|
||||
}
|
||||
|
||||
/** Send a request and collect messages until a terminal event ('done' or 'error') is received. */
|
||||
function sendAndWaitForDone(ws: WebSocket, msg: object): Promise<Array<GatewayResponse | GatewayError | GatewayEvent>> {
|
||||
return new Promise((resolve) => {
|
||||
const messages: Array<GatewayResponse | GatewayError | GatewayEvent> = [];
|
||||
const handler = (data: Buffer) => {
|
||||
const parsed = JSON.parse(data.toString()) as GatewayResponse | GatewayError | GatewayEvent;
|
||||
messages.push(parsed);
|
||||
const event = parsed as GatewayEvent;
|
||||
if (event.event === 'done' || event.event === 'error') {
|
||||
ws.off('message', handler);
|
||||
resolve(messages);
|
||||
}
|
||||
};
|
||||
ws.on('message', handler);
|
||||
ws.send(JSON.stringify(msg));
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
LISTEN_ALLOWED = await canListenOnLocalhost();
|
||||
});
|
||||
@@ -231,9 +249,10 @@ describe('GatewayServer integration', () => {
|
||||
}
|
||||
const ws = await createClient();
|
||||
try {
|
||||
// agent.send streams events — we expect a 'done' event
|
||||
const messages = await sendAndReceiveAll(ws, { id: 4, method: 'agent.send', params: { message: 'hi' } }, 1);
|
||||
const doneEvent = messages[0] as GatewayEvent;
|
||||
// agent.send streams events (run_state + done) — wait for terminal done event
|
||||
const messages = await sendAndWaitForDone(ws, { id: 4, method: 'agent.send', params: { message: 'hi' } });
|
||||
const doneEvent = messages.find((m) => (m as GatewayEvent).event === 'done') as GatewayEvent;
|
||||
expect(doneEvent).toBeTruthy();
|
||||
expect(doneEvent.id).toBe(4);
|
||||
expect(doneEvent.event).toBe('done');
|
||||
expect((doneEvent.data as { content?: string }).content).toBe('Hello from Flynn!');
|
||||
|
||||
@@ -823,7 +823,7 @@ export class GatewayServer {
|
||||
rawBody = await this.readRequestBody(req);
|
||||
} catch (err) {
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
return true;
|
||||
}
|
||||
@@ -887,7 +887,7 @@ export class GatewayServer {
|
||||
rawBody = await this.readRequestBody(req);
|
||||
} catch (err) {
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
return true;
|
||||
}
|
||||
@@ -974,7 +974,7 @@ export class GatewayServer {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (err) {
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
} else {
|
||||
console.error('Gmail push handler error:', err instanceof Error ? err.message : err);
|
||||
|
||||
@@ -248,8 +248,11 @@ describe('ChatPage wiring', () => {
|
||||
await Promise.resolve();
|
||||
|
||||
const statusLine = Array.from(root.querySelectorAll('div'))
|
||||
.find((el: any) => String(el.textContent ?? '').startsWith('Run status:'));
|
||||
.find((el: any) => String(el.textContent ?? '').startsWith('Run status:')) as any;
|
||||
expect(statusLine).toBeTruthy();
|
||||
if (!statusLine) {
|
||||
throw new Error('Run status line not found');
|
||||
}
|
||||
expect(statusLine.classList.contains('hidden')).toBe(false);
|
||||
|
||||
resolveResult?.({ content: 'ok' });
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('readRequestBody', () => {
|
||||
expect(req.destroyed).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects oversized body and destroys request', async () => {
|
||||
it('rejects oversized body with RequestBodyTooLargeError', async () => {
|
||||
const req = new MockRequest();
|
||||
const bodyPromise = readRequestBody(asIncoming(req), { maxBytes: 5 });
|
||||
|
||||
@@ -37,6 +37,5 @@ describe('readRequestBody', () => {
|
||||
req.emit('data', Buffer.from('6'));
|
||||
|
||||
await expect(bodyPromise).rejects.toBeInstanceOf(RequestBodyTooLargeError);
|
||||
expect(req.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,9 +40,6 @@ export function readRequestBody(req: IncomingMessage, opts: ReadRequestBodyOptio
|
||||
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
totalBytes += buf.length;
|
||||
if (totalBytes > opts.maxBytes) {
|
||||
if (typeof req.destroy === 'function') {
|
||||
req.destroy();
|
||||
}
|
||||
fail(new RequestBodyTooLargeError(opts.maxBytes, totalBytes));
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user