2026-04-22 21:03:02 +03:00
|
|
|
import { EventEmitter } from 'node:events';
|
2026-04-20 15:41:15 +03:00
|
|
|
import { describe, expect, it } from 'vitest';
|
2026-04-17 16:07:26 +08:00
|
|
|
|
2026-04-22 21:03:02 +03:00
|
|
|
import { createGlobalUiEventBroadcaster, createMessageStreamWsRuntime } from './runtime.js';
|
|
|
|
|
|
|
|
|
|
class FakeSocket extends EventEmitter {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.readyState = 1;
|
|
|
|
|
this.sent = [];
|
|
|
|
|
this.closeCalls = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
send(payload) {
|
|
|
|
|
this.sent.push(JSON.parse(payload));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ping() {
|
|
|
|
|
void 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(code, reason) {
|
|
|
|
|
if (this.readyState === 3) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.readyState = 3;
|
|
|
|
|
this.closeCalls.push({ code, reason });
|
|
|
|
|
this.emit('close');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSseResponse({ blocks = [], signal, holdOpen = false }) {
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
body: {
|
|
|
|
|
getReader() {
|
|
|
|
|
return {
|
|
|
|
|
async read() {
|
|
|
|
|
if (index < blocks.length) {
|
|
|
|
|
const next = blocks[index++];
|
|
|
|
|
return { value: encoder.encode(next), done: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!holdOpen) {
|
|
|
|
|
return { value: undefined, done: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const onAbort = () => {
|
|
|
|
|
signal.removeEventListener('abort', onAbort);
|
|
|
|
|
const error = new Error('Aborted');
|
|
|
|
|
error.name = 'AbortError';
|
|
|
|
|
reject(error);
|
|
|
|
|
};
|
|
|
|
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-04-17 16:07:26 +08:00
|
|
|
|
|
|
|
|
describe('event stream broadcaster', () => {
|
|
|
|
|
it('fans out synthetic events to SSE and WS clients', () => {
|
|
|
|
|
const sseEvents = [];
|
|
|
|
|
const wsPayloads = [];
|
|
|
|
|
const sseClient = { id: 'sse-1' };
|
|
|
|
|
const wsClient = {
|
|
|
|
|
readyState: 1,
|
|
|
|
|
send(payload) {
|
|
|
|
|
wsPayloads.push(JSON.parse(payload));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const broadcast = createGlobalUiEventBroadcaster({
|
|
|
|
|
sseClients: new Set([sseClient]),
|
|
|
|
|
wsClients: new Set([wsClient]),
|
|
|
|
|
writeSseEvent(res, payload) {
|
|
|
|
|
sseEvents.push({ res, payload });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
broadcast({ type: 'openchamber:session-status' }, { eventId: 'evt-1', directory: '/tmp/project' });
|
|
|
|
|
|
|
|
|
|
expect(sseEvents).toEqual([
|
|
|
|
|
{
|
|
|
|
|
res: sseClient,
|
|
|
|
|
payload: { type: 'openchamber:session-status' },
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
expect(wsPayloads).toEqual([
|
|
|
|
|
{
|
|
|
|
|
type: 'event',
|
|
|
|
|
payload: { type: 'openchamber:session-status' },
|
|
|
|
|
eventId: 'evt-1',
|
|
|
|
|
directory: '/tmp/project',
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('removes websocket clients that fail to receive a payload', () => {
|
|
|
|
|
const wsClients = new Set([
|
|
|
|
|
{
|
|
|
|
|
readyState: 1,
|
|
|
|
|
send() {
|
|
|
|
|
throw new Error('socket write failed');
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const broadcast = createGlobalUiEventBroadcaster({
|
|
|
|
|
sseClients: new Set(),
|
|
|
|
|
wsClients,
|
|
|
|
|
writeSseEvent() {
|
|
|
|
|
throw new Error('should not be called');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
broadcast({ type: 'openchamber:notification' });
|
|
|
|
|
|
|
|
|
|
expect(wsClients.size).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-22 21:03:02 +03:00
|
|
|
|
|
|
|
|
describe('message stream websocket runtime', () => {
|
|
|
|
|
it('reconnects a stalled upstream SSE stream and resumes from the last event id', async () => {
|
|
|
|
|
const server = new EventEmitter();
|
|
|
|
|
const wsClients = new Set();
|
|
|
|
|
let triggerHealthCheckCalls = 0;
|
|
|
|
|
const fetchCalls = [];
|
|
|
|
|
let upstreamAttempt = 0;
|
|
|
|
|
|
|
|
|
|
const runtime = createMessageStreamWsRuntime({
|
|
|
|
|
server,
|
|
|
|
|
uiAuthController: null,
|
|
|
|
|
isRequestOriginAllowed: async () => true,
|
|
|
|
|
rejectWebSocketUpgrade() {
|
|
|
|
|
throw new Error('upgrade should not be used in this test');
|
|
|
|
|
},
|
|
|
|
|
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
|
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
|
|
|
processForwardedEventPayload() {},
|
|
|
|
|
wsClients,
|
|
|
|
|
triggerHealthCheck: () => {
|
|
|
|
|
triggerHealthCheckCalls += 1;
|
|
|
|
|
},
|
|
|
|
|
heartbeatIntervalMs: 50,
|
|
|
|
|
upstreamStallTimeoutMs: 20,
|
|
|
|
|
upstreamReconnectDelayMs: 0,
|
|
|
|
|
fetchImpl: async (_url, options) => {
|
|
|
|
|
const lastEventId = options?.headers?.['Last-Event-ID'] ?? null;
|
|
|
|
|
fetchCalls.push(lastEventId);
|
|
|
|
|
upstreamAttempt += 1;
|
|
|
|
|
|
|
|
|
|
if (upstreamAttempt === 1) {
|
|
|
|
|
return createSseResponse({
|
|
|
|
|
signal: options.signal,
|
|
|
|
|
holdOpen: true,
|
|
|
|
|
blocks: [
|
|
|
|
|
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return createSseResponse({
|
|
|
|
|
signal: options.signal,
|
|
|
|
|
holdOpen: false,
|
|
|
|
|
blocks: [
|
|
|
|
|
'id: evt-2\ndata: {"type":"server.connected","properties":{}}\n\n',
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const socket = new FakeSocket();
|
|
|
|
|
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 35));
|
|
|
|
|
|
|
|
|
|
const readyFrames = socket.sent.filter((frame) => frame.type === 'ready');
|
|
|
|
|
const eventFrames = socket.sent.filter((frame) => frame.type === 'event' && frame.payload?.type === 'server.connected');
|
|
|
|
|
|
|
|
|
|
expect(readyFrames).toHaveLength(1);
|
|
|
|
|
expect(eventFrames.length).toBeGreaterThanOrEqual(2);
|
|
|
|
|
expect(fetchCalls.slice(0, 2)).toEqual([null, 'evt-1']);
|
|
|
|
|
expect(triggerHealthCheckCalls).toBe(0);
|
|
|
|
|
|
|
|
|
|
socket.close();
|
|
|
|
|
await runtime.close();
|
|
|
|
|
});
|
|
|
|
|
});
|