Files
Codeman/test/log-viewer-reconnect.browser.test.ts
T
687c75b490 Merge #720: fix(log-viewer): stop reconnecting after a startup error
The floating log viewer reconnected every few seconds when its tail-file
stream ended with a startup error (path refused, file missing, too large,
stream limit, tail spawn failure). It now closes the EventSource on an
`error` message that arrives before `connected`; an established stream
keeps its reconnect.

Squashed from #720 (johoja12). Maintainer edits while landing, as the
review said: the onerror close is dropped (it stopped the browser's own
retry after a transport failure, leaving the window on "Connecting"),
the test's check for a button nothing renders is gone, and the stubbed
stream sets `retry: 200` so a reconnect, if one came, shows within the
shorter wait.

Co-authored-by: Codeman maintainer <kay@stutz.science>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:58:33 +01:00

60 lines
2.1 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { chromium, type Browser } from 'playwright';
import { WebServer } from '../src/web/server.js';
const id = 'log-reconnect-fixture',
path = '/workspace/demo.log';
describe('log viewer pre-connect failures', () => {
let server: WebServer, browser: Browser;
beforeAll(async () => {
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
}, 60000);
afterAll(async () => {
await browser?.close();
await server?.stop();
});
it.each([
'Path must be within working directory or allowed log directories',
'File does not exist',
'File not found or not accessible',
'File too large',
'Maximum 5 concurrent streams per session',
'Failed to spawn tail process',
])('does not reconnect after %s before connected', async (error) => {
const page = await browser.newPage(),
errors: string[] = [];
let requests = 0;
page.on('pageerror', (e) => errors.push(e.message));
await page.route(`**/api/sessions/${id}/tail-file?**`, async (route) => {
requests++;
return route.fulfill({
status: 200,
contentType: 'text/event-stream',
// retry: 200 makes a reconnect, if one came, show up within 200 ms.
body: `retry: 200\ndata: ${JSON.stringify({ type: 'error', error })}\n\n`,
});
});
try {
await page.goto(`http://127.0.0.1:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.openLogViewerWindow);
await page.waitForTimeout(3500);
await page.evaluate(
({ id, path }) => {
const app = (window as any).app;
app.sessions.set(id, { id, mode: 'shell', workingDir: '/workspace' });
app.openLogViewerWindow(path, id);
},
{ id, path }
);
await expect.poll(() => page.locator('.log-viewer-window').innerText()).toContain(error);
await page.waitForTimeout(1000);
expect(requests).toBe(1);
expect(errors).toEqual([]);
} finally {
await page.close();
}
});
});