Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,10 @@ domain's DNS names `beboite.github.io`.

## Pull request reviews

`.coderabbit.yaml` enables advisory reviews and chat replies, excluding drafts
and generated artifacts. CodeRabbit is not a required check. Its installed App,
plan eligibility and quotas are external prerequisites.
`.coderabbit.yaml` enables advisory reviews and chat replies, drafts included
(`auto_review.drafts`), and skips generated artifacts. CodeRabbit is not a
required check. Its installed App, plan eligibility and quotas are external
prerequisites.

Follow the [PR rules](../AGENTS.md#follow-through-on-pull-requests): inspect CI
and review findings together, verify and group fixes before pushing, and report
Expand Down
5 changes: 0 additions & 5 deletions scripts/ci/quarantine.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@
"test": "thread jobs of forgotten threads > an id minted per call leaves no Job Object behind once the registry forgets it",
"reason": "failed 4 first attempts that passed on rerun (Windows), 2026-10-16 to 10-18"
},
{
"file": "tests/e2e/coordination-bridge-live.test.ts",
"test": "the owner app relays signed reads, preserves directional grants on reload, and displays actual turn activity",
"reason": "failed 8 first attempts that passed on rerun, its lazy UI chunks getting no response for 15 s, 2026-10-16 to 10-18"
},
{
"file": "tests/e2e/drafts-overlays.test.ts",
"test": "drafts survive reload and composer menus stay above the chrome",
Expand Down
76 changes: 75 additions & 1 deletion tests/e2e/cdp-attach.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { createServer, type Socket } from 'node:net';
import { createServer as createHttpServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { createServer, type AddressInfo, type Socket } from 'node:net';
import type { ServerWebSocket } from 'bun';
import { expect, test } from 'bun:test';
import { BrowserPage, freePort, probeDebuggingPort } from './lib/cdp.ts';
import { requestLog } from './lib/ui-requests.ts';

test.each(['ready', 'delayed', 'explicit-blank'] as const)('attach chooses the requested committed target (%s)', async mode => {
let lists = 0;
Expand Down Expand Up @@ -29,6 +32,77 @@ test.each(['ready', 'delayed', 'explicit-blank'] as const)('attach chooses the r
} finally { await page?.close(); await server.stop(true); }
});

test('a devtools message that is not JSON fails the waiting call at once and every later one, naming it', async () => {
// What Bun's WebSocket client handed over once it lost frame alignment: a
// piece of a frame. Dropped in silence, the reply inside it never came and
// the call waited out its 20 s like a page with a busy main thread.
const server = Bun.serve({ hostname: '127.0.0.1', port: 0,
fetch(request, self) {
if (new URL(request.url).pathname === '/json/list') return Response.json([{ type: 'page', url: 'http://tauri.localhost/', webSocketDebuggerUrl: `ws://127.0.0.1:${self.port}/app` }]);
return self.upgrade(request) ? undefined : new Response('upgrade required');
}, websocket: {
message(socket, raw) {
const frame = JSON.parse(String(raw)) as { id: number; method: string; params?: { expression?: string } };
if (frame.params?.expression === 'garbled') socket.send(`{"id":${frame.id},"result":{"res`);
else socket.send(JSON.stringify({ id: frame.id, result: frame.method === 'Runtime.evaluate' ? { result: { value: false } } : {} }));
},
},
});
let page: BrowserPage | undefined;
try {
page = await BrowserPage.attach(server.port!);
const started = performance.now();
const failure = await page.evaluate('garbled').then(() => 'answered', (error: unknown) => String(error));
expect(failure).toContain('devtools sent a message that is not JSON (');
expect(failure).toContain('{\\"res');
expect(performance.now() - started).toBeLessThan(2_000);
expect(await page.evaluate('1').then(() => 'answered', (error: unknown) => String(error))).toContain('not JSON');
} finally { await page?.close(); await server.stop(true); }
}, 10_000);

test('requests in flight say what each UI server did with them, timed from when Chrome sent them', async () => {
let middleware: ((request: IncomingMessage, response: ServerResponse, next: () => void) => void) | undefined;
requestLog.configureServer({ middlewares: { use(handler) { middleware = handler; } } });
const ui = createHttpServer((request, response) => middleware!(request, response, () => response.end('export {}')));
await new Promise<void>((resolve) => ui.listen(0, '127.0.0.1', resolve));
const base = `http://127.0.0.1:${(ui.address() as AddressInfo).port}`;
// A second UI server that never gets its request: known from the moment it listens.
const idle = createHttpServer((_request, response) => response.end());
requestLog.configurePreviewServer({ middlewares: { use() {} }, httpServer: idle });
await new Promise<void>((resolve) => idle.listen(0, '127.0.0.1', resolve));
const idleBase = `http://127.0.0.1:${(idle.address() as AddressInfo).port}`;
let devtools: ServerWebSocket<unknown> | undefined;
const server = Bun.serve({ hostname: '127.0.0.1', port: 0,
fetch(request, self) {
if (new URL(request.url).pathname === '/json/list') return Response.json([{ type: 'page', url: 'http://tauri.localhost/', webSocketDebuggerUrl: `ws://127.0.0.1:${self.port}/app` }]);
return self.upgrade(request) ? undefined : new Response('upgrade required');
}, websocket: {
open(socket) { devtools = socket; },
message(socket, raw) {
const frame = JSON.parse(String(raw)) as { id: number; method: string };
socket.send(JSON.stringify({ id: frame.id, result: frame.method === 'Runtime.evaluate' ? { result: { value: false } } : {} }));
},
},
});
let page: BrowserPage | undefined;
try {
page = await BrowserPage.attach(server.port!);
const sent = Date.now();
for (const chunk of ['RightPanel.js', 'CommandPalette.js']) expect(await (await fetch(`${base}/assets/${chunk}`)).text()).toBe('export {}');
// A loaded runner: the server answered both before devtools reported them.
await Bun.sleep(30);
const willBeSent = (requestId: string, url: string, wallTime?: number) => JSON.stringify({ method: 'Network.requestWillBeSent', params: { requestId, loaderId: 'l', frameId: 'f', wallTime, request: { url } } });
devtools!.send(willBeSent('1', `${base}/assets/RightPanel.js`, sent / 1_000));
devtools!.send(willBeSent('2', `${base}/assets/CommandPalette.js`));
devtools!.send(willBeSent('3', `${idleBase}/assets/SettingsShell.js`, sent / 1_000));
const failure = await page.waitFor('false', 1_300).then(() => 'answered', (error: unknown) => String(error));
const line = (chunk: string) => failure.split('","').find((entry) => entry.includes(chunk)) ?? `no line for ${chunk} in ${failure}`;
expect(line('RightPanel.js')).toMatch(/; the UI server answered 200 \d+ ms after it was sent/);
expect(line('CommandPalette.js')).toMatch(/; no send time; newest record: the UI server answered 200 \d+ ms before devtools reported it/);
expect(line('SettingsShell.js')).toMatch(/; the UI server never got it/);
} finally { await page?.close(); await server.stop(true); ui.close(); idle.close(); }
}, 10_000);

test('the failure probe tells a closed port at once from one that accepts and never answers', async () => {
const closed = await freePort();
let startedAt = Date.now();
Expand Down
Loading
Loading