From bf1b8db1f58f8550c0f307d12d5d5d73cf2f1284 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 13 Sep 2026 13:05:16 -0700 Subject: [PATCH] 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 --- .../src/app-server/transport-orphan.test.ts | 4 +++- .../googlechat/secret-contract-api.test.ts | 7 +++++-- extensions/microsoft-foundry/index.test.ts | 16 ++++++++++++---- .../msteams/src/sdk-proactive.process.test.ts | 6 ++++-- extensions/oc-path/src/cli.test.ts | 6 ++++-- .../qa-lab/src/bus-server.shutdown.test.ts | 4 +++- .../qa-lab/src/suite-process-lifecycle.test.ts | 16 +++++++--------- src/plugin-sdk/test-fixtures.ts | 1 + src/plugin-sdk/test-helpers/direct-smoke.ts | 12 +++++++++--- 9 files changed, 48 insertions(+), 24 deletions(-) diff --git a/extensions/codex/src/app-server/transport-orphan.test.ts b/extensions/codex/src/app-server/transport-orphan.test.ts index 44e41cfc2319..8fe3d6e1dd0b 100644 --- a/extensions/codex/src/app-server/transport-orphan.test.ts +++ b/extensions/codex/src/app-server/transport-orphan.test.ts @@ -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, diff --git a/extensions/googlechat/secret-contract-api.test.ts b/extensions/googlechat/secret-contract-api.test.ts index 5cb9a7283e59..e665f1a96d6f 100644 --- a/extensions/googlechat/secret-contract-api.test.ts +++ b/extensions/googlechat/secret-contract-api.test.ts @@ -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); diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index 70dc4438ab02..b7f872c157c7 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -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("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, diff --git a/extensions/msteams/src/sdk-proactive.process.test.ts b/extensions/msteams/src/sdk-proactive.process.test.ts index e0461b2e806c..ff5a4703a016 100644 --- a/extensions/msteams/src/sdk-proactive.process.test.ts +++ b/extensions/msteams/src/sdk-proactive.process.test.ts @@ -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), diff --git a/extensions/oc-path/src/cli.test.ts b/extensions/oc-path/src/cli.test.ts index b8fd338e5d19..382c50009333 100644 --- a/extensions/oc-path/src/cli.test.ts +++ b/extensions/oc-path/src/cli.test.ts @@ -114,7 +114,7 @@ function optionArgs(options: PathCommandOptions): string[] { async function invokePathCli(args: string[], runtime: TestRuntime): Promise { 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 { const { stdout, stderr } = await execFileAsync( - process.execPath, + nodeExecPath, ["--import", "./scripts/tsx.mjs", fixturePath, kind], { cwd: repoRoot, encoding: "utf8", timeout: 20_000 }, ); diff --git a/extensions/qa-lab/src/suite-process-lifecycle.test.ts b/extensions/qa-lab/src/suite-process-lifecycle.test.ts index 6cbbb81a295b..925a3e4dd9af 100644 --- a/extensions/qa-lab/src/suite-process-lifecycle.test.ts +++ b/extensions/qa-lab/src/suite-process-lifecycle.test.ts @@ -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(); +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 = ""; diff --git a/src/plugin-sdk/test-fixtures.ts b/src/plugin-sdk/test-fixtures.ts index 6dbdd8ea98d9..064565b603c9 100644 --- a/src/plugin-sdk/test-fixtures.ts +++ b/src/plugin-sdk/test-fixtures.ts @@ -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 { diff --git a/src/plugin-sdk/test-helpers/direct-smoke.ts b/src/plugin-sdk/test-helpers/direct-smoke.ts index 8dd1ab33add1..7e242898fdf6 100644 --- a/src/plugin-sdk/test-helpers/direct-smoke.ts +++ b/src/plugin-sdk/test-helpers/direct-smoke.ts @@ -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 { - 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 { + 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,