From 68b9d3296e9899695c6cfb5ddd085a433a77a7b3 Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:17:40 +0900 Subject: [PATCH 1/4] fix(tests): avoid released loopback port reservations --- .../claude-picker-runtime.test.ts | 128 ++++++++++-------- tests/cli/cli-client-start-owner-e2e.test.ts | 8 +- tests/cli/cli-export-command.test.ts | 8 +- tests/cli/cli-start-auxiliary-bind.test.ts | 48 ++++--- tests/cli/ocx-launcher-runtime.test.ts | 28 ++-- tests/cli/sibling-home-client-sync.test.ts | 50 +++---- 6 files changed, 132 insertions(+), 138 deletions(-) diff --git a/tests/claude-integration/claude-picker-runtime.test.ts b/tests/claude-integration/claude-picker-runtime.test.ts index 0bf6303053f..53ed6345db9 100644 --- a/tests/claude-integration/claude-picker-runtime.test.ts +++ b/tests/claude-integration/claude-picker-runtime.test.ts @@ -724,68 +724,76 @@ describe("startClaudeIntercept wiring", () => { }); test("a busy picker port leaves the applied profile for the next restart to retry", { timeout: 30_000 }, async () => { - const port = await freePortPair(); - const pickerPort = port + 1; - // Before the restart: an authority was published and Desktop had the picker profile selected. - ensurePickerCa(root); - expect(applyDesktopPickerProfile({ configDir: root, platform: "darwin", proxyPort: pickerPort }).ok).toBe(true); - writeFileSync(join(root, "config.json"), JSON.stringify({ - port: 10100, - providers: {}, - defaultProvider: "openai", - clientIntegrations: { "claude-desktop": true }, - claudeCode: { desktopMode: "first-party", intercept: { picker: true } }, - })); - const trust = keychain({ trusted: true }); - const listener = fakeListener(); - const store = memoryPickerCaStore().store; - const startOpts = () => ({ - config: config({ claudeCode: { intercept: { port } } }), - publicPort: 10100, - configDir: root, - dispatch: async () => new Response("unused"), - loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), - createPicker: (options: CreatePickerRuntimeOptions) => createPickerRuntime({ - ...options, - platform: "darwin" as NodeJS.Platform, - security: trust.run, - resolveMode: () => "first-party" as const, - startListener: listener.start, - refreshIntervalMs: 3_600_000, - }), - pickerSecurity: trust.run, - pickerCaStore: store, pickerPlatform: "darwin" as NodeJS.Platform, - }); - // Occupy the picker port: this restart cannot bind it. - const squatter = createServer(); - await new Promise((resolve, reject) => { - squatter.once("error", reject); - squatter.listen({ port: pickerPort, host: "127.0.0.1" }, () => resolve()); - }); - let first; + // Keep the conflict owned across both starts. Successful listeners choose their + // own ports through the existing bind seam; no released number is reacquired. + const squatter = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("occupied") }); + const pickerPort = squatter.port; + const port = pickerPort - 1; + let retry = false; try { - first = await startClaudeIntercept(startOpts()); - expect(first?.pickerProxyPort).toBeNull(); - expect(first).toMatchObject({ pickerReason: "port_in_use", pickerFailurePort: pickerPort }); - // The main pair still serves, and the applied selection is durable evidence for a retry. - expect(await connectStatusLine(port, "api.anthropic.com")).toContain("200"); - expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }).kind).toBe("applied"); - } finally { - await first?.stop(); - await new Promise(resolve => squatter.close(() => resolve())); - } - // Port free on the next restart: the surviving selection restores in place. - const second = await startClaudeIntercept(startOpts()); - try { - expect(second?.pickerProxyPort).toBe(pickerPort); - for (let i = 0; i < 1600 && inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }).kind !== "applied"; i += 1) { - await Bun.sleep(5); + // Before the restart: an authority was published and Desktop had the picker profile selected. + ensurePickerCa(root); + expect(applyDesktopPickerProfile({ configDir: root, platform: "darwin", proxyPort: pickerPort }).ok).toBe(true); + const original = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); + if (original.kind !== "applied") throw new Error("fixture picker profile was not applied"); + writeFileSync(join(root, "config.json"), JSON.stringify({ + port: 10100, + providers: {}, + defaultProvider: "openai", + clientIntegrations: { "claude-desktop": true }, + claudeCode: { desktopMode: "first-party", intercept: { picker: true } }, + })); + const trust = keychain({ trusted: true }); + const listener = fakeListener(); + const store = memoryPickerCaStore().store; + const startOpts = () => ({ + config: config({ claudeCode: { intercept: { port } } }), + publicPort: 10100, + configDir: root, + dispatch: async () => new Response("unused"), + loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), + createPicker: (options: CreatePickerRuntimeOptions) => createPickerRuntime({ + ...options, + platform: "darwin" as NodeJS.Platform, + security: trust.run, + resolveMode: () => "first-party" as const, + startListener: listener.start, + refreshIntervalMs: 3_600_000, + }), + pickerSecurity: trust.run, + pickerCaStore: store, pickerPlatform: "darwin" as NodeJS.Platform, + startProxy: (requested: number, options: Parameters[1]) => + startConnectProxy(requested === pickerPort && !retry ? pickerPort : 0, options), + }); + let first; + try { + first = await startClaudeIntercept(startOpts()); + expect(first?.pickerProxyPort).toBeNull(); + expect(first).toMatchObject({ pickerReason: "port_in_use", pickerFailurePort: pickerPort }); + // The main pair still serves, and the applied selection is durable evidence for a retry. + expect(await connectStatusLine(first!.proxyPort, "api.anthropic.com")).toContain("200"); + expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" })) + .toMatchObject({ kind: "applied", entryId: original.entryId, proxyUrl: original.proxyUrl }); + } finally { + await first?.stop(); } - expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" })) - .toMatchObject({ kind: "applied", proxyUrl: `http://127.0.0.1:${pickerPort}` }); - } finally { - await second?.stop(); - } + // The next restart gets a fresh listener; the surviving selection restores in place. + retry = true; + const second = await startClaudeIntercept(startOpts()); + try { + expect(second?.pickerProxyPort).toBeGreaterThan(0); + const restoredUrl = `http://127.0.0.1:${second!.pickerProxyPort}`; + for (let i = 0; i < 1600; i += 1) { + const current = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); + if (current.kind === "applied" && current.proxyUrl === restoredUrl) break; + await Bun.sleep(5); + } + expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" })) + .toMatchObject({ kind: "applied", entryId: original.entryId, proxyUrl: restoredUrl }); + } finally { + await second?.stop(); + } + } finally { await squatter.stop(true); } }); test("only the app's browser tunnels on Desktop's egress proxy consult the picker", async () => { diff --git a/tests/cli/cli-client-start-owner-e2e.test.ts b/tests/cli/cli-client-start-owner-e2e.test.ts index 3286ebdd3db..06a9ba3f842 100644 --- a/tests/cli/cli-client-start-owner-e2e.test.ts +++ b/tests/cli/cli-client-start-owner-e2e.test.ts @@ -128,12 +128,10 @@ test.skipIf(process.platform === "win32")("a desktop claim committed as the clie let watchdog: ReturnType | undefined; const timeoutMs = watchdogMs(10_000); try { - // A port nothing listens on, so the start path's liveness probe reads absent. - const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response(null, { status: 404 }) }); - const freePort = probe.port; - probe.stop(true); + // Port zero is an absent endpoint and lets an erroneous runtime start bind + // its own socket. The ownership fence must still prevent all publication. writeFileSync(join(ocxHome, "config.json"), JSON.stringify({ - port: freePort, + port: 0, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, diff --git a/tests/cli/cli-export-command.test.ts b/tests/cli/cli-export-command.test.ts index 1a7d4da8bfe..3bd09786c09 100644 --- a/tests/cli/cli-export-command.test.ts +++ b/tests/cli/cli-export-command.test.ts @@ -383,17 +383,15 @@ describe("ocx export with no live proxy (accept criterion 5)", () => { * would be discovered and the assertion would pass for the wrong reason. */ test("fails through the runtime-api error naming ocx start, emitting no config", () => { - const probe = Bun.serve({ port: 0, fetch: () => new Response("") }); - const deadPort = probe.port; - probe.stop(true); - const home = tempDir(); // Must pass config validation: a rejected config falls back to the DEFAULT config, whose // port 10100 may host the developer's own proxy — the test would then probe a live one. writeFileSync( join(home, "config.json"), JSON.stringify({ - port: deadPort, + // A configured zero is an absent endpoint, never an allocatable port + // another process can turn into a proxy between setup and discovery. + port: 0, defaultProvider: "mock", providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1", allowPrivateNetwork: true } }, }), diff --git a/tests/cli/cli-start-auxiliary-bind.test.ts b/tests/cli/cli-start-auxiliary-bind.test.ts index 6c00cb0ad89..f78e5b68745 100644 --- a/tests/cli/cli-start-auxiliary-bind.test.ts +++ b/tests/cli/cli-start-auxiliary-bind.test.ts @@ -1,8 +1,8 @@ -import { expect, test } from "bun:test"; +import { expect, spyOn, test } from "bun:test"; +import * as net from "node:net"; import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { findAvailablePort } from "../../src/server/ports"; import { repoPath } from "../helpers/repo-root"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { watchdogMs } from "../helpers/ci-watchdog"; @@ -20,7 +20,10 @@ for (const listener of ["unauthenticatedLoopbackListener", "hub.managementIngres for (const path of [home, ocxHome, codexHome]) mkdirSync(path); const occupied = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("occupied") }); const auxiliaryPort = occupied.port!; - const publicPort = await findAvailablePort(0, "127.0.0.1"); + // Also hold the public endpoint: auxiliary failure must win before even a + // pinned start attempts public-port reclamation. Nobody can steal this port. + const publicListener = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("public canary") }); + const publicPort = publicListener.port; writeFileSync(join(ocxHome, "config.json"), JSON.stringify({ port: publicPort, hostname: "127.0.0.1", providers: {}, defaultProvider: "openai", codexAutoStart: false, syncResumeHistory: false, @@ -52,38 +55,45 @@ for (const listener of ["unauthenticatedLoopbackListener", "hub.managementIngres // publication (and its synchronous Windows icacls subprocesses). expect(existsSync(join(ocxHome, "spend-ledger-owner.sqlite"))).toBe(false); expect(existsSync(join(ocxHome, "admin-api-token"))).toBe(false); - const rebound = Bun.serve({ port: publicPort, hostname: "127.0.0.1", fetch: () => new Response("free") }); - await rebound.stop(true); + expect(await (await fetch(`http://127.0.0.1:${publicPort}`)).text()).toBe("public canary"); } finally { clearTimeout(deadline); if (child.exitCode === null) { child.kill(); await child.exited; } await occupied.stop(true); + await publicListener.stop(true); removeTreeWithRetry(root); } }, DEADLINE + 10_000); } } -test("auxiliary preflight leaves free sockets available for the actual bind", async () => { - const loopbackPort = await findAvailablePort(0); - const managementPort = await findAvailablePort(0, "127.0.0.1", { reservedPort: loopbackPort }); - await assertAuxiliaryListenerPortsAvailable({ - port: 0, providers: {}, defaultProvider: "openai", runtimeRole: "hub", - unauthenticatedLoopbackListener: { enabled: true, port: loopbackPort }, - hub: { managementIngress: { enabled: true, port: managementPort } }, +test("auxiliary preflight closes its sockets before returning", async () => { + // Observe the actual handles instead of rebinding a released number. + const probes: net.Server[] = []; + const create = net.createServer; + const spy = spyOn(net, "createServer").mockImplementation((...args) => { + const probe = create(...args); probes.push(probe); return probe; }); - // Bind inside try: if another process takes the second port, the first server still stops. - const listeners: ReturnType[] = []; + const base = { port: 0, providers: {}, defaultProvider: "openai", runtimeRole: "hub" as const }; try { - for (const port of [loopbackPort, managementPort]) { - listeners.push(Bun.serve({ hostname: "127.0.0.1", port, fetch: () => new Response("ok") })); + await assertAuxiliaryListenerPortsAvailable({ ...base, unauthenticatedLoopbackListener: { enabled: true, port: 0 } }); + await assertAuxiliaryListenerPortsAvailable({ ...base, hub: { managementIngress: { enabled: true, port: 0 } } }); + expect(probes).toHaveLength(2); + for (const probe of probes) { + expect(probe.listening).toBe(false); + await new Promise((resolve, reject) => { probe.once("error", reject); probe.listen(0, "127.0.0.1", resolve); }); + expect(probe.listening).toBe(true); + await new Promise((resolve, reject) => probe.close(error => error ? reject(error) : resolve())); } - expect(listeners.map(server => server.port)).toEqual([loopbackPort, managementPort]); - } finally { await Promise.all(listeners.map(server => server.stop(true))); } + } finally { + spy.mockRestore(); + for (const probe of probes) if (probe.listening) probe.close(); + } }); test("auxiliary preflight rejects two listeners configured for one port", async () => { - const port = await findAvailablePort(0); + // Collision is rejected before binding, so no allocation is required. + const port = 12345; await expect(assertAuxiliaryListenerPortsAvailable({ port: 0, providers: {}, defaultProvider: "openai", runtimeRole: "hub", unauthenticatedLoopbackListener: { enabled: true, port }, diff --git a/tests/cli/ocx-launcher-runtime.test.ts b/tests/cli/ocx-launcher-runtime.test.ts index ded8132ac03..5803c7dc436 100644 --- a/tests/cli/ocx-launcher-runtime.test.ts +++ b/tests/cli/ocx-launcher-runtime.test.ts @@ -1,7 +1,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; -import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; @@ -50,18 +49,6 @@ type WindowsProcessIdentity = { creationDate: string; }; -function freePort(): Promise { - return new Promise((resolvePort, reject) => { - const server = createServer(); - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - const port = typeof address === "object" && address ? address.port : 0; - server.close(() => (port ? resolvePort(port) : reject(new Error("no port")))); - }); - }); -} - async function healthAt(port: number): Promise { try { const response = await fetch(`http://127.0.0.1:${port}/healthz`, { @@ -82,15 +69,18 @@ async function healthAt(port: number): Promise { } async function waitForHealth( - port: number, + runtimeRecord: string, deadlineMs: number, launcher: ChildProcess, ): Promise { const deadline = Date.now() + deadlineMs; while (Date.now() < deadline) { if (launcher.exitCode !== null) return null; - const health = await healthAt(port); - if (health) return health; + try { + const record = JSON.parse(readFileSync(runtimeRecord, "utf8")) as { port: number; pid: number }; + const health = await healthAt(record.port); + if (health?.pid === record.pid) return health; + } catch { /* runtime publication has not finished */ } await Bun.sleep(200); } return null; @@ -208,8 +198,7 @@ async function effectiveRuntime(override: string): Promise { let hasPrimaryError = false; let primaryError: unknown; try { - port = await freePort(); - launcher = spawn("node", [BIN_OCX, "start", "--port", String(port)], { + launcher = spawn("node", [BIN_OCX, "start", "--port", "0"], { stdio: "ignore", windowsHide: true, env: isolatedLauncherEnv(root, override), @@ -218,8 +207,9 @@ async function effectiveRuntime(override: string): Promise { launcherPid = launcher.pid; ownedLauncher = captureWindowsProcessIdentity(launcherPid); - const health = await waitForHealth(port, PROXY_HEALTH_TIMEOUT_MS, launcher); + const health = await waitForHealth(join(root, "opencodex", "runtime-port.json"), PROXY_HEALTH_TIMEOUT_MS, launcher); if (!health) throw new Error("proxy did not become healthy"); + port = health.port; const identity = windowsProcessIdentity(health.pid); if (!identity || identity.parentPid !== launcher.pid) { throw new Error("health PID is not the spawned Node launcher's direct Bun child"); diff --git a/tests/cli/sibling-home-client-sync.test.ts b/tests/cli/sibling-home-client-sync.test.ts index b40c69c6476..3db9ed84c53 100644 --- a/tests/cli/sibling-home-client-sync.test.ts +++ b/tests/cli/sibling-home-client-sync.test.ts @@ -296,11 +296,16 @@ test("same PID, null PID, foreign, stale and remote hints grant no sibling owner const foreign = healthServer(process.pid + 1, "another-service"); writeFileSync(grokPath, grokFence(foreign)); expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull(); - const closed = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("closed") }); - const stale = closed.port; - closed.stop(true); + // A stale URL exercises transport refusal, not a subsequent bind. Inject the + // refusal so an unrelated process cannot turn the "dead" hint into a live one. + const stale = 42101; + let refusedProbes = 0; writeFileSync(grokPath, grokFence(stale)); - expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull(); + expect(await findCrossHomeOwnerDetailed({ homeDir: fx.home, io: { + fetchFn: async () => { refusedProbes++; throw Object.assign(new Error("fixture refusal"), { code: "ECONNREFUSED" }); }, + sleepFn: async () => {}, + } })).toEqual({ kind: "none" }); + expect(refusedProbes).toBeGreaterThan(0); writeFileSync(grokPath, grokFence("not-a-port")); expect(await findCrossHomeOwner({ homeDir: fx.home })).toBeNull(); writeFileSync(grokPath, grokFence(foreign).replace("127.0.0.1", "example.com")); @@ -324,9 +329,6 @@ test("a secondary start preserves shared client bytes and records the sibling ow const fx = fixture(); const fakeOwnerPid = 1_000_000_000; const ownerPort = healthServer(fakeOwnerPid); - const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") }); - const secondaryPort = reservation.port; - reservation.stop(true); defaultRuntime(fx, fakeOwnerPid, ownerPort); const grokPath = join(fx.grok, "config.toml"); const codexPath = join(fx.codex, "config.toml"); @@ -336,11 +338,11 @@ test("a secondary start preserves shared client bytes and records the sibling ow writeFileSync(claudePath, "owned roster bytes\n"); const before = [grokPath, codexPath, claudePath].map(path => readFileSync(path)); writeFileSync(join(fx.ocx, "config.json"), JSON.stringify({ - port: secondaryPort, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, + port: 0, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, checkForUpdates: false, clientIntegrations: { codex: true, grok: true, "claude-desktop": false }, claudeCode: { injectAgents: false, systemEnv: false }, providers: {}, defaultProvider: "openai", })); - const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { + const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); trackChild(child); @@ -357,9 +359,6 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by const fx = fixture(); const ownerPort = healthServer(process.pid); defaultRuntime(fx, process.pid, ownerPort); - const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") }); - const secondaryPort = reservation.port; - reservation.stop(true); const grokPath = join(fx.grok, "config.toml"); const codexPath = join(fx.codex, "config.toml"); const claudePath = join(fx.claude, "agents", "ocx-existing.md"); @@ -368,7 +367,7 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by writeFileSync(claudePath, "---\ngenerated-by: opencodex\n---\nowner roster\n"); const before = [grokPath, codexPath, claudePath].map(path => readFileSync(path)); writeFileSync(join(fx.ocx, "config.json"), JSON.stringify({ - port: secondaryPort, hostname: "127.0.0.1", codexAutoStart: true, syncResumeHistory: false, + port: 0, hostname: "127.0.0.1", codexAutoStart: true, syncResumeHistory: false, checkForUpdates: false, clientIntegrations: { codex: true, grok: true, "claude-desktop": false }, claudeCode: { injectAgents: false, systemEnv: false }, providers: {}, defaultProvider: "openai", })); @@ -380,11 +379,11 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by const output = captured.stdout.tail; const error = captured.stderr.tail; expect(await ensure.exited).toBe(0); - expect(output + error).toContain(`Proxy running on port ${secondaryPort}`); const runtime = JSON.parse(readFileSync(join(fx.ocx, "runtime-port.json"), "utf8")) as { pid: number; port: number; siblingOfPort?: number; }; detachedPids.push(runtime.pid); + expect(output + error).toContain(`Proxy running on port ${runtime.port}`); expect(runtime.siblingOfPort).toBe(ownerPort); expect([grokPath, codexPath, claudePath].map(path => readFileSync(path))).toEqual(before); @@ -401,19 +400,16 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by test("a lone custom-home start still syncs Grok and prunes its own Claude roster", async () => { const fx = fixture(); - const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") }); - const port = reservation.port; - reservation.stop(true); const grokPath = join(fx.grok, "config.toml"); const claudePath = join(fx.claude, "agents", "ocx-existing.md"); writeFileSync(grokPath, grokFence(12345)); writeFileSync(claudePath, "---\ngenerated-by: opencodex\n---\nold roster\n"); writeFileSync(join(fx.ocx, "config.json"), JSON.stringify({ - port, hostname: "127.0.0.1", codexAutoStart: true, syncResumeHistory: false, + port: 0, hostname: "127.0.0.1", codexAutoStart: true, syncResumeHistory: false, checkForUpdates: false, clientIntegrations: { codex: false, grok: true, "claude-desktop": false }, claudeCode: { injectAgents: false, systemEnv: false }, providers: {}, defaultProvider: "openai", })); - const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(port)], { + const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); trackChild(child); @@ -421,7 +417,7 @@ test("a lone custom-home start still syncs Grok and prunes its own Claude roster expect(runtime.siblingOfPort).toBeUndefined(); const deadline = Date.now() + 10_000; while (Date.now() < deadline && readFileSync(grokPath, "utf8").includes("127.0.0.1:12345")) await Bun.sleep(20); - expect(readFileSync(grokPath, "utf8")).toContain(`127.0.0.1:${port}`); + expect(readFileSync(grokPath, "utf8")).toContain(`127.0.0.1:${runtime.port}`); expect(existsSync(claudePath)).toBe(false); writeFileSync(claudePath, "---\ngenerated-by: opencodex\n---\nstale roster\n"); const ensure = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "ensure"], { @@ -655,9 +651,6 @@ async function secondaryStartPreservesBytes( ownerPort: number, expectedSiblingPort: number | null, ): Promise { - const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") }); - const secondaryPort = reservation.port; - reservation.stop(true); const grokPath = join(fx.grok, "config.toml"); const codexPath = join(fx.codex, "config.toml"); const claudePath = join(fx.claude, "agents", "ocx-existing.md"); @@ -666,11 +659,11 @@ async function secondaryStartPreservesBytes( writeFileSync(claudePath, "owned roster bytes\n"); const before = [grokPath, codexPath, claudePath].map(path => readFileSync(path)); writeFileSync(join(fx.ocx, "config.json"), JSON.stringify({ - port: secondaryPort, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, + port: 0, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, checkForUpdates: false, clientIntegrations: { codex: true, grok: true, "claude-desktop": false }, claudeCode: { injectAgents: false, systemEnv: false }, providers: {}, defaultProvider: "openai", })); - const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { + const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); trackChild(child); @@ -747,15 +740,12 @@ test("a registry-only discovered owner vetoes shared writes end to end", async ( writeFileSync(codexPath, "model = \"gpt-6\"\n"); writeFileSync(claudePath, "owned roster bytes\n"); const before = [grokPath, codexPath, claudePath].map(path => readFileSync(path)); - const reservation = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("reserved") }); - const secondaryPort = reservation.port; - reservation.stop(true); writeFileSync(join(fx.ocx, "config.json"), JSON.stringify({ - port: secondaryPort, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, + port: 0, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, checkForUpdates: false, clientIntegrations: { codex: true, grok: true, "claude-desktop": false }, claudeCode: { injectAgents: false, systemEnv: false }, providers: {}, defaultProvider: "openai", })); - const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { + const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); trackChild(child); From 7c4dd93f0f8fd01de632e086504594e353d24f71 Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:33:19 +0900 Subject: [PATCH 2/4] test(cli): fix ephemeral launcher and refused-probe contracts --- tests/cli/cli-client-start-owner-e2e.test.ts | 9 ++++++--- tests/cli/cli-export-command.test.ts | 9 +++++---- tests/cli/ocx-launcher-runtime.test.ts | 9 +++++++-- tests/fixtures/refused-local-probe-preload.ts | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/refused-local-probe-preload.ts diff --git a/tests/cli/cli-client-start-owner-e2e.test.ts b/tests/cli/cli-client-start-owner-e2e.test.ts index 06a9ba3f842..3f3e20c1d99 100644 --- a/tests/cli/cli-client-start-owner-e2e.test.ts +++ b/tests/cli/cli-client-start-owner-e2e.test.ts @@ -128,10 +128,10 @@ test.skipIf(process.platform === "win32")("a desktop claim committed as the clie let watchdog: ReturnType | undefined; const timeoutMs = watchdogMs(10_000); try { - // Port zero is an absent endpoint and lets an erroneous runtime start bind - // its own socket. The ownership fence must still prevent all publication. + // The child preload refuses the configured-port probe deterministically; + // the ownership fence must still prevent all runtime publication. writeFileSync(join(ocxHome, "config.json"), JSON.stringify({ - port: 0, + port: 42101, hostname: "127.0.0.1", codexAutoStart: false, syncResumeHistory: false, @@ -155,6 +155,8 @@ test.skipIf(process.platform === "win32")("a desktop claim committed as the clie child = Bun.spawn([ process.execPath, "--preload", + repoPath("tests/fixtures/refused-local-probe-preload.ts"), + "--preload", repoPath("tests/fixtures/client-runtime-claim-preload.ts"), repoPath("src/cli/index.ts"), "start", @@ -180,6 +182,7 @@ test.skipIf(process.platform === "win32")("a desktop claim committed as the clie }), ]); const output = result[2] || result[1]; + expect(result[2], output).toContain("[refused-probe] ECONNREFUSED"); // The claim landed on the connected-client branch, after the recovery fence passed. expect(result[2], output).toContain("[claim-preload] desktop claim committed at client runtime load"); expect(result[0], output).toBe(0); diff --git a/tests/cli/cli-export-command.test.ts b/tests/cli/cli-export-command.test.ts index 3bd09786c09..de46076425f 100644 --- a/tests/cli/cli-export-command.test.ts +++ b/tests/cli/cli-export-command.test.ts @@ -389,16 +389,16 @@ describe("ocx export with no live proxy (accept criterion 5)", () => { writeFileSync( join(home, "config.json"), JSON.stringify({ - // A configured zero is an absent endpoint, never an allocatable port - // another process can turn into a proxy between setup and discovery. - port: 0, + // The child preload injects ECONNREFUSED at this exact endpoint. + port: 42101, defaultProvider: "mock", providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1", allowPrivateNetwork: true } }, }), "utf8", ); - const result = spawnSync(process.execPath, [cliPath, "export", "--client", "opencode", "--json"], { + const preload = join(repoRoot, "tests", "fixtures", "refused-local-probe-preload.ts"); + const result = spawnSync(process.execPath, ["--preload", preload, cliPath, "export", "--client", "opencode", "--json"], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: home }, encoding: "utf8", @@ -406,6 +406,7 @@ describe("ocx export with no live proxy (accept criterion 5)", () => { expect(result.status).not.toBe(0); expect(result.stdout).toBe(""); + expect(result.stderr).toContain("[refused-probe] ECONNREFUSED"); expect(result.stderr).toContain("ocx start"); // Routed by the dispatcher, not swallowed by the unknown-command branch. expect(result.stderr).not.toContain("Unknown command"); diff --git a/tests/cli/ocx-launcher-runtime.test.ts b/tests/cli/ocx-launcher-runtime.test.ts index 5803c7dc436..a755d0d2bde 100644 --- a/tests/cli/ocx-launcher-runtime.test.ts +++ b/tests/cli/ocx-launcher-runtime.test.ts @@ -198,10 +198,15 @@ async function effectiveRuntime(override: string): Promise { let hasPrimaryError = false; let primaryError: unknown; try { - launcher = spawn("node", [BIN_OCX, "start", "--port", "0"], { + const env = isolatedLauncherEnv(root, override); + // CLI --port requires a positive pin; configured zero requests allocation. + writeFileSync(join(root, "opencodex", "config.json"), JSON.stringify({ + port: 0, providers: {}, defaultProvider: "openai", + })); + launcher = spawn("node", [BIN_OCX, "start"], { stdio: "ignore", windowsHide: true, - env: isolatedLauncherEnv(root, override), + env, }); if (!launcher.pid) throw new Error("Node launcher has no process id"); launcherPid = launcher.pid; diff --git a/tests/fixtures/refused-local-probe-preload.ts b/tests/fixtures/refused-local-probe-preload.ts new file mode 100644 index 00000000000..a24e8bafe19 --- /dev/null +++ b/tests/fixtures/refused-local-probe-preload.ts @@ -0,0 +1,18 @@ +// Child-only transport seam for CLI cases that require a refused /healthz probe. +// No released port or host listener can turn this fixture into a live endpoint. +import { plugin } from "bun"; + +plugin({ + name: "refused-local-health-probe", + setup(build) { + build.onLoad({ filter: /[\\/]src[\\/]server[\\/]direct-local-http\.ts$/ }, () => ({ + loader: "ts", + contents: `export async function directLocalHttpFetch(input) { + if (String(input) !== "http://127.0.0.1:42101/healthz") throw new Error("unexpected fixture probe"); + const error = Object.assign(new Error("fixture connection refused"), { code: "ECONNREFUSED" }); + console.error("[refused-probe] " + error.code); + throw error; + }`, + })); + }, +}); From 65186a6a2a8fde94124617127416b59174ba29ff Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:57:58 +0900 Subject: [PATCH 3/4] test: finish held-port picker restart fixtures --- .../claude-intercept-proxy-auth.test.ts | 21 ++- .../claude-picker-runtime.test.ts | 140 ++++++++++++------ tests/providers/minimax-clients.test.ts | 13 +- tests/service/shutdown-launcher.test.ts | 28 ++-- 4 files changed, 120 insertions(+), 82 deletions(-) diff --git a/tests/claude-integration/claude-intercept-proxy-auth.test.ts b/tests/claude-integration/claude-intercept-proxy-auth.test.ts index effbb56207a..95ebeee48aa 100644 --- a/tests/claude-integration/claude-intercept-proxy-auth.test.ts +++ b/tests/claude-integration/claude-intercept-proxy-auth.test.ts @@ -1,7 +1,8 @@ import { afterEach, expect, spyOn, test } from "bun:test"; import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import * as filesystem from "node:fs"; -import { connect, createServer } from "node:net"; +import { connect } from "node:net"; +import { startConnectProxy } from "../../src/claude/intercept/connect-proxy"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { claudeInterceptProxyTokenPath, ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "../../src/claude/intercept/proxy-auth"; @@ -94,14 +95,6 @@ test("a linked credential directory never grants authority", () => { expect(readFileSync(join(external, "proxy-token"), "utf8")).toBe("D".repeat(43) + "\n"); }); -async function unusedPort(): Promise { - const server = createServer(); - await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); - const address = server.address(); - if (!address || typeof address === "string") throw new Error("no test port"); - await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); - return address.port; -} function connectStatus(port: number, token?: string): Promise { return new Promise((resolve, reject) => { const socket = connect({ host: "127.0.0.1", port }, () => { @@ -129,14 +122,17 @@ test("a live intercept adopts an explicitly recreated token without restarting", process.env.CLAUDE_CONFIG_DIR = claudeHome; const { startClaudeIntercept } = await import("../../src/claude/intercept/runtime"); const { applyDesktopFirstParty } = await import("../../src/claude/desktop-first-party"); - const port = await unusedPort(); const config = { port: 10100, defaultProvider: "openai", providers: {}, - claudeCode: { intercept: { enabled: true, port, picker: false } } }; + claudeCode: { intercept: { enabled: true, port: 0, picker: false } } }; let handle: Awaited> = null; try { handle = await startClaudeIntercept({ config, configDir: root, publicPort: 10100, - dispatch: async () => new Response("local fixture") }); + dispatch: async () => new Response("local fixture"), + startProxy: (_port, options) => startConnectProxy(0, options) }); expect(handle).not.toBeNull(); + const port = handle!.proxyPort; + expect(port).toBeGreaterThan(0); + config.claudeCode.intercept.port = port; expect(applyDesktopFirstParty(config, { opencodexConfigDir: root, claudeConfigDir: claudeHome }).ok).toBe(true); const old = readClaudeInterceptProxyToken(root)!; expect(await connectStatus(port, old)).toBe(200); @@ -147,6 +143,7 @@ test("a live intercept adopts an explicitly recreated token without restarting", const current = readClaudeInterceptProxyToken(root)!; expect(current).not.toBe(old); const settings = JSON.parse(readFileSync(join(claudeHome, "settings.json"), "utf8")); + expect(Number(new URL(settings.env.HTTPS_PROXY).port)).toBe(port); expect(new URL(settings.env.HTTPS_PROXY).password).toBe(current); expect(await connectStatus(port, current)).toBe(200); expect(await connectStatus(port, old)).toBe(407); diff --git a/tests/claude-integration/claude-picker-runtime.test.ts b/tests/claude-integration/claude-picker-runtime.test.ts index 53ed6345db9..7fcaa7f8c78 100644 --- a/tests/claude-integration/claude-picker-runtime.test.ts +++ b/tests/claude-integration/claude-picker-runtime.test.ts @@ -28,6 +28,7 @@ let root = ""; const previous: Record = {}; const ENV_KEYS = ["OPENCODEX_HOME", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const; const running: PickerRuntime[] = []; +const closeFixtures: (() => void | Promise)[] = []; const LISTENER_PORT = 45_678; function config(extra: Partial = {}): OcxConfig { @@ -94,6 +95,7 @@ beforeEach(() => { afterEach(async () => { for (const created of running.splice(0)) await created.stop(); + for (const close of closeFixtures.splice(0)) await close(); for (const key of ENV_KEYS) { if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key]; @@ -269,12 +271,33 @@ async function canBind(port: number): Promise { }); } -async function freePortPair(): Promise { - for (let attempt = 0; attempt < 50; attempt += 1) { - const port = 20_000 + Math.floor(Math.random() * 30_000); - if (await canBind(port) && await canBind(port + 1)) return port; - } - throw new Error("no free port pair"); +// Successful binds belong to the real proxy; no probe releases a future endpoint. +const ephemeralProxy: typeof startConnectProxy = (_port, options) => startConnectProxy(0, options); + +function heldPort() { + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("occupied") }); + closeFixtures.push(() => server.stop(true)); + return server.port!; +} + +// Own the profile's exact URL before publishing it, then adopt the real relay +// through the runtime's bind seam. Tunnel decisions still come from the runtime. +async function heldEgress(dialUpstream?: Parameters[1]["dialUpstream"]) { + let selectTunnel: Parameters[1]["selectTunnel"]; + const proxy = await startConnectProxy(0, { + interceptPort: 0, interceptHosts: [], dialUpstream, + selectTunnel: (...args) => selectTunnel?.(...args) ?? null, + }); + closeFixtures.push(() => proxy.close()); + const startProxy: typeof startConnectProxy = async (port, options) => { + if (port !== proxy.port) return ephemeralProxy(port, options); + expect(options.interceptHosts).toEqual([]); + expect(options.authToken).toBeUndefined(); + expect(options.allowedTargets).toBeUndefined(); + selectTunnel = options.selectTunnel; + return proxy; + }; + return { port: proxy.port, startProxy }; } const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Claude/2.7032.0"; @@ -323,13 +346,14 @@ describe("startClaudeIntercept wiring", () => { dispatch: async () => new Response("unused"), loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), createPicker, + startProxy: ephemeralProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin" as NodeJS.Platform, }; } for (const failure of ["construction", "start"] as const) { test(`a picker ${failure} failure rejects the start and releases every port`, async () => { - const port = await freePortPair(); + const ports: number[] = []; const createPicker = failure === "construction" ? () => { throw new Error("picker construction failed"); } : () => ({ @@ -337,16 +361,24 @@ describe("startClaudeIntercept wiring", () => { start: async () => { throw new Error("picker start failed"); }, stop: async () => {}, }) as unknown as PickerRuntime; - await expect(startClaudeIntercept(interceptOptions(port, createPicker))).rejects.toThrow(`picker ${failure} failed`); - expect(await canBind(port)).toBe(true); - expect(await canBind(port + 1)).toBe(true); + await expect(startClaudeIntercept({ + ...interceptOptions(0, createPicker), + startProxy: async (_port, options) => { + const proxy = await startConnectProxy(0, options); + ports.push(proxy.port); + return proxy; + }, + })).rejects.toThrow(`picker ${failure} failed`); + expect(ports).toHaveLength(failure === "construction" ? 1 : 2); + for (const port of ports) expect(await canBind(port)).toBe(true); expect(getClaudePickerRuntime()).toBeNull(); }); } test("a restart with an applied picker profile keeps the reused CA trusted and reselects the profile", { timeout: 30_000 }, async () => { - const port = await freePortPair(); - const pickerPort = port + 1; + const port = heldPort(); + const egress = await heldEgress(); + const pickerPort = egress.port; // Before the restart: an authority was published and Desktop had the picker profile selected. ensurePickerCa(root); expect(applyDesktopPickerProfile({ configDir: root, platform: "darwin", proxyPort: pickerPort }).ok).toBe(true); @@ -374,9 +406,16 @@ describe("startClaudeIntercept wiring", () => { refreshIntervalMs: 3_600_000, }), pickerSecurity: trust.run, + startProxy: (requested, options) => options.authToken + ? ephemeralProxy(requested, options) : egress.startProxy(egress.port, options), pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { + expect(handle!.proxyPort).toBeGreaterThan(0); + expect(handle!.proxyPort).not.toBe(port); + expect(pickerPort).toBeGreaterThan(0); + expect(handle!.pickerProxyPort).toBe(pickerPort); + expect(handle!.proxyPort).not.toBe(pickerPort); // The restart reuses this process's authority, so cleanup must NOT untrust it — the applied // row survives in place and the restore enable only rewrites the row's proxy URL. Poll until // that rewrite lands; the metadata's atomic write can otherwise be observed as a transient @@ -403,7 +442,7 @@ describe("startClaudeIntercept wiring", () => { }); test("a restart keeps a reused authority trusted without touching the keychain", async () => { - const port = await freePortPair(); + const port = 0; // Published authority is the process cache's — restarting in-process must not revoke its trust. ensurePickerCa(root); const trust = keychain({ trusted: true }); @@ -420,6 +459,7 @@ describe("startClaudeIntercept wiring", () => { loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), createPicker: () => fake, pickerSecurity: trust.run, + startProxy: ephemeralProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { @@ -435,7 +475,7 @@ describe("startClaudeIntercept wiring", () => { }); test("a restart with a foreign published CA untrusts the outgoing certificate and arms the picker", async () => { - const port = await freePortPair(); + const port = 0; // The file on disk holds a different authority than this process will publish — e.g. written // by a since-exited peer — so startup must drop its keychain trust before arming. ensurePickerCa(root); @@ -470,6 +510,7 @@ describe("startClaudeIntercept wiring", () => { loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), createPicker: () => fake, pickerSecurity: run, + startProxy: ephemeralProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { @@ -485,7 +526,7 @@ describe("startClaudeIntercept wiring", () => { }); test("a malformed published certificate is unidentifiable, so startup still arms", async () => { - const port = await freePortPair(); + const port = 0; // Damaged write: the file exists but is not a certificate. Nothing can be identified for // removal, so startup must keep the intercept pair and picker running rather than fail. ensurePickerCa(root); @@ -504,12 +545,13 @@ describe("startClaudeIntercept wiring", () => { loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), createPicker: () => fake, pickerSecurity: trust.run, + startProxy: ephemeralProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { expect(handle).not.toBeNull(); expect(getClaudePickerRuntime()).toBe(fake); - expect(await connectStatusLine(port, "api.anthropic.com")).toContain("200"); + expect(await connectStatusLine(handle!.proxyPort, "api.anthropic.com")).toContain("200"); const names = trust.calls.map(call => call[0]); expect(names).not.toContain("remove-trusted-cert"); expect(names).not.toContain("delete-certificate"); @@ -521,8 +563,9 @@ describe("startClaudeIntercept wiring", () => { // INV-PICKER-01: a failed predecessor untrust keeps the applied profile's egress URL serving blind CONNECT, never picker TLS. test("a failed rotation untrust refuses the picker but keeps the intercept pair serving", async () => { - const port = await freePortPair(); - const pickerPort = port + 1; + const egress = await heldEgress(() => connect({ host: "127.0.0.1", port: upstreamPort })); + const pickerPort = egress.port; + const port = pickerPort - 1; const stateDir = pickerStateDir(root); // Legacy state: a published certificate and the exportable signing key it paired with. ensurePickerCa(root); @@ -563,11 +606,8 @@ describe("startClaudeIntercept wiring", () => { dispatch: async () => new Response("unused"), loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [] }), createPicker: () => { pickerCreated = true; throw new Error("must not start"); }, - startProxy: (boundPort, options) => startConnectProxy(boundPort, { - ...options, - ...(boundPort === egressPort ? { dialUpstream: () => connect({ host: "127.0.0.1", port: upstreamPort }) } : {}), - }), pickerSecurity: broken, + startProxy: egress.startProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { @@ -585,8 +625,8 @@ describe("startClaudeIntercept wiring", () => { expect(JSON.parse(stateBefore).previousAppliedId).toBe("previous-profile"); expect(existsSync(pickerCaPendingUntrustPath(root))).toBe(true); // The main intercept proxy stayed bound and still terminates api.anthropic.com CONNECTs. - expect(await canBind(port)).toBe(false); - expect(await connectStatusLine(port, "api.anthropic.com")).toContain("200"); + expect(await canBind(handle!.proxyPort)).toBe(false); + expect(await connectStatusLine(handle!.proxyPort, "api.anthropic.com")).toContain("200"); } finally { await handle?.stop(); await new Promise(resolve => upstream.close(() => resolve())); @@ -596,8 +636,11 @@ describe("startClaudeIntercept wiring", () => { }); test("a busy applied egress port leaves the failed-cleanup profile and journal intact", async () => { - const port = await freePortPair(); - const pickerPort = port + 1; + const squatter = createServer(socket => socket.end("held-by-peer")); + await new Promise(resolve => squatter.listen(0, "127.0.0.1", resolve)); + closeFixtures.push(() => new Promise(resolve => squatter.close(() => resolve()))); + const pickerPort = (squatter.address() as { port: number }).port; + const port = pickerPort - 1; ensurePickerCa(root); expect(applyDesktopPickerProfile({ configDir: root, platform: "darwin", proxyPort: pickerPort }).ok).toBe(true); const profileBefore = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); @@ -605,8 +648,6 @@ describe("startClaudeIntercept wiring", () => { const foreign = createCertificateAuthority({ commonName: PICKER_CA_COMMON_NAME, permittedDnsNames: [PICKER_HOST] }); writeFileSync(pickerCaCertPath(root), foreign.certPem); const sha1 = pickerCaFingerprints(foreign.certPem).sha1; - const squatter = createServer(socket => socket.end("held-by-peer")); - await new Promise(resolve => squatter.listen(pickerPort, "127.0.0.1", resolve)); let handle; try { handle = await startClaudeIntercept({ @@ -617,6 +658,7 @@ describe("startClaudeIntercept wiring", () => { pickerSecurity: async args => args[0] === "find-certificate" ? { code: 0, stdout: `SHA-1 hash: ${sha1}\n`, stderr: "" } : { code: 1, stdout: "", stderr: "" }, + startProxy: (requested, options) => startConnectProxy(requested === pickerPort ? pickerPort : 0, options), pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); expect(handle?.pickerProxyPort).toBeNull(); @@ -633,8 +675,9 @@ describe("startClaudeIntercept wiring", () => { }); test("a corrupt pending record keeps an applied egress blind without a keychain call", async () => { - const port = await freePortPair(); - const pickerPort = port + 1; + const egress = await heldEgress(); + const pickerPort = egress.port; + const port = pickerPort - 1; ensurePickerCa(root); expect(applyDesktopPickerProfile({ configDir: root, platform: "darwin", proxyPort: pickerPort }).ok).toBe(true); writeFileSync(pickerCaPendingUntrustPath(root), "{corrupt"); @@ -649,6 +692,7 @@ describe("startClaudeIntercept wiring", () => { calls.push(args[0]!); return { code: 0, stdout: "", stderr: "" }; }, + startProxy: egress.startProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { @@ -662,8 +706,9 @@ describe("startClaudeIntercept wiring", () => { }); test("a restart preserves the picker row identity and its recorded previous selection", { timeout: 30_000 }, async () => { - const port = await freePortPair(); - const pickerPort = port + 1; + const egress = await heldEgress(); + const pickerPort = egress.port; + const port = pickerPort - 1; const library = join(root, "desktop-library"); // Before the restart: Desktop selected a foreign profile "Personal", then the picker was // applied — so profile-state.json records it as the previous selection. @@ -700,14 +745,19 @@ describe("startClaudeIntercept wiring", () => { refreshIntervalMs: 3_600_000, }), pickerSecurity: trust.run, + startProxy: egress.startProxy, pickerCaStore: memoryPickerCaStore().store, pickerPlatform: "darwin", }); try { + expect(pickerPort).toBeGreaterThan(0); + expect(handle!.pickerProxyPort).toBe(pickerPort); // The rotation must not pivot through the previous profile: no placeholder is created, the // row keeps its id, and the recorded previous selection still points at "Personal". // Same slack as the sibling restart test: on Windows each lifecycle-lock acquisition is a // PowerShell SID lookup, so the restore enable can take several seconds to land. - for (let i = 0; i < 1600 && inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }).kind !== "applied"; i += 1) { + for (let i = 0; i < 1600; i += 1) { + const profile = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); + if (profile.kind === "applied" && profile.proxyUrl === `http://127.0.0.1:${pickerPort}`) break; await Bun.sleep(5); } const applied = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); @@ -797,43 +847,45 @@ describe("startClaudeIntercept wiring", () => { }); test("only the app's browser tunnels on Desktop's egress proxy consult the picker", async () => { - const port = await freePortPair(); const asked: string[] = []; const fake = { selectTunnel: (host: string) => { asked.push(host); return null; }, start: async () => {}, stop: async () => {}, } as unknown as PickerRuntime; - const handle = await startClaudeIntercept(interceptOptions(port, () => fake)); + const handle = await startClaudeIntercept(interceptOptions(0, () => fake)); + const port = handle!.proxyPort; + const pickerPort = handle!.pickerProxyPort!; try { - expect(handle?.proxyPort).toBe(port); - expect(handle?.pickerProxyPort).toBe(port + 1); + expect(port).toBeGreaterThan(0); + expect(pickerPort).toBeGreaterThan(0); + expect(pickerPort).not.toBe(port); expect(getClaudePickerRuntime()).toBe(fake); // The Claude Code proxy never consults the picker, whoever connects. expect(await connectStatusLine(port, "picker-probe.invalid", BROWSER_UA)).toContain("502"); expect(asked).toEqual([]); // Claude Code processes Desktop spawns reach the egress proxy without a User-Agent: claude.ai // stays blind for them, and api.anthropic.com gets the intercept (a local listener, so 200). - expect(await connectStatusLine(port + 1, "picker-probe.invalid")).toContain("502"); - expect(await connectStatusLine(port + 1, "api.anthropic.com")).toContain("200"); + expect(await connectStatusLine(pickerPort, "picker-probe.invalid")).toContain("502"); + expect(await connectStatusLine(pickerPort, "api.anthropic.com")).toContain("200"); expect(asked).toEqual([]); // The app's own tunnels carry its browser User-Agent and are the only ones the picker sees. - expect(await connectStatusLine(port + 1, "picker-probe.invalid", BROWSER_UA)).toContain("502"); + expect(await connectStatusLine(pickerPort, "picker-probe.invalid", BROWSER_UA)).toContain("502"); expect(asked).toEqual(["picker-probe.invalid"]); // The User-Agent is a routing hint. A client that fakes a browser one only reaches the picker // decision (claude.ai relay, which needs the keychain-trusted CA); one that omits it only // reaches the api.anthropic.com intercept, which the Claude Code proxy already offers any // local process, and its api.anthropic.com tunnel is not asked of the picker. - expect(await connectStatusLine(port + 1, "api.anthropic.com", "curl/8.7.1")).toContain("200"); + expect(await connectStatusLine(pickerPort, "api.anthropic.com", "curl/8.7.1")).toContain("200"); // An empty User-Agent is not a browser: blind, not asked. A faked browser one is only asked. - expect(await connectStatusLine(port + 1, "empty-ua.invalid", "")).toContain("502"); - expect(await connectStatusLine(port + 1, "spoofed-ua.invalid", `${BROWSER_UA} spoofed`)).toContain("502"); + expect(await connectStatusLine(pickerPort, "empty-ua.invalid", "")).toContain("502"); + expect(await connectStatusLine(pickerPort, "spoofed-ua.invalid", `${BROWSER_UA} spoofed`)).toContain("502"); expect(asked).toEqual(["picker-probe.invalid", "spoofed-ua.invalid"]); } finally { await handle?.stop(); } expect(getClaudePickerRuntime()).toBeNull(); - expect(await canBind(port + 1)).toBe(true); + expect(await canBind(pickerPort)).toBe(true); }); }); diff --git a/tests/providers/minimax-clients.test.ts b/tests/providers/minimax-clients.test.ts index 651e6c96695..9706c440611 100644 --- a/tests/providers/minimax-clients.test.ts +++ b/tests/providers/minimax-clients.test.ts @@ -438,13 +438,11 @@ describe("MiniMax CLI wrapper", () => { }); test("returns 502 when the selected OpenCodex proxy address is unavailable", async () => { - const reservation = Bun.serve({ - hostname: "127.0.0.1", - port: 0, - fetch: () => Response.json({ reserved: true }), - }); - const deadPort = reservation.port; - await reservation.stop(true); + // Own the endpoint throughout: a peer that closes TCP without an HTTP response + // is unavailable to the bridge even when another test is binding nearby ports. + const reservation = createServer(socket => socket.destroy()); + await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve)); + const deadPort = (reservation.address() as { port: number }).port; const bridge = startMmxTextBridge({ hostname: "127.0.0.1", port: deadPort }); try { const response = await fetch(`${bridge.baseUrl}/anthropic/v1/messages`, { @@ -459,6 +457,7 @@ describe("MiniMax CLI wrapper", () => { }); } finally { await bridge.stop(); + await new Promise(resolve => reservation.close(() => resolve())); } }); diff --git a/tests/service/shutdown-launcher.test.ts b/tests/service/shutdown-launcher.test.ts index 8b97d731168..31bb4dda714 100644 --- a/tests/service/shutdown-launcher.test.ts +++ b/tests/service/shutdown-launcher.test.ts @@ -1,7 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; -import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { claimOwnedServiceHome } from "../helpers/owned-service-home"; @@ -46,18 +45,6 @@ afterAll(() => { } }); -function freePort(): Promise { - return new Promise((resolve, reject) => { - const srv = createServer(); - srv.on("error", reject); - srv.listen(0, "127.0.0.1", () => { - const addr = srv.address(); - const port = typeof addr === "object" && addr ? addr.port : 0; - srv.close(() => (port ? resolve(port) : reject(new Error("no port")))); - }); - }); -} - async function healthy(port: number): Promise { try { const res = await fetch(`http://127.0.0.1:${port}/healthz`, { @@ -101,7 +88,7 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { async () => { const home = mkdtempSync(join(tmpdir(), "ocx-shutdown-")); tmpHomes.push(home); - const port = await freePort(); + let port = 0; const identity = claimTempHome(home); // Seed a native Codex config so the proxy actually injects on start (injectCodexConfig @@ -111,10 +98,8 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { ? 'model_provider = "chatgpt-http"\n[model_providers.chatgpt-http]\nbase_url = "https://chatgpt.com/backend-api/codex"\nwire_api = "responses"\nrequires_openai_auth = true\nsupports_websockets = false\n' : 'model = "gpt-5.1"\n'; writeFileSync(codexConfig, original); - // Pin the configured port to this test's own port. `ocx start` probes the CONFIGURED - // port for a live owner even with no state files, and a fresh home defaults to 10100. - // On a developer machine running ocx there, the child found that proxy, took the sibling - // path and by design never injected Codex config, so this test could not pass locally. + // Bind port 0 and discover the actual endpoint from this home's publication. + // Pin config too: the startup owner probe must not find a real proxy on 10100. writeFileSync(join(home, "config.json"), JSON.stringify({ port })); // stdout/stderr are CAPTURED, not discarded. @@ -144,7 +129,7 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { }); expect(consent.status, consent.stderr).toBe(0); } - const child = spawn("node", [BIN_OCX, "start", "--port", String(port)], { + const child = spawn("node", [BIN_OCX, "start"], { stdio: ["ignore", "pipe", "pipe"], env: childEnv, }); spawned.push(child); @@ -163,6 +148,11 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { // The health listener may answer before the launcher completes injection. let healthSeen = false; const up = await waitUntil(async () => { + try { + const runtime = JSON.parse(readFileSync(join(home, "runtime-port.json"), "utf8")); + if (!Number.isInteger(runtime.port) || runtime.port <= 0) return false; + port = runtime.port; + } catch { return false; } if (!(await healthy(port))) return false; healthSeen = true; return readFileSync(codexConfig, "utf8").includes(approved ? `http://127.0.0.1:${port}/v1` : OCX_ROUTING_MARKER_LINE); From f09199506d49f1b0e2b79a92fd9f984af7bb5a04 Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:25:33 +0900 Subject: [PATCH 4/4] test: retain picker relay and MiniMax proxy canary --- .../claude-picker-runtime.test.ts | 20 +++-- tests/fixtures/minimax-bridge-direct.ts | 11 --- tests/providers/minimax-clients.test.ts | 77 ++++++++++--------- 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/tests/claude-integration/claude-picker-runtime.test.ts b/tests/claude-integration/claude-picker-runtime.test.ts index 7fcaa7f8c78..095938bcad6 100644 --- a/tests/claude-integration/claude-picker-runtime.test.ts +++ b/tests/claude-integration/claude-picker-runtime.test.ts @@ -297,7 +297,7 @@ async function heldEgress(dialUpstream?: Parameters[1] selectTunnel = options.selectTunnel; return proxy; }; - return { port: proxy.port, startProxy }; + return { port: proxy.port, startProxy, close: proxy.close }; } const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Claude/2.7032.0"; @@ -774,10 +774,10 @@ describe("startClaudeIntercept wiring", () => { }); test("a busy picker port leaves the applied profile for the next restart to retry", { timeout: 30_000 }, async () => { - // Keep the conflict owned across both starts. Successful listeners choose their - // own ports through the existing bind seam; no released number is reacquired. - const squatter = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("occupied") }); - const pickerPort = squatter.port; + // Keep the real egress relay bound: the first start conflicts with it, and the + // retry adopts it at the persisted URL without reacquiring a released port. + const egress = await heldEgress(); + const pickerPort = egress.port; const port = pickerPort - 1; let retry = false; try { @@ -813,7 +813,9 @@ describe("startClaudeIntercept wiring", () => { pickerSecurity: trust.run, pickerCaStore: store, pickerPlatform: "darwin" as NodeJS.Platform, startProxy: (requested: number, options: Parameters[1]) => - startConnectProxy(requested === pickerPort && !retry ? pickerPort : 0, options), + requested === pickerPort && retry + ? egress.startProxy(requested, options) + : startConnectProxy(requested === pickerPort ? pickerPort : 0, options), }); let first; try { @@ -827,12 +829,14 @@ describe("startClaudeIntercept wiring", () => { } finally { await first?.stop(); } - // The next restart gets a fresh listener; the surviving selection restores in place. + // The next restart adopts the held relay; the surviving selection restores in place. retry = true; const second = await startClaudeIntercept(startOpts()); try { expect(second?.pickerProxyPort).toBeGreaterThan(0); + expect(second?.pickerProxyPort).toBe(pickerPort); const restoredUrl = `http://127.0.0.1:${second!.pickerProxyPort}`; + expect(restoredUrl).toBe(original.proxyUrl); for (let i = 0; i < 1600; i += 1) { const current = inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }); if (current.kind === "applied" && current.proxyUrl === restoredUrl) break; @@ -843,7 +847,7 @@ describe("startClaudeIntercept wiring", () => { } finally { await second?.stop(); } - } finally { await squatter.stop(true); } + } finally { await egress.close(); } }); test("only the app's browser tunnels on Desktop's egress proxy consult the picker", async () => { diff --git a/tests/fixtures/minimax-bridge-direct.ts b/tests/fixtures/minimax-bridge-direct.ts index dcce715eb6a..861335d7154 100644 --- a/tests/fixtures/minimax-bridge-direct.ts +++ b/tests/fixtures/minimax-bridge-direct.ts @@ -1,6 +1,5 @@ import { startMmxTextBridge } from "../../src/cli/minimax"; -let proxyRequests = 0; let upstreamBody = ""; const upstream = Bun.serve({ hostname: "127.0.0.1", @@ -10,14 +9,6 @@ const upstream = Bun.serve({ return Response.json({ source: "upstream" }); }, }); -const attackerProxy = Bun.serve({ - hostname: "127.0.0.1", - port: Number(process.env.TEST_PROXY_PORT), - fetch() { - proxyRequests += 1; - return Response.json({ source: "proxy" }); - }, -}); const bridge = startMmxTextBridge({ hostname: "127.0.0.1", port: upstream.port }); try { @@ -31,11 +22,9 @@ try { console.log(JSON.stringify({ responseStatus: response.status, responseText, - proxyRequests, upstreamBody, })); } finally { await bridge.stop(); await upstream.stop(true); - await attackerProxy.stop(true); } diff --git a/tests/providers/minimax-clients.test.ts b/tests/providers/minimax-clients.test.ts index 9706c440611..2077833b2eb 100644 --- a/tests/providers/minimax-clients.test.ts +++ b/tests/providers/minimax-clients.test.ts @@ -152,45 +152,48 @@ describe("MiniMax Code client config", () => { describe("MiniMax CLI wrapper", () => { test("keeps the bridge's upstream hop direct when the parent has a proxy", async () => { - const reservation = createServer(); - const proxyPort = await new Promise((resolve, reject) => { - reservation.once("error", reject); - reservation.listen(0, "127.0.0.1", () => { - const address = reservation.address(); - if (!address || typeof address === "string") reject(new Error("proxy port reservation failed")); - else resolve(address.port); - }); - }); - await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve())); - - const child = Bun.spawn([process.execPath, fixturePath("minimax-bridge-direct.ts")], { - env: { - ...process.env, - HTTP_PROXY: `http://127.0.0.1:${proxyPort}`, - HTTPS_PROXY: `http://127.0.0.1:${proxyPort}`, - ALL_PROXY: `http://127.0.0.1:${proxyPort}`, - http_proxy: `http://127.0.0.1:${proxyPort}`, - https_proxy: `http://127.0.0.1:${proxyPort}`, - all_proxy: `http://127.0.0.1:${proxyPort}`, - NO_PROXY: "", - no_proxy: "", - TEST_PROXY_PORT: String(proxyPort), + let proxyRequests = 0; + const attackerProxy = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + proxyRequests += 1; + return Response.json({ source: "proxy" }); }, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(child.stdout).text(), - new Response(child.stderr).text(), - child.exited, - ]); - expect(exitCode, stderr).toBe(0); - expect(JSON.parse(stdout.trim())).toEqual({ - responseStatus: 200, - responseText: JSON.stringify({ source: "upstream" }), - proxyRequests: 0, - upstreamBody: JSON.stringify({ prompt: "private bridge payload" }), }); + const proxyPort = attackerProxy.port; + try { + // This is the actual canary, held for the child's entire lifetime. + const child = Bun.spawn([process.execPath, fixturePath("minimax-bridge-direct.ts")], { + env: { + ...process.env, + HTTP_PROXY: `http://127.0.0.1:${proxyPort}`, + HTTPS_PROXY: `http://127.0.0.1:${proxyPort}`, + ALL_PROXY: `http://127.0.0.1:${proxyPort}`, + http_proxy: `http://127.0.0.1:${proxyPort}`, + https_proxy: `http://127.0.0.1:${proxyPort}`, + all_proxy: `http://127.0.0.1:${proxyPort}`, + NO_PROXY: "", + no_proxy: "", + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]); + expect(exitCode, stderr).toBe(0); + expect(JSON.parse(stdout.trim())).toEqual({ + responseStatus: 200, + responseText: JSON.stringify({ source: "upstream" }), + upstreamBody: JSON.stringify({ prompt: "private bridge payload" }), + }); + expect(proxyRequests).toBe(0); + } finally { + await attackerProxy.stop(true); + } }); test("passes through only standalone help and officially supported version invocations", () => {