mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
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.
This commit is contained in:
parent
25afb0c390
commit
12253cc519
2 changed files with 108 additions and 9 deletions
99
test/scripts/pr-merge-gh-process.test-support.ts
Normal file
99
test/scripts/pr-merge-gh-process.test-support.ts
Normal file
|
|
@ -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();
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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: "",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue