From 12253cc5192dd0f0219e61ba410f8206ecee6378 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 29 Sep 2026 19:04:44 -0700 Subject: [PATCH] test(pr): serve fake GitHub in-process for Node PR helpers Use one fixture handler for the fake GitHub CLI and fixture-scoped Node preload, avoiding repeated Node startup for synchronous fake-gh reads. Real Git operations and process supervision keep their native boundaries. In a 4-CPU Linux container with Node 24.21.0, all 20 importer files passed 728/728 tests in both variants. Suite wall fell 36.4%, from 1,070.10s (17m50s) to 680.06s (11m20s); the main-drift settlement case fell from 16.213s to 7.096s under its unchanged 20-second deadline. Production code, deadlines, test cases and assertions are unchanged. This reduces but does not eliminate load-sensitive deadline risk; it is not claimed as a complete flake fix. The loaded comparison was incomplete. Validation: full Linux importer comparison, check-changed, four root test type shards, formatting and boundary lint, Knip, source-contract checks, and independent P2 review. --- .../pr-merge-gh-process.test-support.ts | 99 +++++++++++++++++++ test/scripts/pr-merge-outcome.test-support.ts | 18 ++-- 2 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 test/scripts/pr-merge-gh-process.test-support.ts diff --git a/test/scripts/pr-merge-gh-process.test-support.ts b/test/scripts/pr-merge-gh-process.test-support.ts new file mode 100644 index 000000000000..a59a913a50b2 --- /dev/null +++ b/test/scripts/pr-merge-gh-process.test-support.ts @@ -0,0 +1,99 @@ +/** Keep fake GitHub responses in the requesting Node helper; real Git stays native. */ +export function createMergeGhFixturePrograms(body: string) { + return { + cli: String.raw` +"use strict"; +const nativeFs = require("node:fs"); +const { execFileSync, spawnSync } = require("node:child_process"); +const { createHash } = require("node:crypto"); +const { format } = require("node:util"); +const hostProcess = process; +module.exports = function runGh(route, args, options = {}) { + let stdout = "", stderr = "", status = 0; + const stopped = {}; + const process = { + env: options.env ?? hostProcess.env, + execArgv: hostProcess.execArgv, + kill: (pid, signal) => hostProcess.kill(pid, signal), + stdout: { write: (value) => { stdout += value; return true; } }, + stderr: { write: (value) => { stderr += value; return true; } }, + exit(code = 0) { status = code; throw stopped; }, + }; + const console = { + log: (...values) => { stdout += format(...values) + "\n"; }, + error: (...values) => { stderr += format(...values) + "\n"; }, + }; + const fs = { + ...nativeFs, + readFileSync(file, encoding) { + if (file !== 0 || options.input === undefined) return nativeFs.readFileSync(file, encoding); + const bytes = Buffer.from(options.input); + const selected = typeof encoding === "string" ? encoding : encoding?.encoding; + return selected ? bytes.toString(selected) : bytes; + }, + }; + try { +${body} + } catch (error) { + if (error !== stopped) { status = 1; stderr += String(error?.stack ?? error) + "\n"; } + } + return { status, stdout, stderr }; +}; +if (require.main === module) { + const [route, ...args] = process.argv.slice(2); + const result = module.exports(route, args); + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + process.exit(result.status); +} +`, + preload: String.raw` +"use strict"; +const childProcess = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const { syncBuiltinESMExports } = require("node:module"); +const nativeExecFileSync = childProcess.execFileSync; +function resolvesToFixture(file, options) { + const env = options.env ?? process.env; + const cwd = options.cwd ?? process.cwd(); + if (file.includes("/")) return path.resolve(cwd, file) === env.FIXTURE_GH_BIN; + if (file !== "gh") return false; + for (const directory of (env.PATH ?? "").split(path.delimiter)) { + const candidate = path.resolve(cwd, directory, file); + try { fs.accessSync(candidate, fs.constants.X_OK); } catch { continue; } + return candidate === env.FIXTURE_GH_BIN; + } + return false; +} +childProcess.execFileSync = function(file, args, options = {}) { + // Other child boundaries, working directories and stream shapes remain native. + if (typeof file !== "string" || !Array.isArray(args) || typeof options !== "object" || + options.timeout !== undefined || options.signal !== undefined || + (options.cwd !== undefined && path.resolve(options.cwd) !== process.cwd()) || + (options.stdio !== undefined && (!Array.isArray(options.stdio) || + options.stdio[1] !== "pipe" || options.stdio[2] !== "pipe")) || + !resolvesToFixture(file, options)) return nativeExecFileSync(file, args, options); + const env = options.env ?? process.env; + const stdin = options.stdio?.[0]; + if (stdin !== undefined && !["pipe", "ignore", "inherit", 0].includes(stdin)) { + return nativeExecFileSync(file, args, options); + } + const result = require(env.FIXTURE_GH)("direct", args, { + env, + input: options.input ?? (stdin === "inherit" || stdin === 0 ? undefined : Buffer.alloc(0)), + }); + const encode = (value) => options.encoding && options.encoding !== "buffer" + ? Buffer.from(value).toString(options.encoding) : Buffer.from(value); + const stdout = encode(result.stdout), stderr = encode(result.stderr); + if (result.status !== 0) { + throw Object.assign(new Error("Command failed: " + file + "\n" + result.stderr), { + status: result.status, signal: null, stdout, stderr, output: [null, stdout, stderr], + }); + } + return stdout; +}; +syncBuiltinESMExports(); +`, + }; +} diff --git a/test/scripts/pr-merge-outcome.test-support.ts b/test/scripts/pr-merge-outcome.test-support.ts index 8ba592abe900..89deca78de3d 100644 --- a/test/scripts/pr-merge-outcome.test-support.ts +++ b/test/scripts/pr-merge-outcome.test-support.ts @@ -6,6 +6,7 @@ import { resolveVitestNodeArgs } from "../../scripts/lib/vitest-process-env.mts" import { requireNodeTool } from "../helpers/node-toolchain.js"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; import { createMergeGitFixtureFactory } from "./pr-merge-fixture-git.test-support.js"; +import { createMergeGhFixturePrograms } from "./pr-merge-gh-process.test-support.js"; import { createPriorCiFixtureState, priorCiSecurityFixtureSource, @@ -291,16 +292,11 @@ export function createMergeOutcomeFixtureHarness() { }; const state = (): typeof initial => JSON.parse(readFileSync(statePath, "utf8")); save(initial); - const gh = template.program( - "gh.mjs", + const ghPrograms = createMergeGhFixturePrograms( ` -import fs from "node:fs"; -import { createHash } from "node:crypto"; -import { execFileSync, spawnSync } from "node:child_process"; -const [route,...args]=process.argv.slice(2); const file=process.env.FIXTURE_STATE; const s=JSON.parse(fs.readFileSync(file,"utf8")); -const git=(args,input)=>execFileSync("git",["-c","commit.gpgsign=false","-c","core.hooksPath=/dev/null",...args],{cwd:process.env.FIXTURE_REPO,input,encoding:"utf8"}).trim(); +const git=(args,input)=>execFileSync("git",["-c","commit.gpgsign=false","-c","core.hooksPath=/dev/null",...args],{cwd:process.env.FIXTURE_REPO,env:process.env,input,encoding:"utf8"}).trim(); const save=()=>fs.writeFileSync(file,JSON.stringify(s)); const out=(value)=>{ const body=typeof value==="string"?value:JSON.stringify(value); @@ -409,7 +405,7 @@ else if(args[0]==="api"&&args.some(arg=>new RegExp("^repos/[^/]+/[^/]+$").test(a if(!args.includes("--hostname")) fail("missing repository hostname"); if(s.repoAuthorityUnavailable) fail("repository metadata unavailable"); if(s.restDispatchChange) { - const retained=spawnSync("git",["show","refs/openclaw/pr-merge-outcomes/123:outcome.json"],{cwd:process.env.FIXTURE_REPO,encoding:"utf8"}); + const retained=spawnSync("git",["show","refs/openclaw/pr-merge-outcomes/123:outcome.json"],{cwd:process.env.FIXTURE_REPO,env:process.env,encoding:"utf8"}); if(retained.status===0) { const intent=JSON.parse(retained.stdout); if(intent.phase==="intent"&&intent.accepted===false) { @@ -463,7 +459,7 @@ else if(args[0]==="api"&&args.includes("repos/fixture/repo/git/ref/heads/main")) if(s.restObservation?.main&&!s.restObservation.afterPolicyRead&&s.restMainReads===0&&s.observationReads>0) applyRestObservation(); s.restMainReads++; if(s.restMainAdvance&&s.pr.state==="OPEN") { - const retained=spawnSync("git",["show","refs/openclaw/pr-merge-outcomes/123:outcome.json"],{cwd:process.env.FIXTURE_REPO,encoding:"utf8"}); + const retained=spawnSync("git",["show","refs/openclaw/pr-merge-outcomes/123:outcome.json"],{cwd:process.env.FIXTURE_REPO,env:process.env,encoding:"utf8"}); if(retained.status===0) { const intent=JSON.parse(retained.stdout); if(intent.phase==="intent"&&intent.accepted===false) { @@ -693,6 +689,8 @@ else if(args[0]==="pr"&&args[1]==="view") { save(); `, ); + const gh = template.program("gh.cjs", ghPrograms.cli); + const ghPreload = template.program("gh-preload.cjs", ghPrograms.preload); const shell = template.program( "invoke.sh", `#!/usr/bin/env bash @@ -787,12 +785,14 @@ fi TMPDIR: root, // Reuse compiled owner modules across native children, never mutable fixture state. NODE_COMPILE_CACHE: template.compileCache, + NODE_OPTIONS: `--require ${JSON.stringify(ghPreload)}`, FIXTURE_STATE: statePath, FIXTURE_ROOT: root, FIXTURE_REPO: repo, FIXTURE_REMOTE: remote, FIXTURE_SCRIPTS: scripts, FIXTURE_GH: gh, + FIXTURE_GH_BIN: join(bin, "gh"), FIXTURE_NODE: nodeExecutable, OPENCLAW_PR_MERGE_METHOD: "squash", OPENCLAW_PR_STRICT_DRIFT: "",