mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* test(agents): deslop s035 tests * test(scripts): deslop s030 tests * test(browser): deslop s039 tests * test(gateway): deslop s034 tests * test(copilot): deslop s040 tests * test(auto-reply): deslop s042 tests * test(agents): deslop s041 tests * test(scripts): deslop s022 tests * test(sessions): deslop s036 tests * test(cli): deslop s037 tests * test: adapt d011 fixtures to current owners * test: preserve current native i18n guidance * test: preserve d011 authorization coverage and diagnostic assertions * test: retain fixture imports used by newer main tests * test: fix lint in d011 test reductions * test(copilot): remove obsolete max-lines suppression
1601 lines
55 KiB
TypeScript
1601 lines
55 KiB
TypeScript
// Run Vitest tests cover run vitest script behavior.
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { EventEmitter } from "node:events";
|
|
import fs from "node:fs";
|
|
import nodePath from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { parseCLI } from "vitest/node";
|
|
import {
|
|
resolveVitestCliEntry,
|
|
resolveMissingVitestDependencyMessage,
|
|
} from "../../scripts/lib/vitest-build-prerequisites.mts";
|
|
import {
|
|
DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
|
DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
|
VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS,
|
|
resolveDefaultVitestNoOutputTimeoutMs,
|
|
resolveRunVitestSpawnEnv,
|
|
resolveVitestNoOutputHeartbeatMs,
|
|
resolveVitestNodeArgs,
|
|
resolveVitestNoOutputTimeoutMs,
|
|
} from "../../scripts/lib/vitest-process-env.mts";
|
|
import { resolveVitestTestCommand } from "../../scripts/lib/vitest-test-runtime.mts";
|
|
import {
|
|
createVitestUnhandledErrorDetector,
|
|
writeVitestUnhandledErrorSummary,
|
|
} from "../../scripts/lib/vitest-unhandled-errors.mts";
|
|
import {
|
|
TOOLING_EXCLUDED_TESTS,
|
|
installVitestNoOutputWatchdog,
|
|
resolveBoundedVitestInvocations,
|
|
resolveDirectNodeVitestArgs,
|
|
resolveExplicitTestFileNoPassArgs,
|
|
resolveImplicitVitestArgs,
|
|
resolveMissingExplicitTestFiles,
|
|
resolveTestProjectsDelegationArgs,
|
|
resolveVitestSpawnParams,
|
|
spawnWatchedVitestProcess,
|
|
shouldSuppressVitestStderrLine,
|
|
} from "../../scripts/run-vitest.mts";
|
|
import { parseTestProjectsArgs } from "../../scripts/test-projects.test-support.mts";
|
|
import { forceKillVitestProcessGroup } from "../../scripts/vitest-process-group.mts";
|
|
import { listGitTrackedFiles } from "../../src/test-utils/repo-files.js";
|
|
import { isGatewayServerTestFile } from "../vitest/vitest.gateway-server-paths.mjs";
|
|
|
|
const posixIt = process.platform === "win32" ? it.skip : it;
|
|
// These bounds only guard broken fixtures; readiness and exit are asserted via process signals.
|
|
const LOAD_SENSITIVE_PROCESS_TIMEOUT_MS = process.env.CI ? 30_000 : 15_000;
|
|
|
|
describe("scripts/run-vitest", () => {
|
|
it("reports an actionable error when Vitest cannot be resolved", () => {
|
|
const error = new Error("Cannot find module 'vitest/package.json'");
|
|
(error as NodeJS.ErrnoException).code = "MODULE_NOT_FOUND";
|
|
|
|
expect(() =>
|
|
resolveVitestCliEntry({
|
|
baseDir: "/repo",
|
|
fsImpl: { existsSync: () => false },
|
|
requireResolve: () => {
|
|
throw error;
|
|
},
|
|
}),
|
|
).toThrow(
|
|
[
|
|
"[vitest] node_modules is missing; Vitest cannot be resolved.",
|
|
"Install dependencies before running scripts/run-vitest.mjs:",
|
|
" pnpm install --frozen-lockfile",
|
|
"For raw Crabbox/AWS macOS source syncs, hydrate or install dependencies before this runner.",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
it.each(["mjs", "mts"])("ends %s argument failures with one final trailer", (extension) => {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[nodePath.resolve(`scripts/run-vitest.${extension}`)],
|
|
{
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(1);
|
|
const trailer = `[${extension === "mjs" ? "test" : "vitest"}] FAILED (exit 1)`;
|
|
expect(result.stderr.match(/^\[.*\] FAILED \(exit \d+\)$/gmu)).toEqual([trailer]);
|
|
expect(result.stderr.trim().split("\n").at(-1)).toBe(trailer);
|
|
});
|
|
|
|
it.each([...VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS.keys(), ...TOOLING_EXCLUDED_TESTS])(
|
|
"keeps hardcoded Vitest path %s valid",
|
|
(referencedPath) => {
|
|
expect(fs.existsSync(nodePath.resolve(referencedPath))).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("keeps Sparkplug compilation synchronous in test processes", () => {
|
|
expect(resolveVitestNodeArgs({ PATH: "/usr/bin" })).toEqual([
|
|
"--no-maglev",
|
|
"--no-concurrent-sparkplug",
|
|
]);
|
|
});
|
|
|
|
it("detects pnpm exec node wrappers that can be spawned directly", () => {
|
|
expect(
|
|
resolveDirectNodeVitestArgs([
|
|
"exec",
|
|
"node",
|
|
"--no-maglev",
|
|
"node_modules/vitest/vitest.mjs",
|
|
]),
|
|
).toEqual(["--no-maglev", "node_modules/vitest/vitest.mjs"]);
|
|
expect(resolveDirectNodeVitestArgs(["exec", "vitest", "run"])).toBeNull();
|
|
});
|
|
|
|
it.each([undefined, "node", "bun"])(
|
|
"selects the %s test runtime without changing the compiled bootstrap or test operands",
|
|
(runtime) => {
|
|
const operands = [
|
|
"scripts/lib/vitest-worker-bootstrap.mts",
|
|
"/compiled/generation",
|
|
"node_modules/vitest/vitest.mjs",
|
|
"run",
|
|
"--testNamePattern",
|
|
"--no-maglev",
|
|
];
|
|
const flags = ["--no-maglev", "--no-concurrent-sparkplug"];
|
|
expect(
|
|
resolveVitestTestCommand([...flags, ...operands], {
|
|
OPENCLAW_VITEST_RUNTIME: runtime,
|
|
}),
|
|
).toEqual({
|
|
command: runtime === "bun" ? "bun" : process.execPath,
|
|
args:
|
|
runtime === "bun"
|
|
? [
|
|
"--tsconfig-override",
|
|
fileURLToPath(new URL("../../tsconfig.json", import.meta.url)),
|
|
...operands,
|
|
]
|
|
: [...flags, ...operands],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects an unsupported test runtime before launching a child", () => {
|
|
expect(() =>
|
|
spawnWatchedVitestProcess({
|
|
pnpmArgs: ["exec", "node", "node_modules/vitest/vitest.mjs", "run"],
|
|
spawnParams: {},
|
|
env: { OPENCLAW_VITEST_RUNTIME: "deno" },
|
|
}),
|
|
).toThrow("Invalid OPENCLAW_VITEST_RUNTIME: deno; expected node or bun");
|
|
});
|
|
|
|
it("keeps native preparation tools on Node when tests select Bun", () => {
|
|
const args = ["--import", "tsx", "scripts/ensure-playwright-chromium.mts"];
|
|
expect(resolveVitestTestCommand(args, { OPENCLAW_VITEST_RUNTIME: "bun" })).toEqual({
|
|
command: process.execPath,
|
|
args,
|
|
});
|
|
});
|
|
|
|
it.each(["mjs", "mts"])(
|
|
"resolves dependencies before native parsing at the %s entrypoint",
|
|
(extension) => {
|
|
const preload = `import {registerHooks} from 'node:module';
|
|
registerHooks({resolve(specifier, context, nextResolve) {
|
|
if (specifier === 'vitest/package.json' || specifier === 'vitest/node') {
|
|
console.error('dependency request: ' + specifier);
|
|
const error = new Error("Cannot find module '" + specifier + "'");
|
|
error.code = 'MODULE_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
return nextResolve(specifier, context);
|
|
}});`;
|
|
const importUrl = `data:text/javascript,${encodeURIComponent(preload)}`;
|
|
// The JS shim creates another Node process; inject at the inherited dependency
|
|
// boundary, and retain a bounded empty selection even if injection regresses.
|
|
const result = spawnSync(
|
|
process.versions.bun ? "node" : process.execPath,
|
|
[
|
|
nodePath.resolve(`scripts/run-vitest.${extension}`),
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.tooling.config.ts",
|
|
"test/scripts/run-vitest.test.ts",
|
|
"--testNamePattern=^dependency-order-no-test$",
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--import=${JSON.stringify(importUrl)}`]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
},
|
|
},
|
|
);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Install dependencies before running scripts/run-vitest.mjs:",
|
|
);
|
|
expect(result.stderr).toContain("dependency request: vitest/package.json");
|
|
expect(result.stderr).not.toContain("dependency request: vitest/node");
|
|
},
|
|
);
|
|
|
|
it("restores the workspace node_modules link from a hydrated pnpm modules directory", () => {
|
|
const error = new Error("Cannot find module 'vitest/package.json'");
|
|
(error as NodeJS.ErrnoException).code = "MODULE_NOT_FOUND";
|
|
const symlinks: Array<{ target: string; path: string; type: string }> = [];
|
|
|
|
expect(
|
|
resolveVitestCliEntry({
|
|
baseDir: "/repo",
|
|
env: { PNPM_CONFIG_MODULES_DIR: "/runner/openclaw-pnpm-node-modules" },
|
|
fsImpl: {
|
|
existsSync: (filePath: string) =>
|
|
filePath.replaceAll("\\", "/") ===
|
|
"/runner/openclaw-pnpm-node-modules/vitest/package.json",
|
|
symlinkSync: (target: string, path: string, type: string) => {
|
|
symlinks.push({ target, path, type });
|
|
},
|
|
},
|
|
platform: "win32",
|
|
requireResolve: () => {
|
|
throw error;
|
|
},
|
|
}),
|
|
).toBe("/repo/node_modules/vitest/vitest.mjs");
|
|
expect(symlinks).toEqual([
|
|
{
|
|
target: "/runner/openclaw-pnpm-node-modules",
|
|
path: "/runner/openclaw-pnpm-node-modules/node_modules",
|
|
type: "junction",
|
|
},
|
|
{
|
|
target: "/runner/openclaw-pnpm-node-modules",
|
|
path: "/repo/node_modules",
|
|
type: "junction",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("self-links hydrated pnpm modules when pnpm lowercases the env key", () => {
|
|
const symlinks: Array<{ target: string; path: string; type: string }> = [];
|
|
|
|
expect(
|
|
resolveVitestCliEntry({
|
|
baseDir: "/repo",
|
|
env: { npm_config_modules_dir: "/runner/openclaw-pnpm-node-modules" },
|
|
fsImpl: {
|
|
existsSync: (filePath: string) =>
|
|
filePath.replaceAll("\\", "/") ===
|
|
"/runner/openclaw-pnpm-node-modules/vitest/package.json",
|
|
symlinkSync: (target: string, path: string, type: string) => {
|
|
symlinks.push({ target, path, type });
|
|
},
|
|
},
|
|
platform: "win32",
|
|
requireResolve: () => "/runner/openclaw-pnpm-node-modules/vitest/package.json",
|
|
}),
|
|
).toBe("/repo/node_modules/vitest/vitest.mjs");
|
|
expect(symlinks).toEqual([
|
|
{
|
|
target: "/runner/openclaw-pnpm-node-modules",
|
|
path: "/runner/openclaw-pnpm-node-modules/node_modules",
|
|
type: "junction",
|
|
},
|
|
{
|
|
target: "/runner/openclaw-pnpm-node-modules",
|
|
path: "/repo/node_modules",
|
|
type: "junction",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("distinguishes missing Vitest from a completely missing dependency install", () => {
|
|
expect(
|
|
resolveMissingVitestDependencyMessage("/repo", {
|
|
existsSync: (filePath: string) => filePath.replaceAll("\\", "/").endsWith("node_modules"),
|
|
}),
|
|
).toContain("[vitest] Vitest is not installed in node_modules.");
|
|
});
|
|
|
|
it("does not override explicit vitest configs", () => {
|
|
const argv = [
|
|
"--config",
|
|
"test/vitest/vitest.ui.config.ts",
|
|
"ui/src/pages/chat/chat-send.test.ts",
|
|
];
|
|
expect(resolveImplicitVitestArgs(argv)).toBe(argv);
|
|
});
|
|
|
|
it("isolates mixed explicit directory targets across Vitest projects", () => {
|
|
expect(resolveImplicitVitestArgs(["extensions/canvas", "src/node-host"])).toEqual([
|
|
"extensions/canvas",
|
|
"src/node-host",
|
|
"--isolate",
|
|
]);
|
|
expect(resolveImplicitVitestArgs(["src/node-host"])).toEqual(["src/node-host"]);
|
|
expect(
|
|
resolveImplicitVitestArgs(["extensions/canvas", "src/node-host", "--no-isolate"]),
|
|
).toEqual(["extensions/canvas", "src/node-host", "--no-isolate"]);
|
|
expect(
|
|
resolveImplicitVitestArgs(["extensions/canvas", "src/node-host", "--", "--no-isolate"]),
|
|
).toEqual(["extensions/canvas", "src/node-host", "--isolate", "--", "--no-isolate"]);
|
|
});
|
|
|
|
it("bounds config-only Gateway server runs in fresh worker processes", () => {
|
|
const argv = [
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.gateway-server.config.ts",
|
|
"--reporter=verbose",
|
|
"--",
|
|
"-x",
|
|
];
|
|
|
|
expect(
|
|
resolveBoundedVitestInvocations(argv, {
|
|
env: {},
|
|
gatewayServerTargetChunks: [
|
|
["src/gateway/server-a.test.ts"],
|
|
["src/gateway/server-b.test.ts"],
|
|
],
|
|
}),
|
|
).toEqual([
|
|
[
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.gateway-server.config.ts",
|
|
"--reporter=verbose",
|
|
"src/gateway/server-a.test.ts",
|
|
"--",
|
|
"-x",
|
|
],
|
|
[
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.gateway-server.config.ts",
|
|
"--reporter=verbose",
|
|
"src/gateway/server-b.test.ts",
|
|
"--",
|
|
"-x",
|
|
],
|
|
]);
|
|
});
|
|
|
|
it("keeps every Gateway server file in one bounded native invocation", () => {
|
|
const argv = ["run", "--config", "test/vitest/vitest.gateway-server.config.ts"];
|
|
const invocations = resolveBoundedVitestInvocations(argv, { env: {} });
|
|
const targets = invocations.map((args) => args.slice(argv.length));
|
|
|
|
expect(targets.every((files) => files.length > 0 && files.length <= 50)).toBe(true);
|
|
expect(targets.flat()).toEqual(
|
|
listGitTrackedFiles({ pathspecs: "src/gateway" })
|
|
?.filter(isGatewayServerTestFile)
|
|
.toSorted((a, b) => a.localeCompare(b)),
|
|
);
|
|
});
|
|
|
|
it("bounds implicit CI runs for absolute Gateway server config paths", () => {
|
|
expect(
|
|
resolveBoundedVitestInvocations(
|
|
["--config", "/repo/test/vitest/vitest.gateway-server.config.ts"],
|
|
{
|
|
env: { CI: "1" },
|
|
gatewayServerTargetChunks: [
|
|
["src/gateway/server-a.test.ts"],
|
|
["src/gateway/server-b.test.ts"],
|
|
],
|
|
},
|
|
),
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
it("bounds the complete E2E selection without multiplying configured workers", () => {
|
|
const argv = ["run", "--config", "test/vitest/vitest.e2e.config.ts", "--maxWorkers", "2"];
|
|
expect(resolveBoundedVitestInvocations(argv, { env: {} })).toEqual([
|
|
[...argv, "--shard=1/4"],
|
|
[...argv, "--shard=2/4"],
|
|
[...argv, "--shard=3/4"],
|
|
[...argv, "--shard=4/4"],
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["doctor"],
|
|
["src/commands/doctor.e2e.test.ts"],
|
|
["--", "doctor"],
|
|
["--shard=2/3"],
|
|
["--watch"],
|
|
["--run=false"],
|
|
["--no-run"],
|
|
["--bail", "1"],
|
|
["--coverage"],
|
|
["--outputFile", "report.json"],
|
|
["--reporter=json"],
|
|
["--listTags"],
|
|
["--clearCache"],
|
|
["--standalone"],
|
|
["--testNamePattern", "doctor"],
|
|
["-t", "doctor"],
|
|
["--exclude", "src/**"],
|
|
["--root", "/other"],
|
|
["--project", "other"],
|
|
["--help"],
|
|
])("preserves explicit E2E selection or execution options %j", (...options) => {
|
|
const argv = ["run", "--config", "test/vitest/vitest.e2e.config.ts", ...options];
|
|
expect(resolveBoundedVitestInvocations(argv, { env: {} })).toEqual([argv]);
|
|
});
|
|
|
|
it.each([
|
|
["local watch default", ["--config", "test/vitest/vitest.gateway-server.config.ts"]],
|
|
["explicit watch", ["watch", "--config", "test/vitest/vitest.gateway-server.config.ts"]],
|
|
[
|
|
"explicit target",
|
|
[
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.gateway-server.config.ts",
|
|
"src/gateway/server-startup.test.ts",
|
|
],
|
|
],
|
|
[
|
|
"coverage output",
|
|
["run", "--config", "test/vitest/vitest.gateway-server.config.ts", "--coverage"],
|
|
],
|
|
["different config", ["run", "--config", "test/vitest/vitest.gateway-core.config.ts"]],
|
|
])("keeps %s as one direct Vitest invocation", (_label, argv) => {
|
|
expect(
|
|
resolveBoundedVitestInvocations(argv, {
|
|
env: {},
|
|
gatewayServerTargetChunks: [
|
|
["src/gateway/server-a.test.ts"],
|
|
["src/gateway/server-b.test.ts"],
|
|
],
|
|
}),
|
|
).toEqual([argv]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "tooling",
|
|
options: ["--exclude", "test/scripts/run-vitest.test.ts"],
|
|
expectedOptions: { exclude: ["test/scripts/run-vitest.test.ts"] },
|
|
},
|
|
{
|
|
name: "Docker tooling",
|
|
options: ["--exclude", "test/scripts/docker-build-helper.test.ts"],
|
|
expectedOptions: { exclude: ["test/scripts/docker-build-helper.test.ts"] },
|
|
},
|
|
{
|
|
name: "UI",
|
|
options: ["--exclude", "ui/src/pages/chat/chat-send.test.ts"],
|
|
expectedOptions: { exclude: ["ui/src/pages/chat/chat-send.test.ts"] },
|
|
},
|
|
{
|
|
name: "browser UI",
|
|
options: ["--exclude", "ui/src/components/markdown-mermaid.runtime.browser.test.ts"],
|
|
expectedOptions: { exclude: ["ui/src/components/markdown-mermaid.runtime.browser.test.ts"] },
|
|
},
|
|
{
|
|
name: "inline",
|
|
options: ["--exclude=test/scripts/run-vitest.test.ts"],
|
|
expectedOptions: { exclude: ["test/scripts/run-vitest.test.ts"] },
|
|
},
|
|
{
|
|
name: "empty inline",
|
|
options: ["--exclude=", "test/scripts/run-vitest.test.ts"],
|
|
expectedOptions: { exclude: ["test/scripts/run-vitest.test.ts"] },
|
|
},
|
|
{
|
|
name: "comma",
|
|
options: [
|
|
"--exclude",
|
|
"test/scripts/run-vitest.test.ts,test/scripts/docker-build-helper.test.ts",
|
|
],
|
|
expectedOptions: {
|
|
exclude: ["test/scripts/run-vitest.test.ts,test/scripts/docker-build-helper.test.ts"],
|
|
},
|
|
},
|
|
{
|
|
name: "pipe",
|
|
options: [
|
|
"--exclude",
|
|
"test/scripts/run-vitest.test.ts|test/scripts/docker-build-helper.test.ts",
|
|
],
|
|
expectedOptions: {
|
|
exclude: ["test/scripts/run-vitest.test.ts|test/scripts/docker-build-helper.test.ts"],
|
|
},
|
|
},
|
|
{
|
|
name: "name regexp",
|
|
options: ["--testNamePattern", "test/scripts/(run-vitest|test-projects).test.ts"],
|
|
expectedOptions: { testNamePattern: "test/scripts/(run-vitest|test-projects).test.ts" },
|
|
},
|
|
{
|
|
name: "repeated",
|
|
options: [
|
|
"--exclude",
|
|
"test/scripts/run-vitest.test.ts",
|
|
"--exclude",
|
|
"test/scripts/run-vitest-progress.test.ts",
|
|
],
|
|
expectedOptions: {
|
|
exclude: ["test/scripts/run-vitest.test.ts", "test/scripts/run-vitest-progress.test.ts"],
|
|
},
|
|
},
|
|
{
|
|
name: "following flag",
|
|
options: ["--exclude", "test/scripts/run-vitest.test.ts", "--passWithNoTests=false"],
|
|
expectedOptions: { exclude: ["test/scripts/run-vitest.test.ts"], passWithNoTests: false },
|
|
},
|
|
{
|
|
name: "missing operand before flag",
|
|
options: ["--exclude=", "--passWithNoTests=false"],
|
|
expectedOptions: { exclude: [true], passWithNoTests: false },
|
|
},
|
|
{
|
|
name: "native separator",
|
|
options: ["--", "test/scripts/run-vitest.test.ts"],
|
|
expectedOptions: { "--": ["test/scripts/run-vitest.test.ts"] },
|
|
},
|
|
])(
|
|
"keeps $name option operands out of implicit config selection",
|
|
({ options, expectedOptions }) => {
|
|
const argv = ["run", ...options];
|
|
const native = parseCLI(["vitest", ...argv]);
|
|
|
|
expect(native.filter).toEqual([]);
|
|
expect(native.options).toMatchObject(expectedOptions);
|
|
expect(resolveTestProjectsDelegationArgs(argv)).toBeNull();
|
|
expect(parseCLI(["vitest", ...resolveImplicitVitestArgs(argv)])).toEqual(native);
|
|
},
|
|
);
|
|
|
|
it("routes a positional UI test independently of excluded tooling files", () => {
|
|
const argv = [
|
|
"list",
|
|
"ui/src/pages/chat/chat-send.test.ts",
|
|
"--exclude",
|
|
"test/scripts/run-vitest.test.ts",
|
|
"--passWithNoTests=false",
|
|
];
|
|
const native = parseCLI(["vitest", ...argv]);
|
|
|
|
expect(native.filter).toEqual(["ui/src/pages/chat/chat-send.test.ts"]);
|
|
expect(native.options.exclude).toEqual(["test/scripts/run-vitest.test.ts"]);
|
|
expect(native.options.passWithNoTests).toBe(false);
|
|
expect(resolveTestProjectsDelegationArgs(argv)).toBeNull();
|
|
expect(parseCLI(["vitest", ...resolveImplicitVitestArgs(argv)])).toEqual(
|
|
parseCLI(["vitest", "--config", "test/vitest/vitest.ui.config.ts", ...argv]),
|
|
);
|
|
});
|
|
|
|
it("routes explicit tooling tests through the tooling config", () => {
|
|
expect(resolveImplicitVitestArgs(["run", "test/scripts/run-vitest.test.ts"])).toEqual([
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.tooling.config.ts",
|
|
"test/scripts/run-vitest.test.ts",
|
|
]);
|
|
});
|
|
|
|
it("routes explicit Docker helper tests through the Docker tooling config", () => {
|
|
expect(resolveImplicitVitestArgs(["run", "test/scripts/docker-build-helper.test.ts"])).toEqual([
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.tooling-docker.config.ts",
|
|
"test/scripts/docker-build-helper.test.ts",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
"test/plugins/bundled-provider-auth-literal-parity.test.ts",
|
|
"test/scripts/openclaw-e2e-instance.test.ts",
|
|
])("keeps tooling-excluded explicit test %s on existing routing", (testFile) => {
|
|
const argv = ["run", testFile];
|
|
expect(resolveImplicitVitestArgs(argv)).toBe(argv);
|
|
});
|
|
|
|
it("keeps boundary tests on existing routing", () => {
|
|
const argv = ["run", "test/plugin-extension-import-boundary.test.ts"];
|
|
expect(resolveImplicitVitestArgs(argv)).toBe(argv);
|
|
});
|
|
|
|
it.each([
|
|
[],
|
|
["--passWithNoTests=false"],
|
|
["--passWithNoTests=true"],
|
|
["--passWithNoTests"],
|
|
["--passWithNoTests", "false"],
|
|
["--passWithNoTests=", "false"],
|
|
["--passWithNoTests", "true"],
|
|
["--no-passWithNoTests", "false"],
|
|
["--passWithNoTests=true", "--no-passWithNoTests"],
|
|
["--pass-with-no-tests=true"],
|
|
["--passWithNoTests=false", "--pass-with-no-tests=true"],
|
|
["--pass-with-no-tests=false", "--passWithNoTests=true"],
|
|
["--reporter=verbose", "--reporter=json"],
|
|
["--", "--passWithNoTests=true", "-x"],
|
|
])("enforces only direct empty-file policy for native args %j", async (...flags) => {
|
|
const argv = [
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.tooling.config.ts",
|
|
"test/scripts/run-vitest.test.ts",
|
|
...flags,
|
|
];
|
|
const original = [...argv];
|
|
const native = parseCLI(["vitest", ...argv]);
|
|
const guarded = await resolveExplicitTestFileNoPassArgs(argv);
|
|
expect(parseCLI(["vitest", ...guarded])).toEqual({
|
|
...native,
|
|
options: { ...native.options, passWithNoTests: false },
|
|
});
|
|
expect(argv).toEqual(original);
|
|
});
|
|
|
|
it.each([
|
|
["--passWithNoTests", "--passWithNoTests"],
|
|
["--no-passWithNoTests", "--passWithNoTests=true"],
|
|
["-no-passWithNoTests", "--passWithNoTests=true"],
|
|
["--pass-with-no-tests=false", "--pass-with-no-tests=false"],
|
|
["--passWithNoTests=", "false", "--passWithNoTests=false"],
|
|
])("preserves native invalid scalar errors for %j", async (...flags) => {
|
|
const argv = ["run", "test/scripts/run-vitest.test.ts", ...flags];
|
|
let nativeError: unknown;
|
|
try {
|
|
parseCLI(["vitest", ...argv]);
|
|
} catch (error) {
|
|
nativeError = error;
|
|
}
|
|
expect(nativeError).toBeInstanceOf(Error);
|
|
await expect(
|
|
Promise.resolve().then(() => resolveExplicitTestFileNoPassArgs(argv)),
|
|
).rejects.toThrow((nativeError as Error).message);
|
|
});
|
|
|
|
it("does not force no-test failure for globs or basename filters", async () => {
|
|
const argv = ["run", "run-vitest.test.ts", "test/**/*.test.ts"];
|
|
expect(await resolveExplicitTestFileNoPassArgs(argv)).toBe(argv);
|
|
});
|
|
|
|
it.each([
|
|
["--passWithNoTests", "false"],
|
|
["--pass-with-no-tests", "true"],
|
|
["--isolate", "false"],
|
|
["--no-passWithNoTests", "false"],
|
|
["--no-isolate", "true"],
|
|
["-no-passWithNoTests", "false"],
|
|
["--passWithNoTests", "--passWithNoTests"],
|
|
["--configLoader", "runner"],
|
|
["--configLoader=", "runner"],
|
|
["--isolate=", "false"],
|
|
])("round-trips native option ownership through delegation: %j", (...flags) => {
|
|
const file = "test/scripts/run-vitest.test.ts";
|
|
const parse = (args: string[]) => {
|
|
try {
|
|
return parseCLI(["vitest", "run", ...args]);
|
|
} catch (error) {
|
|
return String(error);
|
|
}
|
|
};
|
|
for (const argv of [
|
|
[...flags, file],
|
|
[file, ...flags],
|
|
]) {
|
|
const delegated = resolveTestProjectsDelegationArgs(argv);
|
|
expect(delegated).not.toBeNull();
|
|
const { forwardedArgs } = parseTestProjectsArgs(delegated!);
|
|
expect(parse(forwardedArgs)).toEqual(parse(argv));
|
|
}
|
|
});
|
|
|
|
it("keeps line-qualified native filters out of project delegation", () => {
|
|
expect(resolveTestProjectsDelegationArgs(["test/scripts/run-vitest.test.ts:12"])).toBeNull();
|
|
});
|
|
|
|
it("delegates bare explicit test files to the project router", () => {
|
|
const file = "test/scripts/run-vitest.test.ts";
|
|
for (const [argv, expected] of [
|
|
[[file], [file]],
|
|
[["run", file], [file]],
|
|
[
|
|
["run", file, "--reporter=verbose"],
|
|
[file, "--", "--reporter=verbose"],
|
|
],
|
|
[
|
|
["--reporter=verbose", "run", file],
|
|
[file, "--", "--reporter=verbose"],
|
|
],
|
|
[
|
|
["run", file, "--", "--watch"],
|
|
[file, "--", "--watch"],
|
|
],
|
|
[
|
|
["run", file, "--", "--reporter=verbose"],
|
|
[file, "--", "--reporter=verbose"],
|
|
],
|
|
] as const) {
|
|
expect(resolveTestProjectsDelegationArgs([...argv])).toEqual(expected);
|
|
}
|
|
});
|
|
|
|
it("delegates bare explicit source files to the project router", () => {
|
|
const file = "extensions/codex/src/app-server/dynamic-tool-profile.ts";
|
|
|
|
expect(resolveTestProjectsDelegationArgs([file])).toEqual([file]);
|
|
expect(resolveTestProjectsDelegationArgs(["run", file, "--reporter=verbose"])).toEqual([
|
|
file,
|
|
"--",
|
|
"--reporter=verbose",
|
|
]);
|
|
});
|
|
|
|
it("delegates bare explicit directories, globs, and extensionless prefixes", () => {
|
|
expect(resolveTestProjectsDelegationArgs(["test/scripts"])).toEqual(["test/scripts"]);
|
|
expect(
|
|
resolveTestProjectsDelegationArgs(["run", "test/scripts", "--reporter=verbose"]),
|
|
).toEqual(["test/scripts", "--", "--reporter=verbose"]);
|
|
expect(resolveTestProjectsDelegationArgs(["test/scripts/*.test.ts"])).toEqual([
|
|
"test/scripts/*.test.ts",
|
|
]);
|
|
expect(resolveTestProjectsDelegationArgs(["src/agents/**/*.ts"])).toBeNull();
|
|
expect(resolveTestProjectsDelegationArgs(["src/**/*.test.ts"])).toBeNull();
|
|
expect(resolveTestProjectsDelegationArgs(["./src"])).toBeNull();
|
|
const prefix = "extensions/telegram/src/format";
|
|
expect(resolveTestProjectsDelegationArgs([prefix])).toEqual([prefix]);
|
|
});
|
|
|
|
it("delegates an existing extension root to the project router", () => {
|
|
const directory = "extensions/codex";
|
|
|
|
expect(resolveTestProjectsDelegationArgs([directory])).toEqual([directory]);
|
|
expect(resolveTestProjectsDelegationArgs(["run", directory, "--reporter=verbose"])).toEqual([
|
|
directory,
|
|
"--",
|
|
"--reporter=verbose",
|
|
]);
|
|
});
|
|
|
|
it("keeps extension subdirectories and direct-mode root runs on Vitest", () => {
|
|
const directory = "extensions/codex";
|
|
|
|
expect(resolveTestProjectsDelegationArgs([`${directory}/src`])).toBeNull();
|
|
expect(
|
|
resolveTestProjectsDelegationArgs([
|
|
"--config",
|
|
"test/vitest/vitest.extension-codex.config.ts",
|
|
directory,
|
|
]),
|
|
).toBeNull();
|
|
expect(resolveTestProjectsDelegationArgs(["--watch", directory])).toBeNull();
|
|
});
|
|
|
|
it("delegates a plugin browser directory to its Control UI project owner", () => {
|
|
const directory = "extensions/workboard/browser";
|
|
expect(resolveTestProjectsDelegationArgs([directory])).toEqual([directory]);
|
|
});
|
|
|
|
it("delegates owned agent directories with separate Vitest option values", () => {
|
|
const directory = "src/agents/embedded-agent-runner/run";
|
|
|
|
expect(resolveTestProjectsDelegationArgs([directory])).toEqual([directory]);
|
|
expect(
|
|
resolveTestProjectsDelegationArgs([directory, "--sequence.shuffle", "--sequence.seed", "3"]),
|
|
).toEqual([directory, "--", "--sequence.shuffle", "--sequence.seed", "3"]);
|
|
});
|
|
|
|
it("delegates mixed filters when an explicit file target is present", () => {
|
|
expect(
|
|
resolveTestProjectsDelegationArgs(["src/agents", "test/scripts/run-vitest.test.ts"]),
|
|
).toEqual(["src/agents", "test/scripts/run-vitest.test.ts"]);
|
|
expect(
|
|
resolveTestProjectsDelegationArgs(["src/**/*.test.ts", "src/agents/bash-tools.ts"]),
|
|
).toEqual(["src/**/*.test.ts", "src/agents/bash-tools.ts"]);
|
|
});
|
|
|
|
it("keeps direct Vitest runs when project routing could change option semantics", () => {
|
|
const directArgvCases = [
|
|
[
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.tooling.config.ts",
|
|
"test/scripts/run-vitest.test.ts",
|
|
],
|
|
["--root", "packages/example", "src/example.test.ts"],
|
|
["--project", "tooling", "test/scripts/run-vitest.test.ts"],
|
|
["watch", "test/scripts/run-vitest.test.ts"],
|
|
["dev", "test/scripts/run-vitest.test.ts"],
|
|
["related", "src/agents/bash-tools.ts"],
|
|
["list", "src/agents/bash-tools.ts"],
|
|
["bench", "src/agents/bash-tools.ts"],
|
|
["--watch", "test/scripts/run-vitest.test.ts"],
|
|
["--run=false", "test/scripts/run-vitest.test.ts"],
|
|
["--no-run", "test/scripts/run-vitest.test.ts"],
|
|
["--run", "false", "test/scripts/run-vitest.test.ts"],
|
|
];
|
|
for (const argv of directArgvCases) {
|
|
expect(resolveTestProjectsDelegationArgs(argv)).toBeNull();
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
["--diff", "scripts/run-vitest.mjs", "test/scripts/run-vitest.test.ts"],
|
|
["test/scripts/run-vitest.test.ts", "--", "--diff", "scripts/run-vitest.mjs"],
|
|
],
|
|
[
|
|
["--testNamePattern", "run", "test/scripts/run-vitest.test.ts"],
|
|
["test/scripts/run-vitest.test.ts", "--", "--testNamePattern", "run"],
|
|
],
|
|
[
|
|
["run", "test/scripts/run-vitest.test.ts", "-t", "src"],
|
|
["test/scripts/run-vitest.test.ts", "--", "-t", "src"],
|
|
],
|
|
[
|
|
["run", "test/scripts/run-vitest.test.ts", "--repeats", "19"],
|
|
["test/scripts/run-vitest.test.ts", "--", "--repeats", "19"],
|
|
],
|
|
])("keeps option value %j out of project target classification", (argv, expected) => {
|
|
expect(resolveTestProjectsDelegationArgs(argv)).toEqual(expected);
|
|
});
|
|
|
|
it("reports missing explicit source files before Vitest can fan out by project", () => {
|
|
const fsImpl = {
|
|
existsSync: (filePath: string) =>
|
|
filePath.replaceAll("\\", "/").endsWith("src/agents/bash-tools.ts"),
|
|
};
|
|
|
|
expect(
|
|
resolveMissingExplicitTestFiles(
|
|
["src/agents/bash-tools.ts", "extensions/codex/src/app-server/missing.ts"],
|
|
"/repo",
|
|
fsImpl,
|
|
),
|
|
).toEqual(["extensions/codex/src/app-server/missing.ts"]);
|
|
});
|
|
|
|
it("does not treat option values or glob patterns as explicit missing files", () => {
|
|
const fsImpl = {
|
|
existsSync: () => false,
|
|
};
|
|
|
|
expect(
|
|
resolveMissingExplicitTestFiles(
|
|
[
|
|
"-t",
|
|
"missing.test.ts",
|
|
"basename-filter.test.ts",
|
|
"src/**/*.test.ts",
|
|
"--config",
|
|
"missing.config.ts",
|
|
"--exclude",
|
|
"ignored.test.ts",
|
|
"--bail",
|
|
"1",
|
|
"--mode",
|
|
"test",
|
|
"--mergeReports",
|
|
"reports.test.ts",
|
|
"--coverage.exclude",
|
|
"coverage.test.ts",
|
|
],
|
|
"/repo",
|
|
fsImpl,
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("skips missing-file preflight when Vitest controls path resolution", () => {
|
|
const fsImpl = {
|
|
existsSync: () => false,
|
|
};
|
|
|
|
expect(
|
|
resolveMissingExplicitTestFiles(
|
|
["--config", "test/vitest/vitest.gateway.config.ts", "server/health-state.test.ts"],
|
|
"/repo",
|
|
fsImpl,
|
|
),
|
|
).toEqual([]);
|
|
expect(
|
|
resolveMissingExplicitTestFiles(
|
|
["--root", "packages/example", "src/example.test.ts"],
|
|
"/repo",
|
|
fsImpl,
|
|
),
|
|
).toEqual([]);
|
|
expect(
|
|
resolveMissingExplicitTestFiles(["--dir=src", "example.test.ts"], "/repo", fsImpl),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("routes explicit non-e2e ui tests through the ui config", () => {
|
|
expect(resolveImplicitVitestArgs(["run", "ui/src/pages/chat/chat-send.test.ts"])).toEqual([
|
|
"run",
|
|
"--config",
|
|
"test/vitest/vitest.ui.config.ts",
|
|
"ui/src/pages/chat/chat-send.test.ts",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
["ui/src/components/markdown-mermaid.runtime.browser.test.ts"],
|
|
"test/vitest/vitest.ui-browser.config.ts",
|
|
],
|
|
[["ui/src/components/form-controls.browser.test.ts"], "test/vitest/vitest.ui.config.ts"],
|
|
[
|
|
[
|
|
"ui/src/components/markdown-mermaid.runtime.browser.test.ts",
|
|
"ui/src/components/form-controls.browser.test.ts",
|
|
],
|
|
null,
|
|
],
|
|
[["ui/src/**/*.browser.test.ts"], null],
|
|
[["ui/src/components/markdown.progress.node.test.ts"], null],
|
|
[
|
|
[
|
|
"ui/src/components/markdown.progress.node.test.ts",
|
|
"ui/src/components/form-controls.browser.test.ts",
|
|
],
|
|
null,
|
|
],
|
|
[["ui/src/components", "ui/src/pages/chat/chat-message-markdown.browser.test.ts"], null],
|
|
])("preserves browser ownership for implicit targets %j", (targets, config) => {
|
|
expect(resolveImplicitVitestArgs(["run", ...targets])).toEqual(
|
|
config ? ["run", "--config", config, ...targets] : ["run", ...targets],
|
|
);
|
|
});
|
|
|
|
it("allows opting back into Maglev while keeping Sparkplug compilation synchronous", () => {
|
|
expect(
|
|
resolveVitestNodeArgs({
|
|
OPENCLAW_VITEST_ENABLE_MAGLEV: "1",
|
|
PATH: "/usr/bin",
|
|
}),
|
|
).toStrictEqual(["--no-concurrent-sparkplug"]);
|
|
});
|
|
|
|
it("parses the optional no-output timeout env", () => {
|
|
expect(resolveVitestNoOutputTimeoutMs({})).toBeNull();
|
|
expect(resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2500" })).toBe(
|
|
2500,
|
|
);
|
|
expect(
|
|
resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0" }),
|
|
).toBeNull();
|
|
expect(
|
|
resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "1e3" }),
|
|
).toBeNull();
|
|
expect(
|
|
resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2500ms" }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("defaults direct non-watch runs to the stall watchdog", () => {
|
|
const expected = {
|
|
PATH: "/usr/bin",
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
|
|
};
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run", "-t", "watch"])).toEqual(
|
|
expected,
|
|
);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch=false"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch", "false"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--no-watch"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ CI: "true", PATH: "/usr/bin" }, ["src/foo.test.ts"])).toEqual(
|
|
{
|
|
CI: "true",
|
|
PATH: "/usr/bin",
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
|
|
},
|
|
);
|
|
expect(
|
|
resolveRunVitestSpawnEnv({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0", PATH: "/usr/bin" }, [
|
|
"run",
|
|
]),
|
|
).toEqual({
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("keeps mapped configs at their measured silence floor when env is smaller", () => {
|
|
expect(
|
|
resolveRunVitestSpawnEnv(
|
|
{ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000", PATH: "/usr/bin" },
|
|
["run", "--config", "test/vitest/vitest.extension-codex.config.ts"],
|
|
),
|
|
).toEqual({
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2400000",
|
|
PATH: "/usr/bin",
|
|
});
|
|
expect(
|
|
resolveRunVitestSpawnEnv(
|
|
{ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000", PATH: "/usr/bin" },
|
|
["run", "--config", "test/vitest/vitest.unit.config.ts"],
|
|
),
|
|
).toEqual({
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it.each([undefined, "1"])(
|
|
"forwards Node compile cache settings with explicit disable=%s",
|
|
(disabled) => {
|
|
const env = {
|
|
CI: "true",
|
|
NODE_COMPILE_CACHE: "/tmp/node-compile",
|
|
NODE_COMPILE_CACHE_PORTABLE: "1",
|
|
...(disabled ? { NODE_DISABLE_COMPILE_CACHE: disabled } : {}),
|
|
PATH: "/usr/bin",
|
|
};
|
|
for (const argv of [["run"], ["run", "--coverage=false"]]) {
|
|
expect(resolveRunVitestSpawnEnv(env, argv)).toEqual({
|
|
...env,
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
|
|
});
|
|
}
|
|
expect(resolveRunVitestSpawnEnv(env, ["--watch"])).toEqual(env);
|
|
expect(resolveVitestSpawnParams(env, "linux").env).toEqual(env);
|
|
},
|
|
);
|
|
|
|
describe("native config option ownership", () => {
|
|
const config = "test/vitest/vitest.e2e.config.ts";
|
|
const file = "test/scripts/run-vitest.test.ts";
|
|
it.each([
|
|
{ name: "long control", args: ["--config", config] },
|
|
{ name: "short control", args: ["-c", config] },
|
|
{ name: "inline long", args: [`--config=${config}`] },
|
|
{ name: "inline short", args: [`-c=${config}`] },
|
|
{ name: "empty inline long", args: ["--config=", config] },
|
|
{ name: "empty inline short", args: ["-c=", config] },
|
|
])("uses the native $name config for its watchdog", ({ args }) => {
|
|
const argv = ["run", ...args];
|
|
expect(parseCLI(["vitest", ...argv]).options.config).toBe(config);
|
|
expect(resolveDefaultVitestNoOutputTimeoutMs(argv)).toBe(
|
|
DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
|
);
|
|
});
|
|
it.each([
|
|
{ name: "missing long value", args: [file, "--config"] },
|
|
{ name: "missing short inline value", args: [file, "-c="] },
|
|
{ name: "long after separator", args: [file, "--", "--config", config] },
|
|
{ name: "short after separator", args: [file, "--", `-c=${config}`] },
|
|
])("keeps $name with the direct native child", ({ args }) => {
|
|
expect(resolveTestProjectsDelegationArgs(args)).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("uses a longer default stall watchdog for broad e2e and project shard configs", () => {
|
|
const timeout = String(DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS);
|
|
const extraLongTimeout = String(DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS);
|
|
|
|
for (const configArg of [
|
|
"--config=test/vitest/vitest.e2e.config.ts",
|
|
"--config=test/vitest/vitest.package-contract.config.ts",
|
|
"--config=test/vitest/vitest.tui-pty.config.ts",
|
|
"--config=test/vitest/vitest.gateway.config.ts",
|
|
"--config=./test/vitest/vitest.ui-e2e.config.ts",
|
|
"--config=test/vitest/vitest.full-agentic.config.ts",
|
|
"--config=test/vitest/vitest.full-core-contracts.config.ts",
|
|
]) {
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run", configArg])).toEqual({
|
|
PATH: "/usr/bin",
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: timeout,
|
|
});
|
|
}
|
|
for (const configArg of [
|
|
"--config=test/vitest/vitest.contracts-plugin.config.ts",
|
|
"--config=test/vitest/vitest.infra.config.ts",
|
|
"--config=test/vitest/vitest.gateway-core.config.ts",
|
|
"--config=test/vitest/vitest.gateway-server.config.ts",
|
|
]) {
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run", configArg])).toEqual({
|
|
PATH: "/usr/bin",
|
|
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "30000",
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: extraLongTimeout,
|
|
});
|
|
}
|
|
for (const [option, config, expected] of [
|
|
["-c", "gateway", DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["-c", "e2e", DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "full-agentic", DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "full-core-contracts", DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "contracts-plugin", DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "infra", DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "gateway-core", DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
["--config", "gateway-server", DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS],
|
|
] as const) {
|
|
expect(
|
|
resolveDefaultVitestNoOutputTimeoutMs([
|
|
"run",
|
|
option,
|
|
`/repo/test/vitest/vitest.${config}.config.ts`,
|
|
]),
|
|
).toBe(expected);
|
|
}
|
|
});
|
|
|
|
it("does not default implicit interactive runs to the stall watchdog", () => {
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["src/foo.test.ts"])).toEqual({
|
|
PATH: "/usr/bin",
|
|
});
|
|
expect(
|
|
resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, [
|
|
"--config",
|
|
"test/vitest/vitest.unit.config.ts",
|
|
"-t",
|
|
"watch",
|
|
]),
|
|
).toEqual({
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("does not default explicit watch runs to the stall watchdog", () => {
|
|
const expected = { PATH: "/usr/bin" };
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run", "--watch"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["-w"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch=0"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--run=false"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["watch"])).toEqual(expected);
|
|
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["dev"])).toEqual(expected);
|
|
});
|
|
|
|
it("spawns vitest in a detached process group on Unix hosts", () => {
|
|
expect(resolveVitestSpawnParams({ PATH: "/usr/bin" }, "darwin")).toEqual({
|
|
env: { PATH: "/usr/bin" },
|
|
detached: true,
|
|
stdio: ["inherit", "pipe", "pipe"],
|
|
});
|
|
expect(resolveVitestSpawnParams({ PATH: "/usr/bin" }, "win32")).toEqual({
|
|
env: { PATH: "/usr/bin" },
|
|
detached: false,
|
|
stdio: ["inherit", "pipe", "pipe"],
|
|
});
|
|
});
|
|
|
|
posixIt("terminates a silent Vitest child through the watchdog", async () => {
|
|
const watched = spawnWatchedVitestProcess({
|
|
pnpmArgs: ["exec", "node", "-e", "setInterval(() => {}, 1000)"],
|
|
spawnParams: {
|
|
detached: true,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
env: { OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "100" },
|
|
});
|
|
|
|
try {
|
|
expect(await waitForClose(watched.child)).toEqual({ code: null, signal: "SIGTERM" });
|
|
expect(await watched.completion).toEqual({
|
|
code: null,
|
|
signal: "SIGTERM",
|
|
groupJoined: true,
|
|
});
|
|
} finally {
|
|
watched.teardown();
|
|
forceKillVitestProcessGroup(watched.child);
|
|
await watched.completion;
|
|
}
|
|
});
|
|
|
|
posixIt.each([
|
|
{ timeout: false, exitCode: 0, expectedCode: 0 },
|
|
{ timeout: true, exitCode: 0, expectedCode: 1 },
|
|
{ timeout: true, exitCode: 7, expectedCode: 7 },
|
|
])("settles descendants (timeout=$timeout, child=$exitCode)", async (row) => {
|
|
const watchedEnv = {
|
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "5000",
|
|
};
|
|
let noOutputTimedOut = false;
|
|
// Only watchdog timers are fake; child I/O, diagnostics, and group joins stay real.
|
|
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
const { clock } = setTimeout as typeof setTimeout & { clock: { tick(ms: number): void } };
|
|
let watched: ReturnType<typeof spawnWatchedVitestProcess>;
|
|
try {
|
|
watched = spawnWatchedVitestProcess({
|
|
pnpmArgs: [
|
|
"exec",
|
|
"node",
|
|
"-e",
|
|
[
|
|
'const { spawn } = require("node:child_process");',
|
|
`process.once("SIGTERM", () => process.exit(${row.exitCode}));`,
|
|
'const descendant = spawn(process.execPath, ["-e",',
|
|
' "setInterval(() => {}, 1000); process.send(process.pid);",',
|
|
'], { stdio: ["ignore", "ignore", "ignore", "ipc"] });',
|
|
'descendant.once("message", (pid) => {',
|
|
" descendant.disconnect();",
|
|
" process.stdout.write(`${pid}\\n`);",
|
|
"});",
|
|
"descendant.unref();",
|
|
"setInterval(() => {}, 1000);",
|
|
].join("\n"),
|
|
],
|
|
spawnParams: {
|
|
detached: true,
|
|
env: watchedEnv,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
env: watchedEnv,
|
|
onNoOutputTimeout: () => {
|
|
noOutputTimedOut = true;
|
|
},
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
const rawExit = waitForClose(watched.child, LOAD_SENSITIVE_PROCESS_TIMEOUT_MS);
|
|
let descendantPid = 0;
|
|
const lines = createInterface({ input: watched.child.stdout! });
|
|
const ready = new Promise<void>((resolve, reject) => {
|
|
lines.once("line", (line) => {
|
|
try {
|
|
// The descendant acknowledges its running loop on the watched pipe. Check
|
|
// and signal in this notification, before a delayed poll can outlive it.
|
|
descendantPid = Number(line);
|
|
expect(Number.isInteger(descendantPid) && descendantPid > 0).toBe(true);
|
|
expect(isProcessAlive(descendantPid)).toBe(true);
|
|
if (row.timeout) {
|
|
clock.tick(Number(watchedEnv.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS));
|
|
} else {
|
|
process.kill(watched.child.pid!, "SIGTERM");
|
|
}
|
|
resolve();
|
|
} catch (error) {
|
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
}
|
|
});
|
|
lines.once("close", () => reject(new Error("fixture closed before reporting readiness")));
|
|
});
|
|
|
|
try {
|
|
const snapshot = await Promise.race([
|
|
Promise.all([ready, rawExit, watched.completion]).then(([, raw, result]) => {
|
|
const psArgs =
|
|
process.platform === "linux" ? ["-eL", "-o", "pgid=,state="] : ["-axo", "pgid=,state="];
|
|
const stateResult = spawnSync("ps", psArgs, {
|
|
encoding: "utf8",
|
|
});
|
|
const rows = stateResult.stdout
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map((line) => /^\s*(\d+)\s+(\S+)\s*$/.exec(line));
|
|
const groupStopped =
|
|
!stateResult.error &&
|
|
stateResult.signal === null &&
|
|
stateResult.stderr.trim() === "" &&
|
|
stateResult.status === 0 &&
|
|
rows.every(Boolean) &&
|
|
rows
|
|
.filter((processRow) => Number(processRow?.[1]) === watched.child.pid)
|
|
.every((processRow) => /^[ZX]/.test(processRow?.[2] ?? ""));
|
|
return { groupStopped, noOutputTimedOut, raw, result };
|
|
}),
|
|
delay(LOAD_SENSITIVE_PROCESS_TIMEOUT_MS, undefined, { ref: false }).then(() => {
|
|
throw new Error("timed out waiting for watched Vitest completion");
|
|
}),
|
|
]);
|
|
|
|
expect(snapshot).toEqual({
|
|
groupStopped: true,
|
|
noOutputTimedOut: row.timeout,
|
|
raw: { code: row.exitCode, signal: null },
|
|
result: { code: row.expectedCode, signal: null, groupJoined: true },
|
|
});
|
|
} finally {
|
|
lines.close();
|
|
watched.teardown();
|
|
forceKillVitestProcessGroup(watched.child);
|
|
if (descendantPid && isProcessAlive(descendantPid)) {
|
|
process.kill(descendantPid, "SIGKILL");
|
|
}
|
|
await watched.completion;
|
|
}
|
|
});
|
|
|
|
it("reenables local check policy for local Vitest children", () => {
|
|
expect(
|
|
resolveVitestSpawnParams(
|
|
{
|
|
OPENCLAW_LOCAL_CHECK: "0",
|
|
PATH: "/usr/bin",
|
|
},
|
|
"darwin",
|
|
).env,
|
|
).toEqual({
|
|
OPENCLAW_LOCAL_CHECK: "1",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("preserves explicit local-check disablement in CI", () => {
|
|
expect(
|
|
resolveVitestSpawnParams(
|
|
{
|
|
CI: "true",
|
|
OPENCLAW_LOCAL_CHECK: "0",
|
|
PATH: "/usr/bin",
|
|
},
|
|
"linux",
|
|
).env,
|
|
).toEqual({
|
|
CI: "true",
|
|
OPENCLAW_LOCAL_CHECK: "0",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("caps native Rust worker pools for serial Vitest runs", () => {
|
|
expect(
|
|
resolveVitestSpawnParams(
|
|
{
|
|
OPENCLAW_TEST_PROJECTS_SERIAL: "1",
|
|
PATH: "/usr/bin",
|
|
},
|
|
"darwin",
|
|
).env,
|
|
).toEqual({
|
|
OPENCLAW_TEST_PROJECTS_SERIAL: "1",
|
|
PATH: "/usr/bin",
|
|
RAYON_NUM_THREADS: "1",
|
|
TOKIO_WORKER_THREADS: "1",
|
|
});
|
|
});
|
|
|
|
it("keeps explicit native Rust worker pool settings", () => {
|
|
expect(
|
|
resolveVitestSpawnParams(
|
|
{
|
|
OPENCLAW_VITEST_MAX_WORKERS: "2",
|
|
PATH: "/usr/bin",
|
|
RAYON_NUM_THREADS: "8",
|
|
TOKIO_WORKER_THREADS: "6",
|
|
},
|
|
"darwin",
|
|
).env,
|
|
).toEqual({
|
|
OPENCLAW_VITEST_MAX_WORKERS: "2",
|
|
PATH: "/usr/bin",
|
|
RAYON_NUM_THREADS: "8",
|
|
TOKIO_WORKER_THREADS: "6",
|
|
});
|
|
});
|
|
|
|
it("does not truncate malformed native worker budgets", () => {
|
|
expect(
|
|
resolveVitestSpawnParams(
|
|
{
|
|
OPENCLAW_TEST_PROJECTS_SERIAL: "1",
|
|
OPENCLAW_VITEST_MAX_WORKERS: "8x",
|
|
PATH: "/usr/bin",
|
|
},
|
|
"darwin",
|
|
).env,
|
|
).toEqual({
|
|
OPENCLAW_TEST_PROJECTS_SERIAL: "1",
|
|
OPENCLAW_VITEST_MAX_WORKERS: "8x",
|
|
PATH: "/usr/bin",
|
|
RAYON_NUM_THREADS: "1",
|
|
TOKIO_WORKER_THREADS: "1",
|
|
});
|
|
});
|
|
|
|
it("suppresses rolldown plugin timing noise while keeping other stderr intact", () => {
|
|
expect(
|
|
shouldSuppressVitestStderrLine(
|
|
"\u001b[33m[PLUGIN_TIMINGS] Warning:\u001b[0m plugin `foo` was slow\n",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
shouldSuppressVitestStderrLine(
|
|
"\u001b[33m[PLUGIN_TIMINGS] \u001b[0mYour build spent significant time in plugin `externalize-deps`.\n",
|
|
),
|
|
).toBe(true);
|
|
expect(shouldSuppressVitestStderrLine("real failure output\n")).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "plain output",
|
|
output: [
|
|
"⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯",
|
|
"Vitest caught 1 unhandled error during the test run.",
|
|
"⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯",
|
|
"TypeError: request failed",
|
|
].join("\n"),
|
|
expected: { count: 1, errorFirstLine: "TypeError: request failed", origin: undefined },
|
|
},
|
|
{
|
|
name: "ANSI-colored output with an origin",
|
|
output: [
|
|
"\u001b[41m⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯\u001b[0m",
|
|
"\u001b[31mVitest caught 2 unhandled errors during the test run.\u001b[0m",
|
|
"\u001b[41m⎯⎯⎯⎯ Uncaught Exception ⎯⎯⎯⎯\u001b[0m",
|
|
"\u001b[31mReferenceError: ResizeObserver is not defined\u001b[0m",
|
|
'This error originated in "src/app/app-host.dock-suppression.test.ts" test file.',
|
|
].join("\n"),
|
|
expected: {
|
|
count: 2,
|
|
errorFirstLine: "ReferenceError: ResizeObserver is not defined",
|
|
origin: "src/app/app-host.dock-suppression.test.ts",
|
|
},
|
|
},
|
|
{
|
|
name: "ordinary test output",
|
|
output: "✓ unit src/app/app-host.dock-suppression.test.ts (1 test)\n",
|
|
expected: null,
|
|
},
|
|
])("detects unhandled errors in $name", ({ output, expected }) => {
|
|
const detector = createVitestUnhandledErrorDetector();
|
|
detector.observe(output);
|
|
|
|
expect(detector.finish()).toEqual(expected);
|
|
});
|
|
|
|
it("only emits a workflow annotation under GitHub Actions", () => {
|
|
const result = {
|
|
count: 2,
|
|
origin: "src/app/app-host.dock-suppression.test.ts",
|
|
errorFirstLine: "ReferenceError: ResizeObserver is not defined",
|
|
};
|
|
const localLog = vi.fn();
|
|
const actionsLog = vi.fn();
|
|
|
|
writeVitestUnhandledErrorSummary(result, {}, localLog);
|
|
writeVitestUnhandledErrorSummary(result, { GITHUB_ACTIONS: "true" }, actionsLog);
|
|
|
|
const summary =
|
|
"[vitest] UNHANDLED ERRORS (2): src/app/app-host.dock-suppression.test.ts — ReferenceError: ResizeObserver is not defined";
|
|
expect(localLog).toHaveBeenCalledOnce();
|
|
expect(localLog).toHaveBeenCalledWith(summary);
|
|
expect(actionsLog.mock.calls).toEqual([[`::error::${summary}`], [summary]]);
|
|
});
|
|
|
|
it("kills silent vitest runs after the configured idle timeout", () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const stdout = new EventEmitter();
|
|
const timeoutSpy = vi.fn();
|
|
const forceKillSpy = vi.fn();
|
|
const logSpy = vi.fn();
|
|
|
|
const watchdog = installVitestNoOutputWatchdog({
|
|
streams: [stdout],
|
|
timeoutMs: 1000,
|
|
forceKillAfterMs: 5000,
|
|
log: logSpy,
|
|
onTimeout: timeoutSpy,
|
|
onForceKill: forceKillSpy,
|
|
setTimeoutFn: setTimeout,
|
|
clearTimeoutFn: clearTimeout,
|
|
});
|
|
|
|
vi.advanceTimersByTime(900);
|
|
expect(timeoutSpy).not.toHaveBeenCalled();
|
|
|
|
stdout.emit("data", "still alive");
|
|
vi.advanceTimersByTime(900);
|
|
expect(timeoutSpy).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(100);
|
|
expect(timeoutSpy).toHaveBeenCalledTimes(1);
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
"[vitest] no output for 1000ms; terminating stalled Vitest process group.",
|
|
);
|
|
|
|
vi.advanceTimersByTime(5000);
|
|
expect(forceKillSpy).toHaveBeenCalledTimes(1);
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
"[vitest] process group still alive after 5000ms; sending SIGKILL.",
|
|
);
|
|
|
|
watchdog.teardown();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it.each(["output", "preparation"] as const)(
|
|
"keeps force-kill scheduled when %s arrives after the idle timeout",
|
|
(activity) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const stdout = new EventEmitter();
|
|
const timeoutSpy = vi.fn();
|
|
const forceKillSpy = vi.fn();
|
|
|
|
const watchdog = installVitestNoOutputWatchdog({
|
|
streams: [stdout],
|
|
timeoutMs: 1000,
|
|
forceKillAfterMs: 5000,
|
|
onTimeout: timeoutSpy,
|
|
onForceKill: forceKillSpy,
|
|
setTimeoutFn: setTimeout,
|
|
clearTimeoutFn: clearTimeout,
|
|
});
|
|
|
|
vi.advanceTimersByTime(1000);
|
|
expect(timeoutSpy).toHaveBeenCalledTimes(1);
|
|
|
|
if (activity === "output") {
|
|
stdout.emit("data", "too late");
|
|
} else {
|
|
watchdog.recordActivity();
|
|
}
|
|
vi.advanceTimersByTime(5000);
|
|
|
|
expect(timeoutSpy).toHaveBeenCalledTimes(1);
|
|
expect(forceKillSpy).toHaveBeenCalledTimes(1);
|
|
watchdog.teardown();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("prints bounded heartbeats before killing silent vitest runs", () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const stdout = new EventEmitter();
|
|
const timeoutSpy = vi.fn();
|
|
const logSpy = vi.fn();
|
|
|
|
installVitestNoOutputWatchdog({
|
|
streams: [stdout],
|
|
timeoutMs: 1000,
|
|
heartbeatMs: 400,
|
|
forceKillAfterMs: 0,
|
|
log: logSpy,
|
|
onTimeout: timeoutSpy,
|
|
setTimeoutFn: setTimeout,
|
|
clearTimeoutFn: clearTimeout,
|
|
});
|
|
|
|
vi.advanceTimersByTime(400);
|
|
expect(logSpy).toHaveBeenCalledWith("[vitest] still running with no output for 400ms.");
|
|
|
|
vi.advanceTimersByTime(400);
|
|
expect(logSpy).toHaveBeenCalledWith("[vitest] still running with no output for 800ms.");
|
|
|
|
stdout.emit("data", "still alive");
|
|
vi.advanceTimersByTime(400);
|
|
expect(logSpy).toHaveBeenCalledWith("[vitest] still running with no output for 400ms.");
|
|
|
|
vi.advanceTimersByTime(600);
|
|
expect(timeoutSpy).toHaveBeenCalledTimes(1);
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
"[vitest] no output for 1000ms; terminating stalled Vitest process group.",
|
|
);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("parses the optional watchdog heartbeat interval", () => {
|
|
expect(
|
|
resolveVitestNoOutputHeartbeatMs({ OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000" }),
|
|
).toBe(120000);
|
|
expect(
|
|
resolveVitestNoOutputHeartbeatMs({ OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "0" }),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
async function waitForClose(child: ReturnType<typeof spawn>, timeoutMs = 5_000) {
|
|
return await Promise.race([
|
|
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {
|
|
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
}),
|
|
delay(timeoutMs, undefined, { ref: false }).then(() => {
|
|
throw new Error("timed out waiting for child close");
|
|
}),
|
|
]);
|
|
}
|
|
|
|
function isProcessAlive(pid: number) {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|