From dc3f7600ba6d9475aac6b5d8370adf8fec22f023 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 13:47:43 +0900 Subject: [PATCH] test(server): allocate management-auth listener ports at bind time --- .../022_management_auth_port_fixture.md | 27 ++++++++++++ tests/server/server-management-auth.test.ts | 44 +++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md diff --git a/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md b/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md new file mode 100644 index 0000000000..f74adf55c4 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md @@ -0,0 +1,27 @@ +# Check-phase port allocation repair + +CI34011124632 passed the corrected task-input cases and Linux shards, but two +unchanged macOS management-auth tests failed at the public Bun.serve bind with +EADDRINUSE. Both tests used findAvailablePort, whose Node probe closes its socket +before returning a number. The probes bind 127.0.0.1 while remoteConfig makes the public listener bind 0.0.0.0, so a loopback-only availability check also has the wrong address scope. reservedPort prevents the two selected numbers from +being equal; it does not keep either port reserved until Bun binds. The identity +of the intervening occupier is not established by the CI log. + +This is a prerequisite repair to the failing verification instrument, not a +change to authentication or production port policy. Modify only +tests/server/server-management-auth.test.ts: replace those two probe-close +setups with a small test helper that wraps Bun.serve synchronously, changes only +port to zero, calls the real Bun.serve and captures the real public/management +listeners while preserving each original hostname and fetch handler. Restore the spy before requests or any awaited cleanup. Derive the +management URL from its actual listener port and assert distinct live listeners. +Keep a valid positive configured ingress port so production config validation +remains unchanged; the fixture explicitly owns ephemeral bind allocation. + +The helper joins captured-listener cleanup if startup/fixture validation fails; +the existing finally blocks continue using the real composite server.stop. +Retain every trust, origin, credential, health, consent and pairing assertion. +No retry, sleep, skip, wider auth rule, or production test seam is added. + +Verification: independent fixture review followed by fresh exact-head hosted +CI. The same two real HTTP tests must pass, along with the new task-input cases +and full Linux/macOS checks. No local test suite is run. diff --git a/tests/server/server-management-auth.test.ts b/tests/server/server-management-auth.test.ts index aece9bc93a..c1bc56f70a 100644 --- a/tests/server/server-management-auth.test.ts +++ b/tests/server/server-management-auth.test.ts @@ -5,7 +5,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getConfigPath, saveConfig } from "../../src/config"; import { startServer } from "../../src/server"; -import { findAvailablePort } from "../../src/server/ports"; import type { OcxConfig } from "../../src/types"; import { serveGuiFile, serveSessionBootstrap } from "../../src/server/gui-static"; import { isProxyAdmissionSecret } from "../../src/server/auth-cors"; @@ -116,6 +115,37 @@ function hubConfig(publicOrigin = "https://hub.example.test"): OcxConfig { }; } +/** Keep real ingress/handlers while the kernel allocates both ports at the actual bind. */ +async function startEphemeralHubServer(deps: Parameters[1]) { + const nativeServe = Bun.serve.bind(Bun); + const listeners: Array> = []; + const hostnames: unknown[] = []; + const serveSpy = spyOn(Bun, "serve").mockImplementation((options) => { + const listener = nativeServe({ ...options, port: 0 } as Parameters[0]); + listeners.push(listener); + hostnames.push("hostname" in options ? options.hostname : undefined); + return listener; + }); + try { + let server: ReturnType; + try { + server = startServer(0, deps); + } finally { + // startServer is synchronous; restore before requests or any awaited cleanup. + serveSpy.mockRestore(); + } + expect(listeners).toHaveLength(2); + expect(listeners[0]).toBe(server); + expect(hostnames).toEqual(["0.0.0.0", "127.0.0.1"]); + const managementPort = listeners[1]?.port; + if (!managementPort || managementPort === server.port) throw new Error("expected distinct live ingress ports"); + return { server, managementPort }; + } catch (error) { + await Promise.allSettled(listeners.map(async listener => { await listener.stop(true); })); + throw error; + } +} + function websocketHandshakeOpens(url: URL, token: string): Promise { return new Promise(resolve => { const target = new URL("/v1/responses", url); @@ -978,17 +1008,15 @@ describe("management and data-plane credential separation", () => { }); test("the live listener trusts Tailscale identity only on hub management ingress", async () => { - const managementPort = await findAvailablePort(0, "127.0.0.1"); - const publicPort = await findAvailablePort(0, "127.0.0.1", { reservedPort: managementPort }); const config = hubConfig(); config.hub = { ...config.hub, - managementIngress: { enabled: true, port: managementPort }, + managementIngress: { enabled: true, port: 10101 }, }; saveConfig(config); const state = initializeManagementAuthState(config); if (!state.available) throw new Error("expected management auth state"); - const server = startServer(publicPort, { managementAuthState: state }); + const { server, managementPort } = await startEphemeralHubServer({ managementAuthState: state }); const headers = { Host: "hub.example.test", "Tailscale-User-Login": "alice@example.test" }; try { const spoofedPublic = await fetch(new URL("/opencodex-session", server.url), { headers }); @@ -1164,15 +1192,13 @@ describe("management and data-plane credential separation", () => { }); test("the management ingress preserves the one-use pairing exchange contract", async () => { - const managementPort = await findAvailablePort(0, "127.0.0.1"); - const publicPort = await findAvailablePort(0, "127.0.0.1", { reservedPort: managementPort }); const config = hubConfig(); - config.hub = { ...config.hub, managementIngress: { enabled: true, port: managementPort } }; + config.hub = { ...config.hub, managementIngress: { enabled: true, port: 10101 } }; saveConfig(config); const state = initializeManagementAuthState(config); if (!state.available) throw new Error("expected management auth state"); const created = createGuiPairingGrant("https://dashboard.example.test", config, state); - const server = startServer(publicPort, { managementAuthState: state }); + const { server, managementPort } = await startEphemeralHubServer({ managementAuthState: state }); const url = `http://127.0.0.1:${managementPort}/opencodex-session`; const headers = { Host: "hub.example.test",