diff --git a/CLAUDE.md b/CLAUDE.md index 741135c56..cd44bbcec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,6 @@ npm run generate-types ```sh ./scripts/install-binaries -npm run build:http ``` ## Backend architecture diff --git a/tests/support/globalSetup.ts b/tests/support/globalSetup.ts index 2faacc67a..620a3474c 100644 --- a/tests/support/globalSetup.ts +++ b/tests/support/globalSetup.ts @@ -14,6 +14,7 @@ import { removeWorkspace, tmpDir, } from "@tests/support/support.js"; +import { execa } from "execa"; const heartwoodBinaryPath = Path.join( tmpDir, @@ -43,6 +44,13 @@ export default async function globalSetup(): Promise<() => void> { await removeWorkspace(); } + console.log("Building test-http-api"); + await execa("cargo", [ + "build", + "--manifest-path", + "./crates/test-http-api/Cargo.toml", + ]); + const peerManager = await createPeerManager({ dataDir: Path.resolve(tmpDir, "peers"), outputLog: Fs.createWriteStream( @@ -67,7 +75,6 @@ export default async function globalSetup(): Promise<() => void> { alias: "palm", }, }); - await palm.startHttpd(parseInt(process.env.VITE_TEST_HTTP_API_PORT ?? "0")); try { console.log("Creating markdown fixture"); @@ -85,9 +92,8 @@ export default async function globalSetup(): Promise<() => void> { process.exit(1); } await palm.stopNode(); - } else { - await palm.startHttpd(parseInt(process.env.VITE_TEST_HTTP_API_PORT ?? "0")); } + await palm.startHttpd(parseInt(process.env.VITE_TEST_HTTP_API_PORT ?? "0")); return async () => { await peerManager.shutdown(); diff --git a/tests/support/peerManager.ts b/tests/support/peerManager.ts index 5ac4e4ca1..0223cf3ae 100644 --- a/tests/support/peerManager.ts +++ b/tests/support/peerManager.ts @@ -9,7 +9,7 @@ import * as Util from "node:util"; import { type Config, defaultConfig } from "@tests/support/fixtures.js"; import { logPrefix } from "@tests/support/logPrefix.js"; -import { randomTag } from "@tests/support/support.js"; +import { randomTag, testHttpApiBinary } from "@tests/support/support.js"; import { execa } from "execa"; import getPort from "get-port"; import matches from "lodash/matches.js"; @@ -251,30 +251,20 @@ export class RadiclePeer { await this.spawn("rad", ["auth"]); } + // Without a port the OS picks a free one, which the server reports back. + // Choosing one up front raced between workers: the second server failed to + // bind, and its page talked to the other worker's server instead. public async startHttpd(port?: number): Promise { - if (!port) { - port = await getPort(); - } - this.#httpdBaseUrl = { - hostname: "127.0.0.1", - port, - scheme: "http", - }; - void this.spawn("cargo", [ - "run", - "--manifest-path", - "./crates/test-http-api/Cargo.toml", - "--", + const httpd = this.spawn(testHttpApiBinary, [ "--listen", - `${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`, + `127.0.0.1:${port ?? 0}`, ]); - - await waitOn({ - resources: [ - `tcp:${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`, - ], - timeout: 120_000, - }); + if (port) { + await waitOn({ resources: [`tcp:127.0.0.1:${port}`], timeout: 10_000 }); + } else { + port = await reportedPort(httpd); + } + this.#httpdBaseUrl = { hostname: "127.0.0.1", port, scheme: "http" }; } public async startNode(config: Partial = defaultConfig) { @@ -444,3 +434,29 @@ async function updateConfig(radHome: string, configParams: Partial) { }; await Fs.writeFile(configPath, JSON.stringify(config), "utf-8"); } + +async function reportedPort(httpd: SpawnResult): Promise { + if (!httpd.stdout) { + throw new Error("test-http-api has no stdout to read its port from"); + } + const lines = readline.createInterface({ input: httpd.stdout }); + const exited = httpd.then( + () => { + throw new Error("test-http-api exited before it was listening"); + }, + (error: unknown) => { + throw error; + }, + ); + const listening = (async () => { + for await (const line of lines) { + const match = /^listening on .*:(\d+)$/.exec(line); + if (match) { + lines.close(); + return Number(match[1]); + } + } + throw new Error("test-http-api closed its output before it was listening"); + })(); + return Promise.race([listening, exited]); +} diff --git a/tests/support/support.ts b/tests/support/support.ts index 85608f47a..b951f0a31 100644 --- a/tests/support/support.ts +++ b/tests/support/support.ts @@ -24,6 +24,13 @@ const filename = fileURLToPath(import.meta.url); export const supportDir = Path.dirname(filename); export const tmpDir = Path.resolve(supportDir, "..", "./tmp"); export const fixturesDir = Path.resolve(supportDir, "..", "./fixtures"); +export const testHttpApiBinary = Path.resolve( + supportDir, + "../..", + process.env.CARGO_TARGET_DIR ?? "target", + "debug", + "test-http-api", +); const workspacePaths = [Path.join(tmpDir, "peers"), Path.join(tmpDir, "repos")]; export const heartwoodRelease = (