Files
openclaw/test/scripts/direct-run-entrypoints.test.ts
T
Peter Steinberger cfe6ebcd1e fix(scripts): anchor hydrated tsx at checkout root (#121149)
Resolve pnpm's configured modules directory from the checkout root before TypeScript boot, preserving both environment spellings and canonical checkout/common-worktree fallbacks. Use Node package self-resolution from the hydrated tsx package so nested wrappers cannot change relative path semantics.
2026-08-09 17:15:27 -07:00

226 lines
7.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { detectChangedScope } from "../../scripts/ci-changed-scope.mjs";
import { isDirectRunPath } from "../../scripts/lib/direct-run.mjs";
const DIRECT_RUN_SCRIPTS = [
"scripts/android-app-i18n.ts",
"scripts/android-pin-version.ts",
"scripts/ci-run-timings.mjs",
"scripts/e2e/lib/package-compat.mjs",
"scripts/generate-bundled-channel-config-metadata.ts",
"scripts/plan-release-workflow-matrix.mjs",
"scripts/run-additional-boundary-checks.mts",
"scripts/verify-docker-attestations.mjs",
] as const;
const EXECUTABLE_ENTRYPOINTS = [
{
args: ["--direct-run-smoke"],
output: "Unknown CI run timing option: --direct-run-smoke",
script: "scripts/ci-run-timings.mjs",
status: 1,
},
{
args: ["2026.4.25"],
output: "1",
script: "scripts/e2e/lib/package-compat.mjs",
status: 0,
},
{
args: [],
output: "docker_e2e_count=",
script: "scripts/plan-release-workflow-matrix.mjs",
status: 0,
},
{
args: ["--help"],
output: "Usage: node --import tsx scripts/run-additional-boundary-checks.mts",
script: "scripts/run-additional-boundary-checks.mts",
status: 0,
},
{
args: ["--help"],
output: "Usage: node scripts/verify-docker-attestations.mjs",
script: "scripts/verify-docker-attestations.mjs",
status: 0,
},
] as const;
function runEntrypoint(entrypoint: (typeof EXECUTABLE_ENTRYPOINTS)[number]) {
const script = path.resolve(entrypoint.script);
const args = script.endsWith(".mts")
? ["--import", "tsx", script, ...entrypoint.args]
: [script, ...entrypoint.args];
return spawnSync(process.execPath, args, {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
DOCKER_LANES: "",
GITHUB_STEP_SUMMARY: "",
INCLUDE_LIVE_SUITES: "",
INCLUDE_RELEASE_PATH_SUITES: "",
LIVE_MODEL_PROVIDERS: "",
LIVE_SUITE_FILTER: "",
RELEASE_TEST_PROFILE: "",
},
timeout: 30_000,
});
}
const TSX_SHIM_WRAPPERS = [
"scripts/run-vitest.mjs",
"scripts/lib/plugin-npm-package-manifest.mjs",
"scripts/e2e/kitchen-sink-rpc-walk.mjs",
"scripts/perf/summarize-cpuprofile.mjs",
] as const;
type ModulesEnv = Partial<Record<"PNPM_CONFIG_MODULES_DIR" | "npm_config_modules_dir", string>>;
function writeTsxFixture(modulesDir: string, marker: string) {
const packageDir = path.join(modulesDir, "tsx");
mkdirSync(packageDir, { recursive: true });
writeFileSync(
path.join(packageDir, "package.json"),
JSON.stringify({ name: "tsx", type: "module", exports: "./loader.mjs" }),
);
writeFileSync(
path.join(packageDir, "loader.mjs"),
`process.env.OPENCLAW_TSX_FIXTURE_LOADER = ${JSON.stringify(marker)};\n`,
);
}
function runShimFixture(
wrapper: (typeof TSX_SHIM_WRAPPERS)[number],
configureModules: (paths: {
checkoutRoot: string;
fixtureRoot: string;
}) => ModulesEnv = () => ({}),
) {
const fixtureRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "openclaw-tsx-cli-shim-")));
const checkoutRoot = path.join(fixtureRoot, "checkout");
const wrapperPath = path.join(checkoutRoot, wrapper);
const implementationPath = wrapperPath.replace(/\.mjs$/u, ".mts");
try {
mkdirSync(path.dirname(wrapperPath), { recursive: true });
mkdirSync(path.join(checkoutRoot, "scripts", "lib"), { recursive: true });
copyFileSync(wrapper, wrapperPath);
copyFileSync(
"scripts/lib/tsx-cli-shim.mjs",
path.join(checkoutRoot, "scripts", "lib", "tsx-cli-shim.mjs"),
);
writeFileSync(path.join(checkoutRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
writeFileSync(
implementationPath,
"process.stdout.write(JSON.stringify({ loader: process.env.OPENCLAW_TSX_FIXTURE_LOADER, args: process.argv.slice(2) }));\n",
);
writeTsxFixture(path.join(checkoutRoot, "node_modules"), "checkout");
const modulesEnv = configureModules({ checkoutRoot, fixtureRoot });
const env = { ...process.env };
delete env.NODE_OPTIONS;
delete env.NODE_PATH;
delete env.PNPM_CONFIG_MODULES_DIR;
delete env.npm_config_modules_dir;
Object.assign(env, modulesEnv);
return spawnSync(process.execPath, [wrapperPath, "--hydrated-proof"], {
cwd: fixtureRoot,
encoding: "utf8",
env,
timeout: 10_000,
});
} finally {
rmSync(fixtureRoot, { recursive: true, force: true });
}
}
function expectShimLoader(result: ReturnType<typeof runShimFixture>, loader: string) {
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({ loader, args: ["--hydrated-proof"] });
}
describe("script direct-run entrypoints", () => {
it.each(EXECUTABLE_ENTRYPOINTS)("runs $script through its guarded CLI", (entrypoint) => {
const result = runEntrypoint(entrypoint);
const output = `${result.stdout}${result.stderr}`;
expect(result.error).toBeUndefined();
expect(result.status).toBe(entrypoint.status);
expect(output).toContain(entrypoint.output);
});
it.each([
{ envKey: "PNPM_CONFIG_MODULES_DIR", mode: "absolute", wrapper: TSX_SHIM_WRAPPERS[0] },
{ envKey: "npm_config_modules_dir", mode: "relative", wrapper: TSX_SHIM_WRAPPERS[1] },
{ envKey: "PNPM_CONFIG_MODULES_DIR", mode: "relative", wrapper: TSX_SHIM_WRAPPERS[2] },
{ envKey: "npm_config_modules_dir", mode: "absolute", wrapper: TSX_SHIM_WRAPPERS[3] },
] as const)("boots $wrapper from a $mode $envKey", ({ envKey, mode, wrapper }) => {
const result = runShimFixture(wrapper, ({ checkoutRoot, fixtureRoot }) => {
const modulesDir = path.join(fixtureRoot, "hydrated-modules");
writeTsxFixture(modulesDir, "hydrated");
const configuredDir =
mode === "absolute" ? modulesDir : path.relative(checkoutRoot, modulesDir);
return { [envKey]: configuredDir };
});
expectShimLoader(result, "hydrated");
});
it("prefers PNPM_CONFIG_MODULES_DIR over npm_config_modules_dir", () => {
const result = runShimFixture(TSX_SHIM_WRAPPERS[2], ({ fixtureRoot }) => {
const preferredDir = path.join(fixtureRoot, "preferred-modules");
const fallbackDir = path.join(fixtureRoot, "fallback-modules");
writeTsxFixture(preferredDir, "preferred");
writeTsxFixture(fallbackDir, "lowercase");
return {
PNPM_CONFIG_MODULES_DIR: preferredDir,
npm_config_modules_dir: fallbackDir,
};
});
expectShimLoader(result, "preferred");
});
it("falls back to checkout dependencies without an external modules directory", () => {
expectShimLoader(runShimFixture(TSX_SHIM_WRAPPERS[3]), "checkout");
});
it("matches Windows drive paths case-insensitively", () => {
expect(
isDirectRunPath(
"C:\\repo\\scripts\\android-app-i18n.ts",
"c:\\repo\\scripts\\android-app-i18n.ts",
"win32",
),
).toBe(true);
});
it.each(DIRECT_RUN_SCRIPTS)("uses the canonical guard in %s", (script) => {
const source = readFileSync(script, "utf8");
expect(source.match(/isDirectRunUrl\(process\.argv\[1\], import\.meta\.url\)/gu)).toHaveLength(
1,
);
});
it.each([
...DIRECT_RUN_SCRIPTS,
"scripts/lib/direct-run.mjs",
"scripts/lib/tsx-cli-shim.mjs",
"test/scripts/direct-run-entrypoints.test.ts",
])("routes %s through Windows CI", (changedPath) => {
expect(detectChangedScope([changedPath]).runWindows).toBe(true);
});
});