From 03826b80759795ec1783cdfd37e8060f28ea55a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 29 Mar 2026 00:19:57 +0000 Subject: [PATCH] fix(test): harden planner artifact cleanup and profile env fallback --- scripts/test-planner/executor.mjs | 10 ++++- test/scripts/test-planner.test.ts | 19 +++++++++ test/test-env.test.ts | 29 ++++++++++++- test/test-env.ts | 69 +++++++++++++++++++++++-------- 4 files changed, 107 insertions(+), 20 deletions(-) diff --git a/scripts/test-planner/executor.mjs b/scripts/test-planner/executor.mjs index 9a0ea2a2a81..5956d755f6f 100644 --- a/scripts/test-planner/executor.mjs +++ b/scripts/test-planner/executor.mjs @@ -166,6 +166,14 @@ export function createExecutionArtifacts(env = process.env) { return { ensureTempArtifactDir, writeTempJsonArtifact, cleanupTempArtifacts }; } +export function createTempArtifactWriteStream(filePath) { + const fd = fs.openSync(filePath, "w"); + return fs.createWriteStream(filePath, { + fd, + autoClose: true, + }); +} + const ensureNodeOptionFlag = (nodeOptions, flagPrefix, nextValue) => nodeOptions.includes(flagPrefix) ? nodeOptions : `${nodeOptions} ${nextValue}`.trim(); @@ -420,7 +428,7 @@ export async function executePlan(plan, options = {}) { .filter(Boolean) .join("-"); const laneLogPath = path.join(artifacts.ensureTempArtifactDir(), `${artifactStem}.log`); - const laneLogStream = fs.createWriteStream(laneLogPath, { flags: "w" }); + const laneLogStream = createTempArtifactWriteStream(laneLogPath); laneLogStream.write(`[test-parallel] entry=${unit.id}\n`); laneLogStream.write(`[test-parallel] cwd=${process.cwd()}\n`); laneLogStream.write( diff --git a/test/scripts/test-planner.test.ts b/test/scripts/test-planner.test.ts index c5ecbce2512..b28f6bf2f6f 100644 --- a/test/scripts/test-planner.test.ts +++ b/test/scripts/test-planner.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { createExecutionArtifacts, + createTempArtifactWriteStream, resolvePnpmCommandInvocation, resolveVitestFsModuleCachePath, } from "../../scripts/test-planner/executor.mjs"; @@ -348,6 +349,24 @@ describe("test planner", () => { expect(fs.existsSync(artifactDir)).toBe(false); }); + it("keeps fd-backed artifact streams writable after temp cleanup", async () => { + const artifacts = createExecutionArtifacts({}); + const artifactDir = artifacts.ensureTempArtifactDir(); + const logPath = path.join(artifactDir, "lane.log"); + const stream = createTempArtifactWriteStream(logPath); + + stream.write("before cleanup\n"); + artifacts.cleanupTempArtifacts(); + + await expect( + new Promise((resolve, reject) => { + stream.on("error", reject); + stream.end("after cleanup\n", resolve); + }), + ).resolves.toBeNull(); + expect(fs.existsSync(artifactDir)).toBe(false); + }); + it("builds a CI manifest with planner-owned shard counts and matrices", () => { const manifest = buildCIExecutionManifest( { diff --git a/test/test-env.test.ts b/test/test-env.test.ts index c57afa0c2c7..05174a8be03 100644 --- a/test/test-env.test.ts +++ b/test/test-env.test.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { importFreshModule } from "./helpers/import-fresh.js"; import { installTestEnv } from "./test-env.js"; const ORIGINAL_ENV = { ...process.env }; @@ -136,4 +137,30 @@ describe("installTestEnv", () => { expect(process.env.HOME).toBe(realHome); expect(process.env.TEST_PROFILE_ONLY).toBe("from-profile"); }); + + it("falls back to parsing ~/.profile when bash is unavailable", async () => { + const realHome = createTempHome(); + writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n"); + + process.env.HOME = realHome; + process.env.USERPROFILE = realHome; + process.env.OPENCLAW_LIVE_TEST = "1"; + process.env.OPENCLAW_LIVE_USE_REAL_HOME = "1"; + process.env.OPENCLAW_LIVE_TEST_QUIET = "1"; + + vi.doMock("node:child_process", () => ({ + execFileSync: () => { + throw Object.assign(new Error("bash missing"), { code: "ENOENT" }); + }, + })); + + const { installTestEnv: installFreshTestEnv } = await importFreshModule< + typeof import("./test-env.js") + >(import.meta.url, "./test-env.js?scope=profile-fallback"); + + const testEnv = installFreshTestEnv(); + + expect(testEnv.tempHome).toBe(realHome); + expect(process.env.TEST_PROFILE_ONLY).toBe("from-profile"); + }); }); diff --git a/test/test-env.ts b/test/test-env.ts index 3c024fc9529..ef9d5277498 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -50,34 +50,67 @@ function loadProfileEnv(homeDir = os.homedir()): void { if (!fs.existsSync(profilePath)) { return; } + const applyEntry = (entry: string) => { + const idx = entry.indexOf("="); + if (idx <= 0) { + return false; + } + const key = entry.slice(0, idx).trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || (process.env[key] ?? "") !== "") { + return false; + } + process.env[key] = entry.slice(idx + 1); + return true; + }; + const countAppliedEntries = (entries: Iterable) => { + let applied = 0; + for (const entry of entries) { + if (applyEntry(entry)) { + applied += 1; + } + } + return applied; + }; try { const output = execFileSync( "/bin/bash", ["-lc", `set -a; source "${profilePath}" >/dev/null 2>&1; env -0`], { encoding: "utf8" }, ); - const entries = output.split("\0"); - let applied = 0; - for (const entry of entries) { - if (!entry) { - continue; - } - const idx = entry.indexOf("="); - if (idx <= 0) { - continue; - } - const key = entry.slice(0, idx); - if (!key || (process.env[key] ?? "") !== "") { - continue; - } - process.env[key] = entry.slice(idx + 1); - applied += 1; - } + const applied = countAppliedEntries(output.split("\0").filter(Boolean)); if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) { console.log(`[live] loaded ${applied} env vars from ~/.profile`); } } catch { - // ignore profile load failures + try { + const fallbackEntries = fs + .readFileSync(profilePath, "utf8") + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .map((line) => line.replace(/^export\s+/u, "")) + .map((line) => { + const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u); + if (!match) { + return ""; + } + let value = match[2].trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + return `${match[1]}=${value}`; + }) + .filter(Boolean); + const applied = countAppliedEntries(fallbackEntries); + if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) { + console.log(`[live] loaded ${applied} env vars from ~/.profile`); + } + } catch { + // ignore profile load failures + } } }