diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index 9b578d0ccfda..faa66f70e740 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -5,6 +5,10 @@ import { embeddedAgentVitestProjectOwners, } from "../../test/vitest/vitest.agents-paths.mjs"; import { commandsLightTestFiles } from "../../test/vitest/vitest.commands-light-paths.mjs"; +import { + isGatewayServerBackedHttpTestFile, + isGatewayServerTestFile, +} from "../../test/vitest/vitest.gateway-server-paths.mjs"; import { fullSuiteVitestShards } from "../../test/vitest/vitest.test-shards.mjs"; import { toolingIsolatedTestFiles } from "../../test/vitest/vitest.tooling-isolated-paths.mjs"; import { @@ -612,29 +616,6 @@ function createAgentCoreSplitShards() { ]; } -const GATEWAY_SERVER_BACKED_HTTP_TESTS = new Set([ - "src/gateway/embeddings-http.test.ts", - "src/gateway/models-http.test.ts", - "src/gateway/openai-http.test.ts", - "src/gateway/openresponses-http.test.ts", - "src/gateway/probe.auth.integration.test.ts", -]); - -const GATEWAY_SERVER_EXCLUDED_TESTS = new Set([ - "src/gateway/gateway.test.ts", - "src/gateway/server.startup-matrix-migration.integration.test.ts", - "src/gateway/sessions-history-http.test.ts", -]); - -function isGatewayServerTestFile(file) { - return ( - file.startsWith("src/gateway/") && - !file.startsWith("src/gateway/server-methods/") && - !GATEWAY_SERVER_EXCLUDED_TESTS.has(file) && - (file.includes("server") || GATEWAY_SERVER_BACKED_HTTP_TESTS.has(file)) - ); -} - function resolveGatewayStartupShardName(file) { const name = relative("src/gateway", file).replaceAll("\\", "/"); if (name.startsWith("server-startup-config") || name.startsWith("server-startup-early")) { @@ -643,9 +624,7 @@ function resolveGatewayStartupShardName(file) { if ( name.startsWith("server-runtime") || name.startsWith("server.health") || - name.startsWith("server.lazy") || - name.startsWith("server/health-state") || - name.startsWith("server/readiness") + name.startsWith("server.lazy") ) { return "agentic-control-plane-startup-health-runtime"; } @@ -658,7 +637,7 @@ function resolveGatewayStartupShardName(file) { function resolveGatewayServerShardName(file) { const name = relative("src/gateway", file).replaceAll("\\", "/"); if ( - GATEWAY_SERVER_BACKED_HTTP_TESTS.has(file) || + isGatewayServerBackedHttpTestFile(file) || name.startsWith("server.models") || name.startsWith("server.talk") ) { @@ -688,8 +667,6 @@ function resolveGatewayServerShardName(file) { name.startsWith("server-runtime") || name.startsWith("server.lazy") || name.startsWith("server.health") || - name.startsWith("server/health-state") || - name.startsWith("server/readiness") || name === "server-close.test.ts" ) { return resolveGatewayStartupShardName(file); @@ -725,10 +702,7 @@ function resolveGatewayServerShardName(file) { ) { return "agentic-control-plane-runtime-ui-tools"; } - if (name.startsWith("server/")) { - return "agentic-control-plane-runtime-events"; - } - if (name.startsWith("server.") || name.startsWith("server/")) { + if (name.startsWith("server.")) { return "agentic-control-plane-runtime-state"; } return "agentic-control-plane-runtime"; @@ -748,7 +722,6 @@ function createGatewayServerSplitShards() { "agentic-control-plane-runtime", "agentic-control-plane-runtime-config", "agentic-control-plane-runtime-cron", - "agentic-control-plane-runtime-events", "agentic-control-plane-runtime-network", "agentic-control-plane-runtime-server", "agentic-control-plane-runtime-shared-token", diff --git a/scripts/lib/gateway-server-test-plan.d.mts b/scripts/lib/gateway-server-test-plan.d.mts new file mode 100644 index 000000000000..f05eba461c9e --- /dev/null +++ b/scripts/lib/gateway-server-test-plan.d.mts @@ -0,0 +1,4 @@ +export const GATEWAY_SERVER_TEST_PROCESS_COUNT: 4; +export function listGatewayServerTestTargets(cwd?: string): string[]; +export function splitTestTargetChunks(targets: string[], chunkCount: number): string[][]; +export function createGatewayServerTestTargetChunks(cwd?: string): string[][]; diff --git a/scripts/lib/gateway-server-test-plan.mjs b/scripts/lib/gateway-server-test-plan.mjs new file mode 100644 index 000000000000..2d4fe6599c3e --- /dev/null +++ b/scripts/lib/gateway-server-test-plan.mjs @@ -0,0 +1,72 @@ +// Bounds the non-isolated Gateway server project before its shared module graph +// reaches the V8 worker heap limit. +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { isGatewayServerTestFile } from "../../test/vitest/vitest.gateway-server-paths.mjs"; + +export const GATEWAY_SERVER_TEST_PROCESS_COUNT = 4; + +function normalizePath(value) { + return value.replaceAll("\\", "/"); +} + +function listRepoFilesRecursive(root, cwd) { + const entries = fs.readdirSync(root, { withFileTypes: true }); + return entries.flatMap((entry) => { + const absolute = path.join(root, entry.name); + if (entry.isDirectory()) { + return listRepoFilesRecursive(absolute, cwd); + } + return entry.isFile() ? [normalizePath(path.relative(cwd, absolute))] : []; + }); +} + +function listGatewayFilesFromGit(cwd) { + const result = spawnSync("git", ["ls-files", "--", "src/gateway"], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + return null; + } + return result.stdout + .split("\n") + .map((line) => normalizePath(line.trim())) + .filter((line) => line.length > 0); +} + +export function listGatewayServerTestTargets(cwd = process.cwd()) { + const gatewayDir = path.join(cwd, "src/gateway"); + if (!fs.existsSync(gatewayDir)) { + return []; + } + return (listGatewayFilesFromGit(cwd) ?? listRepoFilesRecursive(gatewayDir, cwd)) + .filter(isGatewayServerTestFile) + .toSorted((a, b) => a.localeCompare(b)); +} + +export function splitTestTargetChunks(targets, chunkCount) { + if (targets.length === 0) { + return []; + } + const normalizedChunkCount = Math.min(chunkCount, targets.length); + const baseSize = Math.floor(targets.length / normalizedChunkCount); + const remainder = targets.length % normalizedChunkCount; + const chunks = []; + let offset = 0; + for (let index = 0; index < normalizedChunkCount; index += 1) { + const chunkSize = baseSize + (index < remainder ? 1 : 0); + chunks.push(targets.slice(offset, offset + chunkSize)); + offset += chunkSize; + } + return chunks; +} + +export function createGatewayServerTestTargetChunks(cwd = process.cwd()) { + return splitTestTargetChunks( + listGatewayServerTestTargets(cwd), + GATEWAY_SERVER_TEST_PROCESS_COUNT, + ); +} diff --git a/scripts/run-vitest.d.mts b/scripts/run-vitest.d.mts index c4a36ba53a78..0b22b1bfe2e7 100644 --- a/scripts/run-vitest.d.mts +++ b/scripts/run-vitest.d.mts @@ -41,6 +41,14 @@ export function resolveMissingExplicitTestFiles( fsImpl?: VitestFs, ): string[]; export function resolveImplicitVitestArgs(argv: string[], cwd?: string): string[]; +export function resolveBoundedVitestInvocations( + argv: string[], + options?: { + cwd?: string; + env?: NodeJS.ProcessEnv; + gatewayServerTargetChunks?: string[][]; + }, +): string[][]; export function installVitestNoOutputWatchdog(params: { streams?: Array<{ on(event: string, listener: (...args: unknown[]) => void): unknown } | null>; timeoutMs: number | null; diff --git a/scripts/run-vitest.mjs b/scripts/run-vitest.mjs index 4f8b6d1abd0c..6026fbf412d7 100644 --- a/scripts/run-vitest.mjs +++ b/scripts/run-vitest.mjs @@ -9,6 +9,7 @@ import { toolingIsolatedTestFiles } from "../test/vitest/vitest.tooling-isolated import { isUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs"; import { boundaryTestFiles } from "../test/vitest/vitest.unit-paths.mjs"; import { runWithFailedTrailer, writeFailedTrailer } from "./lib/failed-trailer.mjs"; +import { createGatewayServerTestTargetChunks } from "./lib/gateway-server-test-plan.mjs"; import { signalExitCode } from "./lib/managed-child-process.mjs"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; import { resolveLocalVitestEnv } from "./lib/vitest-local-scheduling.mjs"; @@ -126,6 +127,16 @@ const VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES = [ "--sequence.", "--typecheck.", ]; +const UNBOUNDED_CONFIG_ONLY_OPTIONS = [ + "--changed", + "--coverage", + "--dir", + "--mergeReports", + "--outputFile", + "--project", + "--root", + "--shard", +]; const require = createRequire(import.meta.url); const repoRoot = resolveRepoRoot(import.meta.url); const testProjectsRunnerPath = path.join(repoRoot, "scripts", "test-projects.mjs"); @@ -427,15 +438,66 @@ function resolveVitestConfigArg(argv) { } function resolveVitestConfigNoOutputTimeoutMs(config) { - const normalized = path.normalize(config).replaceAll(path.sep, "/").replace(/^\.\//u, ""); + const normalized = normalizeVitestConfigPath(config); for (const [candidate, timeoutMs] of VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS) { - if (normalized === candidate || normalized.endsWith(`/${candidate}`)) { + if (matchesVitestConfigPath(normalized, candidate)) { return timeoutMs; } } return null; } +function normalizeVitestConfigPath(config) { + return path.normalize(config).replaceAll(path.sep, "/").replace(/^\.\//u, ""); +} + +function matchesVitestConfigPath(normalized, candidate) { + return normalized === candidate || normalized.endsWith("/" + candidate); +} + +function hasVitestOption(argv, option) { + for (const arg of argv) { + if (arg === "--") { + return false; + } + if (arg === option || arg.startsWith(option + "=") || arg.startsWith(option + ".")) { + return true; + } + } + return false; +} + +function insertVitestTargets(argv, targets) { + const separatorIndex = argv.indexOf("--"); + const insertionIndex = separatorIndex < 0 ? argv.length : separatorIndex; + return [...argv.slice(0, insertionIndex), ...targets, ...argv.slice(insertionIndex)]; +} + +/** + * Splits config-only Gateway server runs into fresh processes before the + * non-isolated module graph reaches the worker heap limit. + */ +export function resolveBoundedVitestInvocations(argv, options = {}) { + const config = resolveVitestConfigArg(argv); + const normalizedConfig = config === null ? "" : normalizeVitestConfigPath(config); + const env = options.env ?? process.env; + const cwd = options.cwd ?? process.cwd(); + const mode = resolveExplicitVitestMode(argv); + if ( + !matchesVitestConfigPath(normalizedConfig, GATEWAY_SERVER_VITEST_CONFIG) || + mode === "watch" || + (mode !== "run" && !isTruthyEnvValue(env.CI)) || + hasNonRunVitestSubcommand(argv) || + hasAlternateVitestRootArg(argv) || + collectExplicitProjectRouterTargetArgs(argv, cwd).length > 0 || + UNBOUNDED_CONFIG_ONLY_OPTIONS.some((option) => hasVitestOption(argv, option)) + ) { + return [argv]; + } + const chunks = options.gatewayServerTargetChunks ?? createGatewayServerTestTargetChunks(cwd); + return chunks.length > 1 ? chunks.map((targets) => insertVitestTargets(argv, targets)) : [argv]; +} + /** * Builds spawn options for the primary Vitest child process. */ @@ -1115,9 +1177,11 @@ async function finishVitestProcess({ completion, getForwardedSignal }) { if (exitSignal) { writeFailedTrailer("vitest", signalExitCode(exitSignal)); process.kill(process.pid, exitSignal); - return; + return signalExitCode(exitSignal); } - process.exitCode = code ?? 1; + const exitCode = code ?? 1; + process.exitCode = exitCode; + return exitCode; } async function main(argv = process.argv.slice(2), env = process.env) { @@ -1146,8 +1210,7 @@ async function main(argv = process.argv.slice(2), env = process.env) { } const vitestArgs = resolveImplicitVitestArgs(argv); - const guardedVitestArgs = resolveExplicitTestFileNoPassArgs(vitestArgs); - const spawnEnv = resolveRunVitestSpawnEnv(env, guardedVitestArgs); + const invocations = resolveBoundedVitestInvocations(vitestArgs, { env }); let vitestCliEntry; try { vitestCliEntry = resolveVitestCliEntry(); @@ -1160,20 +1223,30 @@ async function main(argv = process.argv.slice(2), env = process.env) { throw error; } - await finishVitestProcess( - spawnWatchedVitestProcess({ - pnpmArgs: [ - "exec", - "node", - ...resolveVitestNodeArgs(env), - vitestCliEntry, - ...guardedVitestArgs, - ], - spawnParams: resolveVitestSpawnParams(spawnEnv), - env: spawnEnv, - label: guardedVitestArgs.join(" "), - }), - ); + for (let index = 0; index < invocations.length; index += 1) { + const guardedVitestArgs = resolveExplicitTestFileNoPassArgs(invocations[index]); + const spawnEnv = resolveRunVitestSpawnEnv(env, guardedVitestArgs); + if (invocations.length > 1) { + console.error("[vitest] Gateway server process " + (index + 1) + "/" + invocations.length); + } + const exitCode = await finishVitestProcess( + spawnWatchedVitestProcess({ + pnpmArgs: [ + "exec", + "node", + ...resolveVitestNodeArgs(env), + vitestCliEntry, + ...guardedVitestArgs, + ], + spawnParams: resolveVitestSpawnParams(spawnEnv), + env: spawnEnv, + label: guardedVitestArgs.join(" "), + }), + ); + if (exitCode !== 0) { + return; + } + } } if (import.meta.main) { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 6acb31cbb6dd..4deaf1fc56e6 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -86,6 +86,11 @@ import { } from "./changed-lanes.mjs"; import { getChangedPathFacts } from "./lib/changed-path-facts.mjs"; import { createExtensionTestProcessTargetChunks } from "./lib/extension-test-plan.mjs"; +import { + GATEWAY_SERVER_TEST_PROCESS_COUNT, + listGatewayServerTestTargets, + splitTestTargetChunks as splitTargetChunks, +} from "./lib/gateway-server-test-plan.mjs"; import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs"; import { DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS, @@ -723,19 +728,6 @@ export function formatNoChangedTestTargetLines(skippedBroadFallbackPaths) { } const EXPLICIT_SOURCE_FULL_IMPORT_GRAPH_THRESHOLD = 12; -const GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT = 4; -const GATEWAY_SERVER_BACKED_HTTP_TEST_TARGETS = new Set([ - "src/gateway/embeddings-http.test.ts", - "src/gateway/models-http.test.ts", - "src/gateway/openai-http.test.ts", - "src/gateway/openresponses-http.test.ts", - "src/gateway/probe.auth.integration.test.ts", -]); -const GATEWAY_SERVER_EXCLUDED_TEST_TARGETS = new Set([ - "src/gateway/gateway.test.ts", - "src/gateway/server.startup-matrix-migration.integration.test.ts", - "src/gateway/sessions-history-http.test.ts", -]); function resolveTestProjectsVitestNoOutputTimeoutMs(config) { const directRunnerTimeoutMs = resolveDefaultVitestNoOutputTimeoutMs(["run", "--config", config]); return String( @@ -776,63 +768,6 @@ function listRepoFilesRecursive(root, cwd) { }); } -function listGatewayFilesFromGit(cwd) { - const result = spawnSync("git", ["ls-files", "--", "src/gateway"], { - cwd, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - if (result.status !== 0) { - return null; - } - return result.stdout - .split("\n") - .map((line) => normalizePathPattern(line.trim())) - .filter((line) => line.length > 0); -} - -function isGatewayServerFullSuiteTarget(relative) { - if ( - GATEWAY_SERVER_EXCLUDED_TEST_TARGETS.has(relative) || - relative.startsWith("src/gateway/server-methods/") - ) { - return false; - } - return ( - GATEWAY_SERVER_BACKED_HTTP_TEST_TARGETS.has(relative) || - (relative.startsWith("src/gateway/") && - path.posix.basename(relative).includes("server") && - relative.endsWith(".test.ts")) - ); -} - -function resolveGatewayServerFullSuiteTargets(cwd) { - const gatewayDir = path.join(cwd, "src/gateway"); - if (!fs.existsSync(gatewayDir)) { - return []; - } - return (listGatewayFilesFromGit(cwd) ?? listRepoFilesRecursive(gatewayDir, cwd)) - .filter(isGatewayServerFullSuiteTarget) - .toSorted((a, b) => a.localeCompare(b)); -} - -function splitTargetChunks(targets, chunkCount) { - if (targets.length === 0) { - return []; - } - const normalizedChunkCount = Math.min(chunkCount, targets.length); - const baseSize = Math.floor(targets.length / normalizedChunkCount); - const remainder = targets.length % normalizedChunkCount; - const chunks = []; - let offset = 0; - for (let index = 0; index < normalizedChunkCount; index += 1) { - const chunkSize = baseSize + (index < remainder ? 1 : 0); - chunks.push(targets.slice(offset, offset + chunkSize)); - offset += chunkSize; - } - return chunks; -} - let cachedBroadScriptTestTargets = null; let cachedBroadScriptTestTargetsCwd = null; @@ -3684,8 +3619,8 @@ export function buildFullSuiteVitestRunPlans(args, cwd = process.cwd()) { chunks = splitTargetChunks(targets, chunkCount); } else if (config === GATEWAY_SERVER_VITEST_CONFIG) { chunks = splitTargetChunks( - resolveGatewayServerFullSuiteTargets(cwd), - GATEWAY_SERVER_FULL_SUITE_TARGET_CHUNK_COUNT, + listGatewayServerTestTargets(cwd), + GATEWAY_SERVER_TEST_PROCESS_COUNT, ); } else if (config === EXTENSION_MATRIX_VITEST_CONFIG) { chunks = createExtensionTestProcessTargetChunks( diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 38a9f3be4693..09dcb06c707c 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -16,6 +16,8 @@ import { embeddedAgentVitestProjectOwners, } from "../vitest/vitest.agents-paths.mjs"; import { commandsLightTestFiles } from "../vitest/vitest.commands-light-paths.mjs"; +import { isGatewayServerTestFile } from "../vitest/vitest.gateway-server-paths.mjs"; +import { createGatewayServerVitestConfig } from "../vitest/vitest.gateway-server.config.ts"; import { createPluginsVitestConfig } from "../vitest/vitest.plugins.config.ts"; import { createToolingVitestConfig } from "../vitest/vitest.tooling.config.ts"; @@ -33,20 +35,6 @@ const PLUGIN_PRERELEASE_NPM_SPEC_TEST = "src/plugins/install.npm-spec.test.ts"; const PLUGIN_NPM_INSTALL_SECURITY_SCAN_TEST = "src/plugins/npm-install-security-scan.release.test.ts"; const DEFAULT_NODE_TEST_RUNNER = "blacksmith-8vcpu-ubuntu-2404"; -const GATEWAY_SERVER_BACKED_HTTP_TESTS = new Set([ - "src/gateway/embeddings-http.test.ts", - "src/gateway/models-http.test.ts", - "src/gateway/openai-http.test.ts", - "src/gateway/openresponses-http.test.ts", - "src/gateway/probe.auth.integration.test.ts", -]); - -const GATEWAY_SERVER_EXCLUDED_TESTS = new Set([ - "src/gateway/gateway.test.ts", - "src/gateway/server.startup-matrix-migration.integration.test.ts", - "src/gateway/sessions-history-http.test.ts", -]); - function listTestFiles(rootDir: string): string[] { const gitFiles = listGitTrackedFiles({ pathspecs: rootDir }); expect(gitFiles).not.toBeNull(); @@ -103,15 +91,6 @@ function listAllToolingTestFiles(): string[] { } } -function isGatewayServerTestFile(file: string): boolean { - return ( - file.startsWith("src/gateway/") && - !file.startsWith("src/gateway/server-methods/") && - !GATEWAY_SERVER_EXCLUDED_TESTS.has(file) && - (file.includes("server") || GATEWAY_SERVER_BACKED_HTTP_TESTS.has(file)) - ); -} - describe("scripts/lib/ci-node-test-plan.mjs", () => { it("assigns one semantic Vitest cache writer without changing shard order", () => { const full = createNodeTestShardBundles({ includeReleaseOnlyPluginShards: false }); @@ -850,7 +829,6 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { "agentic-control-plane-runtime", "agentic-control-plane-runtime-config", "agentic-control-plane-runtime-cron", - "agentic-control-plane-runtime-events", "agentic-control-plane-runtime-network", "agentic-control-plane-runtime-server", "agentic-control-plane-runtime-shared-token", @@ -880,9 +858,17 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { const controlPlaneShardFiles = controlPlaneShards .flatMap((shard) => shard.includePatterns ?? []) .toSorted((a, b) => a.localeCompare(b)); - const expectedControlPlaneFiles = listTestFiles("src/gateway") - .filter(isGatewayServerTestFile) - .toSorted((a, b) => a.localeCompare(b)); + const expectedControlPlaneFiles = listMatchedTestFiles( + createGatewayServerVitestConfig({ + ...process.env, + OPENCLAW_VITEST_INCLUDE_FILE: undefined, + }), + ); + expect( + listTestFiles("src/gateway") + .filter(isGatewayServerTestFile) + .toSorted((a, b) => a.localeCompare(b)), + ).toEqual(expectedControlPlaneFiles); expect(controlPlaneShardFiles).toEqual(expectedControlPlaneFiles); expect(new Set(controlPlaneShardFiles).size).toBe(controlPlaneShardFiles.length); expect(cliShard).toEqual({ diff --git a/test/scripts/run-vitest.test.ts b/test/scripts/run-vitest.test.ts index 9efb545e3856..5072a1cc6e72 100644 --- a/test/scripts/run-vitest.test.ts +++ b/test/scripts/run-vitest.test.ts @@ -13,6 +13,7 @@ import { TOOLING_EXCLUDED_TESTS, VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS, installVitestNoOutputWatchdog, + resolveBoundedVitestInvocations, resolveDefaultVitestNoOutputTimeoutMs, resolveDirectNodeVitestArgs, resolveExplicitTestFileNoPassArgs, @@ -194,6 +195,90 @@ describe("scripts/run-vitest", () => { ).toEqual(["extensions/linux-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("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.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("routes explicit tooling tests through the tooling config", () => { expect(resolveImplicitVitestArgs(["run", "test/scripts/run-vitest.test.ts"])).toEqual([ "run", diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index edd665b7672b..c1cff13ab650 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -4645,7 +4645,7 @@ describe("scripts/test-projects full-suite sharding", () => { expect(gatewayPlans).toHaveLength(4); expect(gatewayTargets.length).toBeGreaterThan(90); expect(new Set(gatewayTargets).size).toBe(gatewayTargets.length); - expect(gatewayTargets).toContain("src/gateway/server-network-runtime.e2e.test.ts"); + expect(gatewayTargets).not.toContain("src/gateway/server-network-runtime.e2e.test.ts"); expect(gatewayTargets).not.toContain("src/gateway/gateway.test.ts"); expect(Math.max(...gatewayChunkSizes) - Math.min(...gatewayChunkSizes)).toBeLessThanOrEqual(1); const agentsCoreTargets = agentsCorePlans.flatMap((plan) => plan.forwardedArgs); diff --git a/test/vitest/vitest.gateway-server-paths.d.mts b/test/vitest/vitest.gateway-server-paths.d.mts new file mode 100644 index 000000000000..739232530d98 --- /dev/null +++ b/test/vitest/vitest.gateway-server-paths.d.mts @@ -0,0 +1,4 @@ +export const gatewayServerBackedHttpTestFiles: string[]; +export const gatewayServerExcludedTestFiles: string[]; +export function isGatewayServerBackedHttpTestFile(file: string): boolean; +export function isGatewayServerTestFile(file: string): boolean; diff --git a/test/vitest/vitest.gateway-server-paths.mjs b/test/vitest/vitest.gateway-server-paths.mjs new file mode 100644 index 000000000000..f0cc7b46627f --- /dev/null +++ b/test/vitest/vitest.gateway-server-paths.mjs @@ -0,0 +1,39 @@ +// Canonical file ownership for the non-isolated Gateway server Vitest project. +export const gatewayServerBackedHttpTestFiles = [ + "src/gateway/embeddings-http.test.ts", + "src/gateway/models-http.test.ts", + "src/gateway/openai-http.test.ts", + "src/gateway/openresponses-http.test.ts", + "src/gateway/probe.auth.integration.test.ts", +]; + +export const gatewayServerExcludedTestFiles = [ + "src/gateway/gateway.test.ts", + "src/gateway/server.startup-matrix-migration.integration.test.ts", + "src/gateway/sessions-history-http.test.ts", +]; + +const gatewayServerBackedHttpTestFileSet = new Set(gatewayServerBackedHttpTestFiles); +const gatewayServerExcludedTestFileSet = new Set(gatewayServerExcludedTestFiles); + +export function isGatewayServerBackedHttpTestFile(file) { + return gatewayServerBackedHttpTestFileSet.has(file.replaceAll("\\", "/")); +} + +export function isGatewayServerTestFile(file) { + const normalized = file.replaceAll("\\", "/"); + if ( + gatewayServerExcludedTestFileSet.has(normalized) || + normalized.startsWith("src/gateway/server-methods/") || + normalized.endsWith(".e2e.test.ts") + ) { + return false; + } + const basename = normalized.slice(normalized.lastIndexOf("/") + 1); + return ( + isGatewayServerBackedHttpTestFile(normalized) || + (normalized.startsWith("src/gateway/") && + basename.includes("server") && + normalized.endsWith(".test.ts")) + ); +} diff --git a/test/vitest/vitest.gateway-server.config.ts b/test/vitest/vitest.gateway-server.config.ts index bb4cbaec4947..cfa3f6a2ec9a 100644 --- a/test/vitest/vitest.gateway-server.config.ts +++ b/test/vitest/vitest.gateway-server.config.ts @@ -1,26 +1,17 @@ +import { + gatewayServerBackedHttpTestFiles, + gatewayServerExcludedTestFiles, +} from "./vitest.gateway-server-paths.mjs"; // Vitest gateway server config wires the gateway server test shard. import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; -const gatewayServerBackedHttpTests = [ - "src/gateway/embeddings-http.test.ts", - "src/gateway/models-http.test.ts", - "src/gateway/openai-http.test.ts", - "src/gateway/openresponses-http.test.ts", - "src/gateway/probe.auth.integration.test.ts", -]; - export function createGatewayServerVitestConfig(env?: Record) { return createScopedVitestConfig( - ["src/gateway/**/*server*.test.ts", ...gatewayServerBackedHttpTests], + ["src/gateway/**/*server*.test.ts", ...gatewayServerBackedHttpTestFiles], { dir: "src/gateway", env, - exclude: [ - "src/gateway/server-methods/**/*.test.ts", - "src/gateway/gateway.test.ts", - "src/gateway/server.startup-matrix-migration.integration.test.ts", - "src/gateway/sessions-history-http.test.ts", - ], + exclude: ["src/gateway/server-methods/**/*.test.ts", ...gatewayServerExcludedTestFiles], fileParallelism: false, // Gateway child projects share one include file; preserve this project's ownership. intersectIncludeFile: true,