From 258cc8496571e130af73f314379038e3e3f9643e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Jul 2026 00:41:49 -0400 Subject: [PATCH] test(cli): isolate process exit coverage (#114903) --- scripts/lib/ci-node-test-plan.mjs | 2 +- scripts/test-projects.test-support.mjs | 26 +++++++++++++++++++ test/scripts/ci-node-test-plan.test.ts | 2 +- test/scripts/test-projects.test.ts | 30 +++++++++++++++++++++- test/vitest-scoped-config.test.ts | 12 +++++++++ test/vitest/vitest.cli-process-paths.d.mts | 2 ++ test/vitest/vitest.cli-process-paths.mjs | 14 ++++++++++ test/vitest/vitest.cli-process.config.ts | 18 +++++++++++++ test/vitest/vitest.cli.config.ts | 2 ++ test/vitest/vitest.config.ts | 1 + test/vitest/vitest.scoped-config.ts | 1 + test/vitest/vitest.test-shards.mjs | 1 + 12 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/vitest/vitest.cli-process-paths.d.mts create mode 100644 test/vitest/vitest.cli-process-paths.mjs create mode 100644 test/vitest/vitest.cli-process.config.ts diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index f5d063ebc6f3..5a789c85610e 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -1167,7 +1167,7 @@ const SPLIT_NODE_SHARDS = new Map([ ...createGatewayServerSplitShards(), { shardName: "agentic-cli", - configs: ["test/vitest/vitest.cli.config.ts"], + configs: ["test/vitest/vitest.cli.config.ts", "test/vitest/vitest.cli-process.config.ts"], requiresDist: false, }, { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index c0a90dbdee80..f35fb68e5d88 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -11,6 +11,10 @@ import { isAgentsCoreIsolatedTestFile, } from "../test/vitest/vitest.agents-paths.mjs"; import { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs"; +import { + cliProcessTestFiles, + isCliProcessTestFile, +} from "../test/vitest/vitest.cli-process-paths.mjs"; import { commandsLightTestFiles, isCommandsLightTarget, @@ -98,6 +102,7 @@ const AUTO_REPLY_TOP_LEVEL_VITEST_CONFIG = "test/vitest/vitest.auto-reply-top-le const BOUNDARY_VITEST_CONFIG = "test/vitest/vitest.boundary.config.ts"; const BUNDLED_VITEST_CONFIG = "test/vitest/vitest.bundled.config.ts"; const CHANNEL_VITEST_CONFIG = "test/vitest/vitest.channels.config.ts"; +const CLI_PROCESS_VITEST_CONFIG = "test/vitest/vitest.cli-process.config.ts"; const CLI_VITEST_CONFIG = "test/vitest/vitest.cli.config.ts"; const COMMANDS_LIGHT_VITEST_CONFIG = "test/vitest/vitest.commands-light.config.ts"; const COMMANDS_VITEST_CONFIG = "test/vitest/vitest.commands.config.ts"; @@ -225,6 +230,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ [EXTENSION_TELEGRAM_VITEST_CONFIG, 94], [EXTENSION_WHATSAPP_VITEST_CONFIG, 92], [AUTO_REPLY_CORE_VITEST_CONFIG, 90], + [CLI_PROCESS_VITEST_CONFIG, 87], [CLI_VITEST_CONFIG, 86], [MEDIA_VITEST_CONFIG, 84], [PLUGINS_VITEST_CONFIG, 82], @@ -343,6 +349,7 @@ const VITEST_CONFIG_BY_KIND = { boundary: BOUNDARY_VITEST_CONFIG, bundled: BUNDLED_VITEST_CONFIG, channel: CHANNEL_VITEST_CONFIG, + cliProcess: CLI_PROCESS_VITEST_CONFIG, cli: CLI_VITEST_CONFIG, command: COMMANDS_VITEST_CONFIG, commandLight: COMMANDS_LIGHT_VITEST_CONFIG, @@ -4215,6 +4222,9 @@ function classifyTarget(arg, cwd) { if (isPathAtOrUnder(relative, "src/acp")) { return "acp"; } + if (isCliProcessTestFile(relative)) { + return "cliProcess"; + } if (isPathAtOrUnder(relative, "src/cli")) { return "cli"; } @@ -4438,6 +4448,21 @@ export function buildVitestRunPlans( } groupedTargets.set("uiIsolated", current); } + const cliTargets = groupedTargets.get("cli") ?? []; + const impliedCliProcessTargets = cliProcessTestFiles.filter((file) => + cliTargets.some((targetArg) => + includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [file]), + ), + ); + if (impliedCliProcessTargets.length > 0) { + const current = groupedTargets.get("cliProcess") ?? []; + for (const target of impliedCliProcessTargets) { + if (!current.includes(target)) { + current.push(target); + } + } + groupedTargets.set("cliProcess", current); + } if (watchMode && groupedTargets.size > 1) { throw new Error( @@ -4482,6 +4507,7 @@ export function buildVitestRunPlans( "tuiPty", "mediaUnderstanding", "acp", + "cliProcess", "cli", "commandLight", "command", diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 51500b1505cb..d83e7820b799 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -842,7 +842,7 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { expect(cliShard).toEqual({ checkName: "checks-node-agentic-cli", shardName: "agentic-cli", - configs: ["test/vitest/vitest.cli.config.ts"], + configs: ["test/vitest/vitest.cli.config.ts", "test/vitest/vitest.cli-process.config.ts"], requiresDist: false, runner: DEFAULT_NODE_TEST_RUNNER, }); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 8cf0613c8204..432b369f112f 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -2902,7 +2902,6 @@ describe("scripts/test-projects changed-target routing", () => { ["src/agents", "test/vitest/vitest.agents.config.ts"], ["src/auto-reply", "test/vitest/vitest.auto-reply.config.ts"], ["src/channels", "test/vitest/vitest.channels.config.ts"], - ["src/cli", "test/vitest/vitest.cli.config.ts"], ["src/config", "test/vitest/vitest.runtime-config.config.ts"], ["src/cron", "test/vitest/vitest.cron.config.ts"], ["src/daemon", "test/vitest/vitest.daemon.config.ts"], @@ -2986,6 +2985,34 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); + it("routes CLI process tests through their isolated project", () => { + expect(buildVitestRunPlans(["src/cli/help-exit.process.test.ts"])).toEqual([ + { + config: "test/vitest/vitest.cli-process.config.ts", + forwardedArgs: [], + includePatterns: ["src/cli/help-exit.process.test.ts"], + watchMode: false, + }, + ]); + }); + + it("adds the CLI process project for broad CLI targets", () => { + const plans = buildVitestRunPlans(["src/cli"]); + + expect(plans.map((plan) => plan.config)).toEqual([ + "test/vitest/vitest.unit-fast.config.ts", + "test/vitest/vitest.cli-process.config.ts", + "test/vitest/vitest.cli.config.ts", + ]); + expect(plans[1]?.includePatterns).toContain("src/cli/help-exit.process.test.ts"); + }); + + it("rejects broad CLI watch targets that cross shared and process projects", () => { + expect(() => buildVitestRunPlans(["--watch", "src/cli"])).toThrow( + "watch mode with mixed test suites is not supported", + ); + }); + it("chunks broad shell helper globs after isolated targets", () => { const plans = buildVitestRunPlans(["test/scripts/*.test.ts"], process.cwd()); expect(plans.slice(0, 4)).toEqual([ @@ -4778,6 +4805,7 @@ describe("scripts/test-projects full-suite sharding", () => { gatewayServerConfig, gatewayServerConfig, gatewayServerConfig, + "test/vitest/vitest.cli-process.config.ts", "test/vitest/vitest.cli.config.ts", "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index d3b9071f56f9..0b4806bfc307 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -17,6 +17,8 @@ import { createAutoReplyTopLevelVitestConfig } from "./vitest/vitest.auto-reply- import { createAutoReplyVitestConfig } from "./vitest/vitest.auto-reply.config.ts"; import bundledVitestConfig from "./vitest/vitest.bundled.config.ts"; import { createChannelsVitestConfig } from "./vitest/vitest.channels.config.ts"; +import { cliProcessTestFiles } from "./vitest/vitest.cli-process-paths.mjs"; +import { createCliProcessVitestConfig } from "./vitest/vitest.cli-process.config.ts"; import { createCliVitestConfig } from "./vitest/vitest.cli.config.ts"; import { createCommandsLightVitestConfig } from "./vitest/vitest.commands-light.config.ts"; import { createCommandsVitestConfig } from "./vitest/vitest.commands.config.ts"; @@ -521,6 +523,7 @@ describe("createScopedVitestConfig", () => { describe("scoped vitest configs", () => { const defaultChannelsConfig = createChannelsVitestConfig({}); const defaultAcpConfig = createAcpVitestConfig({}); + const defaultCliProcessConfig = createCliProcessVitestConfig({}); const defaultCliConfig = createCliVitestConfig({}); const defaultExtensionsConfig = createExtensionsVitestConfig({}); const defaultExtensionAcpxConfig = createExtensionAcpxVitestConfig({}); @@ -611,6 +614,15 @@ describe("scoped vitest configs", () => { expectThreadedIsolatedRunner(defaultExtensionMemoryConfig); expectThreadedIsolatedRunner(defaultExtensionProvidersConfig); expectForkedIsolatedRunner(defaultInfraConfig); + expectForkedIsolatedRunner(defaultCliProcessConfig); + }); + + it("keeps process-launching CLI files out of the shared CLI graph", () => { + expect(requireTestConfig(defaultCliConfig).exclude).toEqual( + expect.arrayContaining(cliProcessTestFiles.map((file) => file.replace("src/cli/", ""))), + ); + expect(requireTestConfig(defaultCliProcessConfig).include).toEqual(cliProcessTestFiles); + expect(requireTestConfig(defaultCliProcessConfig).fileParallelism).toBe(false); }); it("keeps native SQLite runtime config tests in forked workers", () => { diff --git a/test/vitest/vitest.cli-process-paths.d.mts b/test/vitest/vitest.cli-process-paths.d.mts new file mode 100644 index 000000000000..3f6fdeca6b03 --- /dev/null +++ b/test/vitest/vitest.cli-process-paths.d.mts @@ -0,0 +1,2 @@ +export const cliProcessTestFiles: string[]; +export function isCliProcessTestFile(file: string): boolean; diff --git a/test/vitest/vitest.cli-process-paths.mjs b/test/vitest/vitest.cli-process-paths.mjs new file mode 100644 index 000000000000..d186ae44c8e5 --- /dev/null +++ b/test/vitest/vitest.cli-process-paths.mjs @@ -0,0 +1,14 @@ +// CLI process tests launch real Node+tsx children and must not contend with the +// shared CLI module graph. Keep the owned list explicit so full and focused runs agree. +export const cliProcessTestFiles = [ + "src/cli/acp-cli-exit.process.test.ts", + "src/cli/gateway-backed-exit.process.test.ts", + "src/cli/help-exit.process.test.ts", + "src/cli/hooks-cli.process.test.ts", +]; + +const cliProcessTestFileSet = new Set(cliProcessTestFiles); + +export function isCliProcessTestFile(value) { + return cliProcessTestFileSet.has(value.replaceAll("\\", "/")); +} diff --git a/test/vitest/vitest.cli-process.config.ts b/test/vitest/vitest.cli-process.config.ts new file mode 100644 index 000000000000..b4fcdbf38ba0 --- /dev/null +++ b/test/vitest/vitest.cli-process.config.ts @@ -0,0 +1,18 @@ +import { cliProcessTestFiles } from "./vitest.cli-process-paths.mjs"; +// CLI process tests run serially in isolated forks so child startup deadlines +// measure the CLI rather than contention from the shared CLI test graph. +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export function createCliProcessVitestConfig(env?: Record) { + return createScopedVitestConfig(cliProcessTestFiles, { + env, + fileParallelism: false, + isolate: true, + name: "cli-process", + passWithNoTests: true, + pool: "forks", + useNonIsolatedRunner: false, + }); +} + +export default createCliProcessVitestConfig(); diff --git a/test/vitest/vitest.cli.config.ts b/test/vitest/vitest.cli.config.ts index 8f3516f7b717..b1a3114d0da9 100644 --- a/test/vitest/vitest.cli.config.ts +++ b/test/vitest/vitest.cli.config.ts @@ -1,3 +1,4 @@ +import { cliProcessTestFiles } from "./vitest.cli-process-paths.mjs"; // Vitest cli config wires the cli test shard. import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; @@ -5,6 +6,7 @@ export function createCliVitestConfig(env?: Record) return createScopedVitestConfig(["src/cli/**/*.test.ts"], { dir: "src/cli", env, + exclude: cliProcessTestFiles, name: "cli", passWithNoTests: true, }); diff --git a/test/vitest/vitest.config.ts b/test/vitest/vitest.config.ts index a8a90b813390..2f63f4b7982f 100644 --- a/test/vitest/vitest.config.ts +++ b/test/vitest/vitest.config.ts @@ -28,6 +28,7 @@ export const rootVitestProjects = [ "test/vitest/vitest.acp.config.ts", "test/vitest/vitest.runtime-config.config.ts", "test/vitest/vitest.secrets.config.ts", + "test/vitest/vitest.cli-process.config.ts", "test/vitest/vitest.cli.config.ts", "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index 8a5d3eb7dc86..b2320a61e7fc 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -107,6 +107,7 @@ const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map( "boundary", "bundled", "channels", + "cli-process", "cli", "commands", "commands-light", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index d3982475ed24..60eefdf69457 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -97,6 +97,7 @@ export const fullSuiteVitestShards = [ "test/vitest/vitest.gateway-client.config.ts", "test/vitest/vitest.gateway-methods.config.ts", "test/vitest/vitest.gateway-server.config.ts", + "test/vitest/vitest.cli-process.config.ts", "test/vitest/vitest.cli.config.ts", "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts",