test(runtime): keep Node-only child proofs on Node (#147329)

* test(runtime): keep Node-only child proofs on Node

* test(runtime): link Bun exit code fix
This commit is contained in:
Peter Steinberger 2026-09-13 13:05:16 -07:00 • committed by GitHub
parent 88c2b10d6b
commit bf1b8db1f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 48 additions and 24 deletions

View file

@ -5,11 +5,13 @@ import os from "node:os";
import path from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
import { resolveTestNodeExecPath } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import { createCodexNativeTestState } from "./native-app-server.test-support.js";
type ProcessTree = { parent: number; child: number; descendant: number };
const fixture = fileURLToPath(new URL("./transport-orphan.test-helper.ts", import.meta.url));
const nodeExecPath = resolveTestNodeExecPath();
function isAlive(pid: number) {
try {
@ -93,7 +95,7 @@ describe.skipIf(process.platform === "win32")("Codex stdio crash recovery", () =
root,
...(native ? [native.command, native.cwd] : []),
];
const parent = spawn(process.execPath, args, {
const parent = spawn(nodeExecPath, args, {
env: {
HOME: root,
...native?.env,

View file

@ -3,7 +3,8 @@ import { describe, expect, it } from "vitest";
describe("googlechat secret contract import boundary", () => {
it("exposes service-account targets without loading secret setup or resolution", async () => {
const stdout = await runDirectImportSmoke(`
const stdout = await runDirectImportSmoke(
`
import { realpathSync } from "node:fs";
import { registerHooks } from "node:module";
import { pathToFileURL } from "node:url";
@ -38,7 +39,9 @@ try {
} finally {
hooks.deregister();
}
`);
`,
{ runtime: "node" },
);
const result = JSON.parse(stdout);
expect(result.observed.entry).toBe(true);
expect(result.collectorCallable).toBe(true);

View file

@ -8,6 +8,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { ProviderAuthMethod } from "openclaw/plugin-sdk/core";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { resolveTestNodeExecPath } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { azLoginDeviceCodeWithOptions, execAz, getAccessTokenResultAsync } from "./cli.js";
import plugin from "./index.js";
@ -41,7 +42,13 @@ vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
execFileSync: execFileSyncMock,
execFileSync: ((file: string, ...args: unknown[]) =>
file === "az"
? execFileSyncMock(file, ...args)
: Reflect.apply(actual.execFileSync, actual, [
file,
...args,
])) as typeof actual.execFileSync,
};
});
@ -755,6 +762,7 @@ describe("microsoft-foundry plugin", () => {
timeout: 180_000,
},
async ({ signal }) => {
const nodeExecPath = resolveTestNodeExecPath();
const { runExec } = await vi.importActual<
typeof import("openclaw/plugin-sdk/process-runtime")
>("openclaw/plugin-sdk/process-runtime");
@ -788,7 +796,7 @@ process.stdout.write(JSON.stringify({
if (process.platform === "win32") {
await fs.writeFile(
path.join(binDir, "az.cmd"),
`@echo off\r\n"${process.execPath}" "${fakeAz}" %*\r\n`,
`@echo off\r\n"${nodeExecPath}" "${fakeAz}" %*\r\n`,
);
} else {
await fs.chmod(fakeAz, 0o700);
@ -861,7 +869,7 @@ assert.equal(afterOldest, 130, "the evicted account must refresh through az");
// The child has its own real profile store and token cache. Only az is synthetic;
// no ambient credentials or the parent Vitest mocks enter its provider graph.
const { stdout } = await runExec(
process.execPath,
nodeExecPath,
["--import", "tsx/esm", script, proofDir, new URL("./index.ts", import.meta.url).href],
{
cwd: repoRoot,
@ -871,7 +879,7 @@ assert.equal(afterOldest, 130, "the evicted account must refresh through az");
baseEnv: {
PATH: [
binDir,
path.dirname(process.execPath),
path.dirname(nodeExecPath),
...(process.platform === "win32" ? [] : ["/usr/bin", "/bin"]),
].join(path.delimiter),
HOME: homeDir,

View file

@ -3,14 +3,16 @@ import { execFile } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { resolveTestNodeExecPath } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
describe("sendMSTeamsActivityWithReference SDK import ordering", () => {
it("keeps root exports intact when quoted behavior is the first SDK access", async () => {
const nodeExecPath = resolveTestNodeExecPath();
await execFileAsync(
process.execPath,
nodeExecPath,
["scripts/lib/plugin-npm-runtime-build.mjs", "extensions/msteams"],
{ cwd: process.cwd() },
);
@ -146,7 +148,7 @@ describe("sendMSTeamsActivityWithReference SDK import ordering", () => {
`;
const { stdout, stderr } = await execFileAsync(
process.execPath,
nodeExecPath,
["--import", "tsx", "--input-type=module", "--eval", fixture],
{
cwd: path.dirname(outDir),

View file

@ -114,7 +114,7 @@ function optionArgs(options: PathCommandOptions): string[] {
async function invokePathCli(args: string[], runtime: TestRuntime): Promise<void> {
const previousExitCode = process.exitCode;
process.exitCode = undefined;
process.exitCode = 0;
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
runtime.writeStdout(String(chunk));
return true;
@ -141,7 +141,9 @@ async function invokePathCli(args: string[], runtime: TestRuntime): Promise<void
} finally {
stdoutWrite.mockRestore();
stderrWrite.mockRestore();
process.exitCode = previousExitCode;
// oxlint-disable-next-line no-warning-comments -- replace the pending link after Bun ships the fix.
// TODO(bun#42607): Assign undefined once Bun clears a nonzero process.exitCode.
process.exitCode = previousExitCode ?? 0;
}
}

View file

@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { resolveTestNodeExecPath } from "openclaw/plugin-sdk/test-fixtures";
import { expect, it } from "vitest";
const execFileAsync = promisify(execFile);
@ -8,11 +9,12 @@ const repoRoot = fileURLToPath(new URL("../../../", import.meta.url));
const fixturePath = fileURLToPath(
new URL("./test-support/bus-server-shutdown.ts", import.meta.url),
);
const nodeExecPath = resolveTestNodeExecPath();
// Vitest's own handles would keep an unreferenced shutdown timer alive.
it.each(["bus", "provider"])("finishes %s shutdown after rejecting an upload", async (kind) => {
const { stdout, stderr } = await execFileAsync(
process.execPath,
nodeExecPath,
["--import", "./scripts/tsx.mjs", fixturePath, kind],
{ cwd: repoRoot, encoding: "utf8", timeout: 20_000 },
);

View file

@ -5,6 +5,7 @@ import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { useAutoCleanupTempDirTracker } from "openclaw/plugin-sdk/test-env";
import { resolveTestNodeExecPath } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it } from "vitest";
import type { QaSuiteSummaryJson } from "./suite-summary.js";
import { runQaWindowsTaskkill } from "./windows-system-tools.js";
@ -16,6 +17,7 @@ const fixturePath = fileURLToPath(
const artifactsRoot = path.join(repoRoot, ".artifacts", "qa-e2e");
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const activeChildren = new Set<ChildProcess>();
const nodeExecPath = resolveTestNodeExecPath();
const PROCESS_LIFECYCLE_SCENARIO = "channel-chat-baseline";
// Suite execution contends with the surrounding extension shard; only the bounded
@ -109,15 +111,11 @@ afterEach(async () => {
});
function startSuiteProcess(outputDir: string, scenarioIds: readonly string[]) {
const child = spawn(
process.execPath,
["--import", "tsx", fixturePath, outputDir, ...scenarioIds],
{
cwd: repoRoot,
env: buildSuiteProcessEnv(outputDir),
stdio: ["ignore", "pipe", "pipe"],
},
);
const child = spawn(nodeExecPath, ["--import", "tsx", fixturePath, outputDir, ...scenarioIds], {
cwd: repoRoot,
env: buildSuiteProcessEnv(outputDir),
stdio: ["ignore", "pipe", "pipe"],
});
activeChildren.add(child);
let stdout = "";
let stderr = "";

View file

@ -26,6 +26,7 @@ export { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text
export { countLines, hasBalancedFences } from "../test-utils/chunk-test-helpers.js";
export { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js";
export { typedCases } from "../test-utils/typed-cases.js";
export { resolveTestNodeExecPath } from "../test-utils/node-process.js";
export { createRequireRecord } from "../../test/helpers/record.js";
export type { RecordRequirementKind, RecordRequirementMessage } from "../../test/helpers/record.js";
export {

View file

@ -5,6 +5,7 @@ import { execFile } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { resolveTestNodeExecPath } from "../../test-utils/node-process.js";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
@ -17,9 +18,14 @@ const SHARED_IMPORT_ENV = {
TERM: process.env.TERM,
} satisfies NodeJS.ProcessEnv;
export async function runDirectImportSmoke(code: string): Promise<string> {
const runtimeArgs = process.versions.bun ? [] : ["--import", "tsx"];
const { stdout } = await execFileAsync(process.execPath, [...runtimeArgs, "-e", code], {
export async function runDirectImportSmoke(
code: string,
options: { runtime?: "current" | "node" } = {},
): Promise<string> {
const useNode = options.runtime === "node" || !process.versions.bun;
const runtimeArgs = useNode ? ["--import", "tsx"] : [];
const execPath = options.runtime === "node" ? resolveTestNodeExecPath() : process.execPath;
const { stdout } = await execFileAsync(execPath, [...runtimeArgs, "-e", code], {
cwd: repoRoot,
env: SHARED_IMPORT_ENV,
timeout: 40_000,