Files
openchamber/packages/web/server/lib/opencode/watcher.test.js
T

240 lines
6.8 KiB
JavaScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { createGlobalMessageStreamHub } from '../event-stream/global-hub.js';
import { createOpenCodeWatcherRuntime } from './watcher.js';
function createSseResponse({ blocks = [], signal, holdOpen = false }) {
const encoder = new TextEncoder();
let index = 0;
return {
ok: true,
status: 200,
body: {
getReader() {
return {
async read() {
if (index < blocks.length) {
return { value: encoder.encode(blocks[index++]), 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 });
});
},
};
},
},
};
}
describe('createOpenCodeWatcherRuntime', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('waits for OpenCode readiness and forwards unwrapped global SSE payloads', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {});
const payloads = [];
const fetchCalls = [];
const watcher = createOpenCodeWatcherRuntime({
waitForOpenCodePort: async () => {
await new Promise((resolve) => setTimeout(resolve, 1));
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({ Authorization: 'Bearer test-token' }),
onPayload(payload) {
payloads.push(payload);
watcher.stop();
},
fetchImpl: async (url, options) => {
fetchCalls.push({ url, headers: options.headers });
return createSseResponse({
signal: options.signal,
blocks: [
'id: evt-1\ndata: {"directory":"/tmp/project","payload":{"type":"session.updated","properties":{"sessionID":"ses_1"}}}\n\n',
],
});
},
});
await watcher.start();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(fetchCalls).toEqual([
{
url: 'http://127.0.0.1:4096/global/event',
headers: {
Accept: 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Authorization: 'Bearer test-token',
},
},
]);
expect(payloads).toEqual([
{
type: 'session.updated',
properties: {
sessionID: 'ses_1',
},
},
]);
});
it('resumes watcher reconnects with Last-Event-ID after a stalled upstream stream', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {});
const fetchLastEventIds = [];
const payloads = [];
let attempt = 0;
const watcher = createOpenCodeWatcherRuntime({
waitForOpenCodePort: async () => {},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
onPayload(payload) {
payloads.push(payload.type);
if (payload.type === 'session.updated') {
watcher.stop();
}
},
fetchImpl: async (_url, options) => {
fetchLastEventIds.push(options.headers['Last-Event-ID'] ?? null);
attempt += 1;
if (attempt === 1) {
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
}
return createSseResponse({
signal: options.signal,
blocks: [
'id: evt-2\ndata: {"type":"session.updated","properties":{}}\n\n',
],
});
},
upstreamStallTimeoutMs: 10,
upstreamReconnectDelayMs: 0,
});
await watcher.start();
await new Promise((resolve) => setTimeout(resolve, 30));
expect(payloads).toEqual(['server.connected', 'session.updated']);
expect(fetchLastEventIds.slice(0, 2)).toEqual([null, 'evt-1']);
});
it('subscribes to a shared global event hub instead of opening its own upstream stream', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {});
const payloads = [];
let hubFetchCalls = 0;
let watcherFetchCalls = 0;
const globalEventHub = createGlobalMessageStreamHub({
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
upstreamReconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
hubFetchCalls += 1;
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"payload":{"type":"session.updated","properties":{"sessionID":"ses_1"}}}\n\n',
],
});
},
});
const watcher = createOpenCodeWatcherRuntime({
waitForOpenCodePort: async () => {},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
globalEventHub,
onPayload(payload) {
payloads.push(payload);
watcher.stop();
},
fetchImpl: async () => {
watcherFetchCalls += 1;
throw new Error('watcher fetch should not be called');
},
});
await watcher.start();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(hubFetchCalls).toBe(1);
expect(watcherFetchCalls).toBe(0);
expect(payloads).toEqual([
{
type: 'session.updated',
properties: {
sessionID: 'ses_1',
},
},
]);
});
it('does not stop a shared global event hub when the watcher stops', async () => {
const events = new Set();
const statuses = new Set();
let startCalls = 0;
let stopCalls = 0;
const globalEventHub = {
start() {
startCalls += 1;
},
stop() {
stopCalls += 1;
},
subscribeEvent(subscriber) {
events.add(subscriber);
return () => {
events.delete(subscriber);
};
},
subscribeStatus(subscriber) {
statuses.add(subscriber);
return () => {
statuses.delete(subscriber);
};
},
};
const watcher = createOpenCodeWatcherRuntime({
waitForOpenCodePort: async () => {},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
globalEventHub,
onPayload() {},
});
await watcher.start();
watcher.stop();
expect(startCalls).toBe(1);
expect(stopCalls).toBe(0);
expect(events.size).toBe(0);
expect(statuses.size).toBe(0);
});
});