From 1133447949ba6bb0b5f12140a18ad66acf596bdc Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 04:08:29 +0900 Subject: [PATCH 1/2] test(rust): derive toolchain homes from the rustup proxy and report them on failure --- .../link-ocx-child-conformance.test.ts | 20 ++++------ tests/clients/link-ocx-child-e2e.test.ts | 18 ++++----- tests/helpers/rust-toolchain-env.ts | 40 +++++++++++++++++++ .../service/service-desktop-recovery.test.ts | 13 +++--- 4 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 tests/helpers/rust-toolchain-env.ts diff --git a/tests/clients/link-ocx-child-conformance.test.ts b/tests/clients/link-ocx-child-conformance.test.ts index d431528fbac..1d39f3d8c46 100644 --- a/tests/clients/link-ocx-child-conformance.test.ts +++ b/tests/clients/link-ocx-child-conformance.test.ts @@ -1,10 +1,10 @@ // Holds INV-LINK-CHILD-01 from structure/overview.md; shared TypeScript/Rust golden conformance suite for native/ocx-child. import { createHash, createHmac } from "node:crypto"; -import { existsSync, readFileSync } from "node:fs"; -import { userInfo } from "node:os"; +import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "bun:test"; import { fixturePath, repoPath } from "../helpers/repo-root"; +import { rustToolchainEnv } from "../helpers/rust-toolchain-env"; import { linkRouteAllowed } from "../../src/link/routes"; import { linkManagementRouteAllowed } from "../../src/link/management-routes"; import { isDetachedEphemeralChildConfig, isEphemeralChildHealth } from "../../src/link/ephemeral-child"; @@ -633,20 +633,14 @@ describe("ocx-child shared golden conformance vectors (INV-LINK-CHILD-01)", () = }, 10100)).toBe(false); }); - const exeSuffix = process.platform === "win32" ? ".exe" : ""; - const realHome = process.env.OCX_REAL_HOME?.trim() || userInfo().homedir; - const cargoBin = Bun.which("cargo") ?? join(realHome, ".cargo", "bin", `cargo${exeSuffix}`); - const cargoAvailable = existsSync(cargoBin); + const cargo = rustToolchainEnv("cargo"); + const cargoAvailable = cargo.bin !== null; test.skipIf(!cargoAvailable)("Rust conformance runner passes against the identical shared fixture", () => { const run = Bun.spawnSync( - [cargoBin, "test", "--locked", "--manifest-path", repoPath("native", "ocx-child", "Cargo.toml"), "--test", "conformance"], + [cargo.bin!, "test", "--locked", "--manifest-path", repoPath("native", "ocx-child", "Cargo.toml"), "--test", "conformance"], { - env: { - ...process.env, - RUSTUP_HOME: process.env.RUSTUP_HOME || join(realHome, ".rustup"), - CARGO_HOME: process.env.CARGO_HOME || join(realHome, ".cargo"), - }, + env: cargo.env, stdout: "pipe", stderr: "pipe", timeout: 90_000, @@ -655,6 +649,6 @@ describe("ocx-child shared golden conformance vectors (INV-LINK-CHILD-01)", () = if (run.signalCode) { throw new Error(`cargo test conformance terminated by signal ${run.signalCode} (90s timeout check):\n${run.stderr.toString()}`); } - expect(run.exitCode, run.stderr.toString()).toBe(0); + expect(run.exitCode, `${run.stderr.toString()}\ntoolchain: ${cargo.describe()}`).toBe(0); }, 90_000); }); diff --git a/tests/clients/link-ocx-child-e2e.test.ts b/tests/clients/link-ocx-child-e2e.test.ts index c886363bd72..bbf4c325a02 100644 --- a/tests/clients/link-ocx-child-e2e.test.ts +++ b/tests/clients/link-ocx-child-e2e.test.ts @@ -2,10 +2,11 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; import { createServer as createNetServer } from "node:net"; -import { tmpdir, userInfo } from "node:os"; +import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { repoPath } from "../helpers/repo-root"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { rustToolchainEnv } from "../helpers/rust-toolchain-env"; import { classifySyncResult, runHomeFanout } from "../../src/cli/sync-fanout"; import { serviceApiTokenFingerprint } from "../../src/lib/service-secrets"; import { homeLinkChildParity } from "../../src/link/child-parity"; @@ -29,24 +30,19 @@ function findOrBuildOcxChildBinary(): string { const targetDir = cargoTargetDir ? resolve(cargoTargetDir) : repoPath("native", "ocx-child", "target"); const releaseBin = join(targetDir, "release", `ocx-child${exeSuffix}`); const debugBin = join(targetDir, "debug", `ocx-child${exeSuffix}`); - const realHome = process.env.OCX_REAL_HOME?.trim() || userInfo().homedir; - const cargo = Bun.which("cargo") ?? join(realHome, ".cargo", "bin", `cargo${exeSuffix}`); - if (existsSync(cargo)) { + const cargo = rustToolchainEnv("cargo"); + if (cargo.bin) { const build = Bun.spawnSync( - [cargo, "build", "--locked", "--manifest-path", repoPath("native", "ocx-child", "Cargo.toml")], + [cargo.bin, "build", "--locked", "--manifest-path", repoPath("native", "ocx-child", "Cargo.toml")], { - env: { - ...process.env, - RUSTUP_HOME: process.env.RUSTUP_HOME || join(realHome, ".rustup"), - CARGO_HOME: process.env.CARGO_HOME || join(realHome, ".cargo"), - }, + env: cargo.env, stdout: "pipe", stderr: "pipe", timeout: 90_000, }, ); if (build.exitCode !== 0) { - throw new Error(`cargo build failed: ${build.stderr.toString()}`); + throw new Error(`cargo build failed: ${build.stderr.toString()}\ntoolchain: ${cargo.describe()}`); } return debugBin; } diff --git a/tests/helpers/rust-toolchain-env.ts b/tests/helpers/rust-toolchain-env.ts new file mode 100644 index 00000000000..4dfcf5448a7 --- /dev/null +++ b/tests/helpers/rust-toolchain-env.ts @@ -0,0 +1,40 @@ +import { existsSync, realpathSync } from "node:fs"; +import { userInfo } from "node:os"; +import { basename, dirname, join } from "node:path"; + +/** + * Rust toolchain environment for tests that build or run native fixtures. + * + * The preload sandboxes HOME/USERPROFILE, and rustup finds its toolchains from RUSTUP_HOME, which + * hosted runners leave unset. Guessing the home from OCX_REAL_HOME or userInfo() has failed on + * Windows only when another file ran first in the same Bun process ("rustup could not choose a + * version of cargo ... no default is configured"; batches 42499262970, 42580170093, 42672304851). + * The rustup proxy's own location names the installation it belongs to, so derive both homes from + * it, and fall back to the real-home guess only when the binary is not inside a .cargo/bin. + */ +export function rustToolchainEnv(tool: "cargo" | "rustc"): { bin: string | null; env: Record; describe: () => string } { + const exeSuffix = process.platform === "win32" ? ".exe" : ""; + const realHome = process.env.OCX_REAL_HOME?.trim() || userInfo().homedir; + const found = Bun.which(tool) ?? join(realHome, ".cargo", "bin", `${tool}${exeSuffix}`); + const bin = existsSync(found) ? found : null; + let cargoHome = process.env.CARGO_HOME || join(realHome, ".cargo"); + let rustupHome = process.env.RUSTUP_HOME || join(realHome, ".rustup"); + if (bin && !process.env.CARGO_HOME && !process.env.RUSTUP_HOME) { + let resolved = bin; + try { resolved = realpathSync(bin); } catch { /* keep the PATH spelling */ } + const binDir = dirname(resolved); + if (basename(binDir) === "bin" && basename(dirname(binDir)) === ".cargo") { + cargoHome = dirname(binDir); + const sibling = join(dirname(cargoHome), ".rustup"); + if (existsSync(sibling)) rustupHome = sibling; + } + } + const env = { ...process.env, CARGO_HOME: cargoHome, RUSTUP_HOME: rustupHome }; + const describe = () => JSON.stringify({ + bin, CARGO_HOME: cargoHome, RUSTUP_HOME: rustupHome, + rustupSettings: existsSync(join(rustupHome, "settings.toml")), + OCX_REAL_HOME: process.env.OCX_REAL_HOME ?? null, USERPROFILE: process.env.USERPROFILE ?? null, HOME: process.env.HOME ?? null, + }); + return { bin, env, describe }; +} + diff --git a/tests/service/service-desktop-recovery.test.ts b/tests/service/service-desktop-recovery.test.ts index 2f38e350029..f3b3876462d 100644 --- a/tests/service/service-desktop-recovery.test.ts +++ b/tests/service/service-desktop-recovery.test.ts @@ -2,13 +2,14 @@ import { describe, expect, test } from "bun:test"; import { ptr, toArrayBuffer } from "bun:ffi"; import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { connect, createServer, type Socket } from "node:net"; -import { tmpdir, userInfo } from "node:os"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { inspectDesktopRecovery, parseRecoveryAnswer, recoveryHomeIdentity, type DesktopRecoveryIo } from "../../src/service/desktop-recovery"; import { linuxRecoveryBirth, linuxRecoveryListeners, linuxRecoverySocket, macRecoveryPaths, macRecoveryRequirement, parseMacRecoveryPath, recoveryImageCandidate, recoverySibling, verifyMacRecoveryImage } from "../../src/service/desktop-recovery-process"; import { repoPath } from "../helpers/repo-root"; +import { rustToolchainEnv } from "../helpers/rust-toolchain-env"; import { SPAWN_BUDGET_MS } from "../helpers/test-budget"; const instance = "a".repeat(32); @@ -331,15 +332,11 @@ import { runDesktopColdStart, productionDesktopColdStartDeps } from ${JSON.strin const evidence = await inspectDesktopRecovery(); const result = await runDesktopColdStart({ ...productionDesktopColdStartDeps(async () => ({ fixture: true })), desktopRecovery: async () => evidence, info: () => {} }); console.log(JSON.stringify({ evidence, result }));`); // The preload sandboxes HOME; rustup must still find the installed toolchain (#7884). - const realHome = process.env.OCX_REAL_HOME?.trim() || userInfo().homedir; - const rustEnv = { ...process.env, - RUSTUP_HOME: process.env.RUSTUP_HOME || join(realHome, ".rustup"), - CARGO_HOME: process.env.CARGO_HOME || join(realHome, ".cargo"), - }; + const rustc = rustToolchainEnv("rustc"); // Hosted runs killed rustc at 30s while other heads passed. Keep a bounded // real-child budget and report the compiler's output rather than only a null/1 exit. - const rustBuild = Bun.spawnSync([Bun.which("rustc")!, "--edition=2021", nativeSource, "-o", app], { env: rustEnv, stdout: "pipe", stderr: "pipe", timeout: SPAWN_BUDGET_MS }); - expect(rustBuild.exitCode, `rustc (signal=${rustBuild.signal}):\n${rustBuild.stderr.toString()}${rustBuild.stdout.toString()}`).toBe(0); + const rustBuild = Bun.spawnSync([rustc.bin ?? Bun.which("rustc")!, "--edition=2021", nativeSource, "-o", app], { env: rustc.env, stdout: "pipe", stderr: "pipe", timeout: SPAWN_BUDGET_MS }); + expect(rustBuild.exitCode, `rustc (signal=${rustBuild.signal}):\n${rustBuild.stderr.toString()}${rustBuild.stdout.toString()}\ntoolchain: ${rustc.describe()}`).toBe(0); const build = Bun.spawnSync([process.execPath, "build", "--compile", entry, "--outfile", cli], { stdout: "pipe", stderr: "pipe", timeout: 30_000 }); expect(build.exitCode, `bun build (signal=${build.signal}):\n${build.stderr.toString()}${build.stdout.toString()}`).toBe(0); const env = { ...process.env, XDG_CONFIG_HOME: join(root, "xdg"), OPENCODEX_HOME: config }; From a2c0fa82b37cf732e1c0ac2acc579a056f871d45 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 05:12:55 +0900 Subject: [PATCH 2/2] test(rust): derive each unset toolchain home independently and report the default toolchain --- tests/helpers/rust-toolchain-env.ts | 38 +++++++++++-------- .../service/service-desktop-recovery.test.ts | 3 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/tests/helpers/rust-toolchain-env.ts b/tests/helpers/rust-toolchain-env.ts index 4dfcf5448a7..2de01d27caf 100644 --- a/tests/helpers/rust-toolchain-env.ts +++ b/tests/helpers/rust-toolchain-env.ts @@ -1,4 +1,4 @@ -import { existsSync, realpathSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; import { userInfo } from "node:os"; import { basename, dirname, join } from "node:path"; @@ -7,10 +7,10 @@ import { basename, dirname, join } from "node:path"; * * The preload sandboxes HOME/USERPROFILE, and rustup finds its toolchains from RUSTUP_HOME, which * hosted runners leave unset. Guessing the home from OCX_REAL_HOME or userInfo() has failed on - * Windows only when another file ran first in the same Bun process ("rustup could not choose a - * version of cargo ... no default is configured"; batches 42499262970, 42580170093, 42672304851). - * The rustup proxy's own location names the installation it belongs to, so derive both homes from - * it, and fall back to the real-home guess only when the binary is not inside a .cargo/bin. + * Windows ("rustup could not choose a version of cargo ... no default is configured"; batches + * 42499262970, 42580170093, 42672304851). The rustup proxy's own location names the installation + * it belongs to, so each home that is not set explicitly is derived from it; the real-home guess + * remains only for a binary that is not inside a .cargo/bin. */ export function rustToolchainEnv(tool: "cargo" | "rustc"): { bin: string | null; env: Record; describe: () => string } { const exeSuffix = process.platform === "win32" ? ".exe" : ""; @@ -19,22 +19,28 @@ export function rustToolchainEnv(tool: "cargo" | "rustc"): { bin: string | null; const bin = existsSync(found) ? found : null; let cargoHome = process.env.CARGO_HOME || join(realHome, ".cargo"); let rustupHome = process.env.RUSTUP_HOME || join(realHome, ".rustup"); - if (bin && !process.env.CARGO_HOME && !process.env.RUSTUP_HOME) { + if (bin) { let resolved = bin; try { resolved = realpathSync(bin); } catch { /* keep the PATH spelling */ } const binDir = dirname(resolved); - if (basename(binDir) === "bin" && basename(dirname(binDir)) === ".cargo") { - cargoHome = dirname(binDir); - const sibling = join(dirname(cargoHome), ".rustup"); - if (existsSync(sibling)) rustupHome = sibling; + // rustup installs lowercase .cargo/bin; Windows paths compare case-insensitively. + const name = (path: string) => process.platform === "win32" ? basename(path).toLowerCase() : basename(path); + if (name(binDir) === "bin" && name(dirname(binDir)) === ".cargo") { + const derivedCargoHome = dirname(binDir); + if (!process.env.CARGO_HOME) cargoHome = derivedCargoHome; + const sibling = join(dirname(derivedCargoHome), ".rustup"); + if (!process.env.RUSTUP_HOME && existsSync(sibling)) rustupHome = sibling; } } const env = { ...process.env, CARGO_HOME: cargoHome, RUSTUP_HOME: rustupHome }; - const describe = () => JSON.stringify({ - bin, CARGO_HOME: cargoHome, RUSTUP_HOME: rustupHome, - rustupSettings: existsSync(join(rustupHome, "settings.toml")), - OCX_REAL_HOME: process.env.OCX_REAL_HOME ?? null, USERPROFILE: process.env.USERPROFILE ?? null, HOME: process.env.HOME ?? null, - }); + const describe = () => { + const settings = join(rustupHome, "settings.toml"); + let defaultToolchain: string | null = null; + try { defaultToolchain = /^default_toolchain\s*=\s*"([^"]*)"/m.exec(readFileSync(settings, "utf8"))?.[1] ?? null; } catch { /* absent */ } + return JSON.stringify({ + bin, CARGO_HOME: cargoHome, RUSTUP_HOME: rustupHome, rustupSettings: existsSync(settings), defaultToolchain, + OCX_REAL_HOME: process.env.OCX_REAL_HOME ?? null, USERPROFILE: process.env.USERPROFILE ?? null, HOME: process.env.HOME ?? null, + }); + }; return { bin, env, describe }; } - diff --git a/tests/service/service-desktop-recovery.test.ts b/tests/service/service-desktop-recovery.test.ts index f3b3876462d..bb8872671b2 100644 --- a/tests/service/service-desktop-recovery.test.ts +++ b/tests/service/service-desktop-recovery.test.ts @@ -333,9 +333,10 @@ const evidence = await inspectDesktopRecovery(); const result = await runDesktop console.log(JSON.stringify({ evidence, result }));`); // The preload sandboxes HOME; rustup must still find the installed toolchain (#7884). const rustc = rustToolchainEnv("rustc"); + if (!rustc.bin) throw new Error(`no rustc available to build the native recovery fixture\ntoolchain: ${rustc.describe()}`); // Hosted runs killed rustc at 30s while other heads passed. Keep a bounded // real-child budget and report the compiler's output rather than only a null/1 exit. - const rustBuild = Bun.spawnSync([rustc.bin ?? Bun.which("rustc")!, "--edition=2021", nativeSource, "-o", app], { env: rustc.env, stdout: "pipe", stderr: "pipe", timeout: SPAWN_BUDGET_MS }); + const rustBuild = Bun.spawnSync([rustc.bin, "--edition=2021", nativeSource, "-o", app], { env: rustc.env, stdout: "pipe", stderr: "pipe", timeout: SPAWN_BUDGET_MS }); expect(rustBuild.exitCode, `rustc (signal=${rustBuild.signal}):\n${rustBuild.stderr.toString()}${rustBuild.stdout.toString()}\ntoolchain: ${rustc.describe()}`).toBe(0); const build = Bun.spawnSync([process.execPath, "build", "--compile", entry, "--outfile", cli], { stdout: "pipe", stderr: "pipe", timeout: 30_000 }); expect(build.exitCode, `bun build (signal=${build.signal}):\n${build.stderr.toString()}${build.stdout.toString()}`).toBe(0);