diff --git a/gui/tests/sidebar-version-browser.ts b/gui/tests/sidebar-version-browser.ts
index 59e4b01c433..b91caa317cb 100644
--- a/gui/tests/sidebar-version-browser.ts
+++ b/gui/tests/sidebar-version-browser.ts
@@ -2,7 +2,8 @@
* when Chrome/Chromium is not on PATH. No browser package or downloads required.
* The fixture uses the real bundled stylesheet and the App drawer/topbar markup;
* it intentionally does not connect to a user's proxy or credentials. */
-import { mkdtemp, readFile, rm, mkdir, writeFile } from "node:fs/promises";
+import { spawn } from "node:child_process";
+import { readFile, rm, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve, sep } from "node:path";
@@ -21,50 +22,47 @@ const css = await readFile(cssFile, "utf8");
const logo = `data:image/png;base64,${(await readFile(join(dist, "logo.png"))).toString("base64")}`;
const brand = ``;
const html = `
${brand}
`;
-const profile = await mkdtemp(join(tmpdir(), "ocx-sidebar-chrome-"));
-const browser = Bun.spawn([chrome, "--headless", "--disable-gpu", "--disable-background-networking",
- "--no-first-run", "--no-default-browser-check", "--remote-debugging-address=127.0.0.1",
- "--remote-debugging-port=0", `--user-data-dir=${profile}`,
+const profile = join(tmpdir(), `ocx-sidebar-chrome-${crypto.randomUUID()}`);
+const browser = spawn(chrome, ["--headless", "--disable-gpu", "--disable-background-networking",
+ "--no-first-run", "--no-default-browser-check", "--remote-debugging-pipe",
+ `--user-data-dir=${profile}`,
...(process.env.CHROME_NO_SANDBOX === "1" ? ["--no-sandbox"] : []), "about:blank"],
-{ stdout: "ignore", stderr: "ignore" });
-let socket: WebSocket | undefined;
+{ stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"] });
+const pipeWrite = browser.stdio[3];
+const pipeRead = browser.stdio[4];
+if (!pipeWrite || !pipeRead) throw new Error("Chrome did not create its private debugging pipe.");
+const exited = new Promise((done, fail) => {
+ browser.once("exit", () => done());
+ browser.once("error", fail);
+});
const delay = (ms: number) => new Promise(done => setTimeout(done, ms));
try {
- let debugPort = "";
- const deadline = Date.now() + 10_000;
- while (!debugPort && Date.now() < deadline) {
- try { debugPort = (await readFile(join(profile, "DevToolsActivePort"), "utf8")).split("\n")[0]; }
- catch { await delay(50); }
- }
- if (!/^\d+$/.test(debugPort)) throw new Error("Chrome did not expose its local debugging port within 10 seconds.");
- const response = await fetch(`http://127.0.0.1:${debugPort}/json/new?about:blank`, { method: "PUT", signal: AbortSignal.timeout(5_000) });
- if (!response.ok) throw new Error(`Cannot create browser target: ${response.status}`);
- const target = await response.json() as { webSocketDebuggerUrl: string };
- socket = new WebSocket(target.webSocketDebuggerUrl);
- const ws = socket;
- await new Promise((done, fail) => {
- const timer = setTimeout(() => fail(new Error("CDP connection timed out")), 5_000);
- ws.addEventListener("open", () => { clearTimeout(timer); done(); }, { once: true });
- ws.addEventListener("error", () => { clearTimeout(timer); fail(new Error("CDP connection failed")); }, { once: true });
- });
let id = 0;
const pending = new Map void; reject: (reason: Error) => void }>();
- ws.addEventListener("message", event => {
- const message = JSON.parse(String(event.data)) as { id?: number; result?: unknown; error?: { message: string } };
- if (message.id === undefined) return;
- const call = pending.get(message.id);
- if (!call) return;
- pending.delete(message.id);
- if (message.error) call.reject(new Error(message.error.message)); else call.resolve(message.result);
+ let buffered = Buffer.alloc(0);
+ pipeRead.on("data", (chunk: Buffer) => {
+ buffered = Buffer.concat([buffered, chunk]);
+ for (let boundary = buffered.indexOf(0); boundary >= 0; boundary = buffered.indexOf(0)) {
+ const message = JSON.parse(buffered.subarray(0, boundary).toString()) as { id?: number; result?: unknown; error?: { message: string } };
+ buffered = buffered.subarray(boundary + 1);
+ if (message.id === undefined) continue;
+ const call = pending.get(message.id);
+ if (!call) continue;
+ pending.delete(message.id);
+ if (message.error) call.reject(new Error(message.error.message)); else call.resolve(message.result);
+ }
});
- function cdp(method: string, params: Record = {}): Promise {
+ function callCdp(method: string, params: Record = {}, sessionId?: string): Promise {
return new Promise((done, fail) => {
const next = ++id;
const timer = setTimeout(() => { pending.delete(next); fail(new Error(`CDP timeout: ${method}`)); }, 5_000);
pending.set(next, { resolve: value => { clearTimeout(timer); done(value as T); }, reject: error => { clearTimeout(timer); fail(error); } });
- ws.send(JSON.stringify({ id: next, method, params }));
+ pipeWrite.write(`${JSON.stringify({ id: next, method, params, ...(sessionId ? { sessionId } : {}) })}\0`);
});
}
+ const { targetId } = await callCdp<{ targetId: string }>("Target.createTarget", { url: "about:blank" });
+ const { sessionId } = await callCdp<{ sessionId: string }>("Target.attachToTarget", { targetId, flatten: true });
+ const cdp = (method: string, params: Record = {}) => callCdp(method, params, sessionId);
async function evaluate(expression: string): Promise {
const result = await cdp<{ result: { value: T }; exceptionDetails?: unknown }>("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
if (result.exceptionDetails) throw new Error(`Browser evaluation failed: ${JSON.stringify(result.exceptionDetails)}`);
@@ -128,13 +126,12 @@ try {
}
}
}
- const version = await cdp("Browser.getVersion");
+ const version = await callCdp("Browser.getVersion");
await writeFile(join(output, "results.json"), JSON.stringify({ scope: "Real Chromium geometry with built production CSS; isolated App header markup, no live proxy", browser: version, cssPath, cssSha256: new Bun.CryptoHasher("sha256").update(css).digest("hex"), cases }, null, 2));
console.log(`PASS: ${cases.length} built-CSS browser cases; full version visible, badge bounded, no drawer-close overlap.`);
} finally {
- socket?.close();
browser.kill();
- await Promise.race([browser.exited, delay(2_000)]);
- if (browser.exitCode === null) { browser.kill("SIGKILL"); await browser.exited; }
+ await Promise.race([exited, delay(2_000)]);
+ if (browser.exitCode === null) { browser.kill("SIGKILL"); await exited; }
await rm(profile, { recursive: true, force: true });
}
diff --git a/gui/tests/sidebar-version-layout.test.ts b/gui/tests/sidebar-version-layout.test.ts
index 6be645179e4..10bda05bfbb 100644
--- a/gui/tests/sidebar-version-layout.test.ts
+++ b/gui/tests/sidebar-version-layout.test.ts
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test";
const css = await Bun.file(new URL("../src/styles/sidebar-brand.css", import.meta.url)).text();
const entry = await Bun.file(new URL("../src/main.tsx", import.meta.url)).text();
+const browserHarness = await Bun.file(new URL("./sidebar-version-browser.ts", import.meta.url)).text();
function block(selector: string): string {
const start = css.indexOf(`${selector} {`);
@@ -49,3 +50,9 @@ test("the fix stays scoped to the drawer and leaves compact topbar policies inta
.map(match => match[1].trim());
expect(selectors).toEqual([".drawer-head .brand", ".drawer-head .brand .ver"]);
});
+
+test("the opt-in browser harness keeps DevTools on a private process pipe", () => {
+ expect(browserHarness).toContain('"--remote-debugging-pipe"');
+ expect(browserHarness).not.toContain("--remote-debugging-port");
+ expect(browserHarness).not.toContain("--remote-debugging-address");
+});
diff --git a/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts b/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
index 41a61c21023..075e3c33186 100644
--- a/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
+++ b/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
@@ -12,6 +12,10 @@ const provider: OcxProviderConfig = {
baseUrl: "https://example.test/v1",
apiKey: "sk-test",
authMode: "key",
+ // The wire role folds to `system` unless a destination is recorded as accepting
+ // `developer`; this suite is about tool-result repair ordering, so it declares the
+ // destination rather than asserting the default.
+ foldDeveloperRoleToSystem: false,
};
interface ChatMsg {
diff --git a/tests/responses/chat-inline-document-bytes.test.ts b/tests/responses/chat-inline-document-bytes.test.ts
index dd88fa05788..a718c1c376a 100644
--- a/tests/responses/chat-inline-document-bytes.test.ts
+++ b/tests/responses/chat-inline-document-bytes.test.ts
@@ -28,6 +28,10 @@ const chatProvider: OcxProviderConfig = {
adapter: "openai-chat",
baseUrl: "https://gateway.example.internal/v1",
apiKey: "k",
+ // The wire role folds to `system` unless a destination is recorded as accepting
+ // `developer`; the document test asserts the role a turn keeps, so it declares the
+ // destination rather than asserting the default.
+ foldDeveloperRoleToSystem: false,
};
const anthropicProvider = {
adapter: "anthropic",