mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
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>
This commit is contained in:
1 parent
9518f4ecec
commit
687c75b490
3 files changed
+63
No files matched your search
@@ -63,6 +63,7 @@ export const BROWSER_TEST_GLOBS = [
|
||||
'test/desktop-connect-page.browser.test.ts', // no server: the page is served by request interception
|
||||
'test/local-model-ui.browser.test.ts',
|
||||
'test/home-breakpoint-swap.browser.test.ts',
|
||||
'test/log-viewer-reconnect.browser.test.ts',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -6163,6 +6163,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
CodemanBase.url(`/api/sessions/${sessionId}/tail-file?path=${encodeURIComponent(filePath)}&lines=50`)
|
||||
);
|
||||
|
||||
let connected = false;
|
||||
eventSource.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const body = document.getElementById(`log-viewer-body-${windowId}`);
|
||||
@@ -6170,6 +6171,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
connected = true;
|
||||
body.innerHTML = '';
|
||||
break;
|
||||
case 'data':
|
||||
@@ -6189,6 +6191,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.updateLogViewerStatus(windowId, 'disconnected', 'ended');
|
||||
break;
|
||||
case 'error':
|
||||
if (!connected) eventSource.close();
|
||||
body.innerHTML += `<div class="log-error">${escapeHtml(data.error)}</div>`;
|
||||
this.updateLogViewerStatus(windowId, 'error', 'error');
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user