From 887c44ef8ff2b8c6b8e5bb6e9ff3c5e1af36f513 Mon Sep 17 00:00:00 2001 From: Jesse Merhi Date: Thu, 13 Aug 2026 03:18:41 +1000 Subject: [PATCH] fix(tooling): bound tsgo core test memory --- package.json | 6 +- scripts/check-tsgo-core-boundary.mts | 115 +++++++++++++++++- scripts/lib/tsgo-core-test-shards.mts | 65 ++++++++++ scripts/lib/tsgo-sparse-guard.mts | 63 +++++++--- scripts/profile-tsgo.mts | 63 ++++++---- test/scripts/run-tsgo.test.ts | 46 ++++++- test/scripts/tsgo-core-test-shards.test.ts | 36 ++++++ .../tsconfig.core.test.agents-other.json | 17 +++ .../tsconfig.core.test.agents-root.json | 10 ++ test/tsconfig/tsconfig.core.test.agents.json | 10 -- .../tsconfig/tsconfig.core.test.commands.json | 10 ++ .../tsconfig.core.test.config-cli.json | 12 ++ test/tsconfig/tsconfig.core.test.gateway.json | 10 ++ test/tsconfig/tsconfig.core.test.infra.json | 10 ++ .../tsconfig.core.test.messaging.json | 12 ++ .../tsconfig.core.test.non-agents.json | 10 -- test/tsconfig/tsconfig.core.test.other.json | 37 ++++++ .../tsconfig.core.test.plugins-platform.json | 26 ++++ .../tsconfig/tsconfig.core.test.projects.json | 8 ++ .../tsconfig/tsconfig.core.test.services.json | 18 +++ test/tsconfig/tsconfig.core.test.shard.json | 19 +++ .../tsconfig.core.test.src.projects.json | 15 +++ .../tsconfig/tsconfig.core.test.ui-other.json | 18 +++ .../tsconfig.core.test.ui-pages-e2e.json | 13 ++ .../tsconfig.core.test.ui.projects.json | 7 ++ test/tsconfig/tsconfig.test.packages.json | 9 +- test/tsconfig/tsconfig.test.src.json | 16 --- test/tsconfig/tsconfig.test.ui.json | 11 -- tsconfig.core.projects.json | 2 +- 29 files changed, 590 insertions(+), 104 deletions(-) create mode 100644 scripts/lib/tsgo-core-test-shards.mts create mode 100644 test/scripts/tsgo-core-test-shards.test.ts create mode 100644 test/tsconfig/tsconfig.core.test.agents-other.json create mode 100644 test/tsconfig/tsconfig.core.test.agents-root.json delete mode 100644 test/tsconfig/tsconfig.core.test.agents.json create mode 100644 test/tsconfig/tsconfig.core.test.commands.json create mode 100644 test/tsconfig/tsconfig.core.test.config-cli.json create mode 100644 test/tsconfig/tsconfig.core.test.gateway.json create mode 100644 test/tsconfig/tsconfig.core.test.infra.json create mode 100644 test/tsconfig/tsconfig.core.test.messaging.json delete mode 100644 test/tsconfig/tsconfig.core.test.non-agents.json create mode 100644 test/tsconfig/tsconfig.core.test.other.json create mode 100644 test/tsconfig/tsconfig.core.test.plugins-platform.json create mode 100644 test/tsconfig/tsconfig.core.test.projects.json create mode 100644 test/tsconfig/tsconfig.core.test.services.json create mode 100644 test/tsconfig/tsconfig.core.test.shard.json create mode 100644 test/tsconfig/tsconfig.core.test.src.projects.json create mode 100644 test/tsconfig/tsconfig.core.test.ui-other.json create mode 100644 test/tsconfig/tsconfig.core.test.ui-pages-e2e.json create mode 100644 test/tsconfig/tsconfig.core.test.ui.projects.json delete mode 100644 test/tsconfig/tsconfig.test.src.json delete mode 100644 test/tsconfig/tsconfig.test.ui.json diff --git a/package.json b/package.json index f9e71cf2aadb..a72723a43fb1 100644 --- a/package.json +++ b/package.json @@ -1942,7 +1942,7 @@ "tsgo:all": "node scripts/run-tsgo.mjs -b tsconfig.projects.json", "tsgo:core": "node scripts/run-tsgo.mjs -p tsconfig.core.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core.tsbuildinfo", "tsgo:core:all": "node scripts/run-tsgo.mjs -b tsconfig.core.projects.json", - "tsgo:core:test": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo", + "tsgo:core:test": "node scripts/run-tsgo.mjs -b test/tsconfig/tsconfig.core.test.projects.json", "tsgo:extensions": "node scripts/run-tsgo.mjs -p tsconfig.extensions.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions.tsbuildinfo", "tsgo:extensions:all": "node scripts/run-tsgo.mjs -b tsconfig.extensions.projects.json", "tsgo:extensions:test": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo", @@ -1953,8 +1953,8 @@ "tsgo:test:extensions": "pnpm tsgo:extensions:test", "tsgo:test:packages": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.packages.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-packages.tsbuildinfo", "tsgo:test:root": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.root.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-root.tsbuildinfo", - "tsgo:test:src": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.src.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-src.tsbuildinfo", - "tsgo:test:ui": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-ui.tsbuildinfo", + "tsgo:test:src": "node scripts/run-tsgo.mjs -b test/tsconfig/tsconfig.core.test.src.projects.json", + "tsgo:test:ui": "node scripts/run-tsgo.mjs -b test/tsconfig/tsconfig.core.test.ui.projects.json", "tsgo:ui": "node scripts/run-tsgo.mjs -p tsconfig.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/ui.tsbuildinfo", "tui": "node scripts/run-node.mjs tui", "tui:dev": "node --import tsx scripts/run-with-env.mts OPENCLAW_PROFILE=dev -- node scripts/run-node.mjs --dev tui", diff --git a/scripts/check-tsgo-core-boundary.mts b/scripts/check-tsgo-core-boundary.mts index 40f2699edc5b..38de95592422 100644 --- a/scripts/check-tsgo-core-boundary.mts +++ b/scripts/check-tsgo-core-boundary.mts @@ -2,18 +2,28 @@ // Enforces core tsgo project boundaries and sparse-checkout safety. import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts"; import { createManagedCommandInvocation } from "./lib/managed-child-process.mts"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; +import { + findTsgoCoreTestShardViolations, + TSGO_CORE_TEST_MAX_ROOTS, + TSGO_CORE_TEST_SHARDS, +} from "./lib/tsgo-core-test-shards.mts"; const repoRoot = resolveRepoRoot(import.meta.url); const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot }); +const canonicalCoreTestConfig = "test/tsconfig/tsconfig.core.test.json"; +const coreTestProjectsConfig = "test/tsconfig/tsconfig.core.test.projects.json"; const coreGraphs = [ { name: "core", config: "tsconfig.core.json" }, { name: "ui", config: "tsconfig.ui.json" }, - { name: "core-test", config: "test/tsconfig/tsconfig.core.test.json" }, - { name: "core-test-agents", config: "test/tsconfig/tsconfig.core.test.agents.json" }, - { name: "core-test-non-agents", config: "test/tsconfig/tsconfig.core.test.non-agents.json" }, + ...TSGO_CORE_TEST_SHARDS.map((shard) => ({ + name: `core-test-${shard.name}`, + config: shard.config, + })), ]; function normalizeFilePath(filePath: string) { const normalized = filePath.trim().replaceAll("\\", "/"); @@ -46,6 +56,105 @@ function listGraphFiles(graph: (typeof coreGraphs)[number]) { return (result.stdout ?? "").split(/\r?\n/u).map(normalizeFilePath).filter(Boolean); } +function readGraphConfig(config: string): { + compilerOptions?: { tsBuildInfoFile?: string }; + files?: string[]; +} { + const tsgo = createManagedCommandInvocation({ + args: ["-p", config, "--pretty", "false", "--showConfig"], + bin: tsgoPath, + }); + const result = spawnSync(tsgo.command, tsgo.args, { + cwd: repoRoot, + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + shell: tsgo.shell, + windowsVerbatimArguments: tsgo.windowsVerbatimArguments, + }); + if (result.error) { + throw result.error; + } + if ((result.status ?? 1) !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); + throw new Error(`${config} config expansion failed with exit code ${result.status}\n${output}`); + } + return JSON.parse(result.stdout ?? "") as { + compilerOptions?: { tsBuildInfoFile?: string }; + files?: string[]; + }; +} + +function listProjectLeaves(config: string, seen = new Set()): string[] { + const absoluteConfig = path.resolve(repoRoot, config); + if (seen.has(absoluteConfig)) { + throw new Error(`Project-reference cycle at ${normalizeFilePath(absoluteConfig)}`); + } + seen.add(absoluteConfig); + const parsed = JSON.parse(fs.readFileSync(absoluteConfig, "utf8")) as { + references?: { path: string }[]; + }; + if (!parsed.references || parsed.references.length === 0) { + return [normalizeFilePath(absoluteConfig)]; + } + return parsed.references.flatMap((reference) => { + const referencedConfig = path.resolve(path.dirname(absoluteConfig), reference.path); + return listProjectLeaves(path.relative(repoRoot, referencedConfig), seen); + }); +} + +const testRootPattern = /\.test\.(?:ts|tsx)$/u; +const canonicalRoots = (readGraphConfig(canonicalCoreTestConfig).files ?? []) + .map(normalizeFilePath) + .filter((file) => testRootPattern.test(file)); +const shardConfigs = TSGO_CORE_TEST_SHARDS.map((shard) => ({ + ...shard, + expanded: readGraphConfig(shard.config), +})); +const shardViolations = findTsgoCoreTestShardViolations({ + canonicalRoots, + shards: shardConfigs.map((shard) => ({ + name: shard.name, + roots: (shard.expanded.files ?? []) + .map(normalizeFilePath) + .filter((file) => testRootPattern.test(file)), + })), +}); + +const expectedLeaves = TSGO_CORE_TEST_SHARDS.map((shard) => shard.config).toSorted(); +const actualLeaves = listProjectLeaves(coreTestProjectsConfig).toSorted(); +if (JSON.stringify(actualLeaves) !== JSON.stringify(expectedLeaves)) { + shardViolations.push( + `${coreTestProjectsConfig} must reference exactly these leaves: ${expectedLeaves.join(", ")}`, + ); +} + +const buildInfoOwners = new Map(); +for (const shard of shardConfigs) { + const buildInfo = shard.expanded.compilerOptions?.tsBuildInfoFile; + if (!buildInfo) { + shardViolations.push(`${shard.name}: missing compilerOptions.tsBuildInfoFile`); + continue; + } + const owners = buildInfoOwners.get(buildInfo) ?? []; + owners.push(shard.name); + buildInfoOwners.set(buildInfo, owners); +} +for (const [buildInfo, owners] of buildInfoOwners) { + if (owners.length > 1) { + shardViolations.push(`shared tsBuildInfoFile (${owners.join(", ")}): ${buildInfo}`); + } +} + +if (shardViolations.length > 0) { + console.error( + `Core test shards must cover every canonical test root exactly once and stay at or below ${TSGO_CORE_TEST_MAX_ROOTS} roots:`, + ); + for (const violation of shardViolations) { + console.error(`- ${violation}`); + } + process.exit(1); +} + const violations: string[] = []; for (const graph of coreGraphs) { const extensionFiles = listGraphFiles(graph).filter((file) => file.startsWith("extensions/")); diff --git a/scripts/lib/tsgo-core-test-shards.mts b/scripts/lib/tsgo-core-test-shards.mts new file mode 100644 index 000000000000..f554e814013e --- /dev/null +++ b/scripts/lib/tsgo-core-test-shards.mts @@ -0,0 +1,65 @@ +export const TSGO_CORE_TEST_MAX_ROOTS = 720; + +export const TSGO_CORE_TEST_SHARDS = [ + { name: "agents-root", config: "test/tsconfig/tsconfig.core.test.agents-root.json" }, + { name: "agents-other", config: "test/tsconfig/tsconfig.core.test.agents-other.json" }, + { name: "gateway", config: "test/tsconfig/tsconfig.core.test.gateway.json" }, + { name: "infra", config: "test/tsconfig/tsconfig.core.test.infra.json" }, + { name: "commands", config: "test/tsconfig/tsconfig.core.test.commands.json" }, + { + name: "plugins-platform", + config: "test/tsconfig/tsconfig.core.test.plugins-platform.json", + }, + { name: "config-cli", config: "test/tsconfig/tsconfig.core.test.config-cli.json" }, + { name: "messaging", config: "test/tsconfig/tsconfig.core.test.messaging.json" }, + { name: "services", config: "test/tsconfig/tsconfig.core.test.services.json" }, + { name: "other", config: "test/tsconfig/tsconfig.core.test.other.json" }, + { + name: "ui-pages-e2e", + config: "test/tsconfig/tsconfig.core.test.ui-pages-e2e.json", + }, + { name: "ui-other", config: "test/tsconfig/tsconfig.core.test.ui-other.json" }, + { name: "packages", config: "test/tsconfig/tsconfig.test.packages.json" }, +] as const; + +export type TsgoCoreTestShard = (typeof TSGO_CORE_TEST_SHARDS)[number]; + +export function findTsgoCoreTestShardViolations(params: { + canonicalRoots: readonly string[]; + maxRoots?: number; + shards: readonly { name: string; roots: readonly string[] }[]; +}): string[] { + const maxRoots = params.maxRoots ?? TSGO_CORE_TEST_MAX_ROOTS; + const canonical = new Set(params.canonicalRoots); + const owners = new Map(); + const violations: string[] = []; + + for (const shard of params.shards) { + if (shard.roots.length > maxRoots) { + violations.push( + `${shard.name}: ${shard.roots.length} test roots exceeds the ${maxRoots} limit`, + ); + } + for (const root of shard.roots) { + const rootOwners = owners.get(root) ?? []; + rootOwners.push(shard.name); + owners.set(root, rootOwners); + } + } + + for (const root of canonical) { + const rootOwners = owners.get(root) ?? []; + if (rootOwners.length === 0) { + violations.push(`unassigned: ${root}`); + } else if (rootOwners.length > 1) { + violations.push(`assigned ${rootOwners.length} times (${rootOwners.join(", ")}): ${root}`); + } + } + for (const [root, rootOwners] of owners) { + if (!canonical.has(root)) { + violations.push(`not in the canonical core-test graph (${rootOwners.join(", ")}): ${root}`); + } + } + + return violations; +} diff --git a/scripts/lib/tsgo-sparse-guard.mts b/scripts/lib/tsgo-sparse-guard.mts index 0edb6e0368e3..fba484a03e47 100644 --- a/scripts/lib/tsgo-sparse-guard.mts +++ b/scripts/lib/tsgo-sparse-guard.mts @@ -4,19 +4,32 @@ import fs from "node:fs"; import path from "node:path"; import { readFlagValue } from "./arg-utils.mts"; import { createManagedCommandInvocation } from "./managed-child-process.mts"; +import { TSGO_CORE_TEST_SHARDS } from "./tsgo-core-test-shards.mts"; +const PACKAGE_TEST_CONFIGS = new Set(["tsconfig.test.packages.json"]); const CORE_TEST_CONFIGS = new Set([ "tsconfig.core.test.json", - "tsconfig.core.test.agents.json", - "tsconfig.core.test.non-agents.json", + "tsconfig.core.test.projects.json", + "tsconfig.core.test.src.projects.json", + "tsconfig.core.test.ui.projects.json", + ...TSGO_CORE_TEST_SHARDS.map((shard) => path.basename(shard.config)).filter( + (config) => !PACKAGE_TEST_CONFIGS.has(config), + ), ]); const CORE_PROD_CONFIGS = new Set(["tsconfig.core.json"]); const UI_PROD_CONFIGS = new Set(["tsconfig.ui.json"]); +const GUARDED_CONFIGS = new Set([ + ...CORE_PROD_CONFIGS, + ...UI_PROD_CONFIGS, + ...CORE_TEST_CONFIGS, + ...PACKAGE_TEST_CONFIGS, +]); const TSGO_SPARSE_SKIP_ENV_KEY = "OPENCLAW_TSGO_SPARSE_SKIP"; const CORE_PROD_SPARSE_ROOTS = ["packages"]; const UI_PROD_SPARSE_ROOTS = ["packages", "src", "ui/config", "ui/src"]; const CORE_TEST_SPARSE_ROOTS = ["packages", "ui/config", "ui/src"]; +const PACKAGE_TEST_SPARSE_ROOTS = ["packages"]; const CORE_PROD_REQUIRED_PATHS = [ { @@ -100,15 +113,8 @@ export function getSparseTsgoGuardError( sparseCheckoutPatterns, }: SparseGuardOptions = {}, ) { - const projectPath = readProjectFlag(args); - const projectName = projectPath ? path.basename(projectPath) : null; - if ( - !projectName || - (!CORE_PROD_CONFIGS.has(projectName) && - !UI_PROD_CONFIGS.has(projectName) && - !CORE_TEST_CONFIGS.has(projectName)) || - isMetadataOnlyCommand(args) - ) { + const projectNames = readProjectNames(args); + if (projectNames.length === 0 || isMetadataOnlyCommand(args)) { return null; } @@ -120,11 +126,17 @@ export function getSparseTsgoGuardError( const sparsePatterns = sparseCheckoutPatterns ?? getSparseCheckoutPatterns({ cwd }); const missingPaths = [ - ...getRequiredSparseRootsForProject(projectName).filter((relativePath) => - sparsePatterns ? !isSparseRootCovered(relativePath, sparsePatterns) : false, + ...new Set( + projectNames + .flatMap(getRequiredSparseRootsForProject) + .filter((relativePath) => + sparsePatterns ? !isSparseRootCovered(relativePath, sparsePatterns) : false, + ), ), - ...getRequiredPathsForProject(projectName, cwd, fileExists).filter( - (relativePath) => !fileExists(path.join(cwd, relativePath)), + ...new Set( + projectNames + .flatMap((projectName) => getRequiredPathsForProject(projectName, cwd, fileExists)) + .filter((relativePath) => !fileExists(path.join(cwd, relativePath))), ), ]; if (missingPaths.length === 0) { @@ -132,7 +144,7 @@ export function getSparseTsgoGuardError( } return [ - `${projectName} cannot be typechecked from this sparse checkout because tracked project inputs are missing or only partially included:`, + `${projectNames.join(", ")} cannot be typechecked from this sparse checkout because tracked project inputs are missing or only partially included:`, ...missingPaths.map((relativePath) => `- ${relativePath}`), "Expand this worktree's sparse checkout to include those paths, or rerun in a full worktree.", ].join("\n"); @@ -148,6 +160,9 @@ function getRequiredSparseRootsForProject(projectName: string) { if (CORE_TEST_CONFIGS.has(projectName)) { return CORE_TEST_SPARSE_ROOTS; } + if (PACKAGE_TEST_CONFIGS.has(projectName)) { + return PACKAGE_TEST_SPARSE_ROOTS; + } return []; } @@ -236,8 +251,20 @@ function normalizeSparsePattern(pattern: string) { .replace(/\/+$/, ""); } -function readProjectFlag(args: readonly string[]) { - return readFlagValue(args, "-p") ?? readFlagValue(args, "--project"); +function readProjectNames(args: readonly string[]) { + const projectPath = readFlagValue(args, "-p") ?? readFlagValue(args, "--project"); + const candidates = projectPath + ? [projectPath] + : args.some((arg) => arg === "-b" || arg === "--build") + ? args.filter((arg) => !arg.startsWith("-")) + : []; + return [ + ...new Set(candidates.map((candidate) => path.basename(candidate)).filter(isGuardedConfig)), + ]; +} + +function isGuardedConfig(config: string) { + return GUARDED_CONFIGS.has(config); } function isMetadataOnlyCommand(args: readonly string[]) { diff --git a/scripts/profile-tsgo.mts b/scripts/profile-tsgo.mts index ebe07bdd3c8c..1166243a111e 100644 --- a/scripts/profile-tsgo.mts +++ b/scripts/profile-tsgo.mts @@ -14,10 +14,23 @@ import { } from "./lib/local-heavy-check-runtime.mts"; import { createManagedCommandInvocation } from "./lib/managed-child-process.mts"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; +import { TSGO_CORE_TEST_SHARDS, type TsgoCoreTestShard } from "./lib/tsgo-core-test-shards.mts"; const repoRoot = resolveRepoRoot(import.meta.url); const artifactRoot = path.resolve(repoRoot, ".artifacts/tsgo-profile"); const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot }); +type GraphDefinition = { config: string; description: string }; +type CoreTestGraphName = `core-test-${TsgoCoreTestShard["name"]}`; +const CORE_TEST_GRAPH_DEFINITIONS = Object.fromEntries( + TSGO_CORE_TEST_SHARDS.map((shard) => [ + `core-test-${shard.name}`, + { + config: shard.config, + description: `bounded core test shard: ${shard.name}`, + }, + ]), +) as Record; + const GRAPH_DEFINITIONS = { core: { config: "tsconfig.core.json", @@ -27,18 +40,7 @@ const GRAPH_DEFINITIONS = { config: "tsconfig.ui.json", description: "UI production graph", }, - "core-test": { - config: "test/tsconfig/tsconfig.core.test.json", - description: "core colocated test graph", - }, - "core-test-agents": { - config: "test/tsconfig/tsconfig.core.test.agents.json", - description: "diagnostic slice: core agent colocated tests", - }, - "core-test-non-agents": { - config: "test/tsconfig/tsconfig.core.test.non-agents.json", - description: "diagnostic slice: core tests excluding agent test roots", - }, + ...CORE_TEST_GRAPH_DEFINITIONS, extensions: { config: "tsconfig.extensions.json", description: "bundled extension production graph", @@ -50,6 +52,10 @@ const GRAPH_DEFINITIONS = { } as const; type GraphName = keyof typeof GRAPH_DEFINITIONS; +const DEFAULT_GRAPHS = [ + ...TSGO_CORE_TEST_SHARDS.map((shard) => `core-test-${shard.name}` as CoreTestGraphName), + "extensions-test", +] satisfies GraphName[]; type ProfileOptions = { all: boolean; deep: boolean; @@ -73,19 +79,19 @@ function usage(): string { "", "Graphs:", ...Object.entries(GRAPH_DEFINITIONS).map( - ([name, graph]) => ` ${name.padEnd(16)} ${graph.description}`, + ([name, graph]) => ` ${name.padEnd(26)} ${graph.description}`, ), "", "Options:", " --all Profile all graphs", " --reuse Reuse profile tsbuildinfo files instead of forcing fresh checks", - " --deep Also write --generateTrace and --generateCpuProfile artifacts", - " --explain Also write --explainFiles artifacts", + " --deep Also write --generateTrace and --pprofDir artifacts", + " --explain Also write list-only --explainFiles artifacts", " --out= Output directory (default: .artifacts/tsgo-profile)", " --json Print JSON report to stdout", " --help Show this help", "", - "Default graphs: core-test extensions-test", + "Default graphs: all bounded core-test shards and extensions-test", ].join("\n"); } @@ -138,7 +144,7 @@ function parseArgs(argv: string[]): { options: ProfileOptions; selectedGraphs: G ? (Object.keys(GRAPH_DEFINITIONS) as GraphName[]) : graphNames.length > 0 ? graphNames - : (["core-test", "extensions-test"] satisfies GraphName[]); + : DEFAULT_GRAPHS; return { options, selectedGraphs }; } @@ -351,7 +357,7 @@ function renderTextReport(report: ProfileReport): string { lines.push(`- ${group.key}: ${group.count}`); } if (graph.deep) { - lines.push(`Deep artifacts: ${graph.deep.traceDir}, ${graph.deep.cpuProfile}`); + lines.push(`Deep artifacts: ${graph.deep.traceDir}, ${graph.deep.profileDir}`); } if (graph.explain) { lines.push(`Explain: ${graph.explain.artifact}`); @@ -397,25 +403,30 @@ function profileGraph(name: GraphName, options: ProfileOptions) { checkBuildInfo, "--extendedDiagnostics", ]; - let deep: { cpuProfile: string; traceDir: string } | undefined; + let deep: { profileDir: string; traceDir: string } | undefined; if (options.deep) { const traceDir = path.join(outDir, `${name}-trace`); - const cpuProfile = path.join(outDir, `${name}.cpuprofile`); + const profileDir = path.join(outDir, `${name}-pprof`); fs.rmSync(traceDir, { force: true, recursive: true }); - fs.rmSync(cpuProfile, { force: true }); - checkArgs.push("--generateTrace", traceDir, "--generateCpuProfile", cpuProfile); + fs.rmSync(profileDir, { force: true, recursive: true }); + fs.mkdirSync(profileDir, { recursive: true }); + checkArgs.push("--generateTrace", traceDir, "--pprofDir", profileDir); deep = { traceDir: path.relative(repoRoot, traceDir), - cpuProfile: path.relative(repoRoot, cpuProfile), + profileDir: path.relative(repoRoot, profileDir), }; } const check = runTsgo(`${name}:check`, checkArgs); let explain: { artifact: string; elapsedMs: number } | undefined; if (options.explain) { const explainArtifact = path.join(outDir, `${name}.explain.txt`); - const explainResult = runTsgo(`${name}:explainFiles`, [...baseArgs, "--explainFiles"], { - maxBuffer: 256 * 1024 * 1024, - }); + const explainResult = runTsgo( + `${name}:explainFiles`, + [...baseArgs, "--listFilesOnly", "--explainFiles"], + { + maxBuffer: 256 * 1024 * 1024, + }, + ); fs.writeFileSync(explainArtifact, `${explainResult.stdout}${explainResult.stderr}`); explain = { artifact: path.relative(repoRoot, explainArtifact), diff --git a/test/scripts/run-tsgo.test.ts b/test/scripts/run-tsgo.test.ts index 5db3212778a3..e14d8e5dc61d 100644 --- a/test/scripts/run-tsgo.test.ts +++ b/test/scripts/run-tsgo.test.ts @@ -84,7 +84,7 @@ describe("run-tsgo sparse guard", () => { } expect( - getSparseTsgoGuardError(["-p", "test/tsconfig/tsconfig.core.test.non-agents.json"], { + getSparseTsgoGuardError(["-p", "test/tsconfig/tsconfig.core.test.other.json"], { cwd, isSparseCheckoutEnabled: () => true, sparseCheckoutPatterns: ["/packages/", "/ui/config/", "/ui/src/"], @@ -92,6 +92,50 @@ describe("run-tsgo sparse guard", () => { ).toBeNull(); }); + it("guards the core-test build aggregate", () => { + const cwd = createTempDir("openclaw-run-tsgo-"); + + expect( + getSparseTsgoGuardError(["-b", "test/tsconfig/tsconfig.core.test.projects.json"], { + cwd, + isSparseCheckoutEnabled: () => true, + }), + ).toContain("tsconfig.core.test.projects.json cannot be typechecked from this sparse checkout"); + }); + + it("guards core-test build projects after options and alongside other projects", () => { + const cwd = createTempDir("openclaw-run-tsgo-"); + + expect( + getSparseTsgoGuardError( + [ + "-b", + "--pretty", + "false", + "tsconfig.extensions.json", + "test/tsconfig/tsconfig.core.test.projects.json", + ], + { + cwd, + isSparseCheckoutEnabled: () => true, + }, + ), + ).toContain("tsconfig.core.test.projects.json cannot be typechecked from this sparse checkout"); + }); + + it("keeps package-test sparse roots package-scoped", () => { + const cwd = createTempDir("openclaw-run-tsgo-"); + + expect( + getSparseTsgoGuardError(["-p", "test/tsconfig/tsconfig.test.packages.json"], { + cwd, + fileExists: () => true, + isSparseCheckoutEnabled: () => true, + sparseCheckoutPatterns: ["/packages/"], + }), + ).toBeNull(); + }); + it("rejects sparse core worktrees that include only selected ui and package files", () => { const cwd = createTempDir("openclaw-run-tsgo-"); const requiredPaths = [ diff --git a/test/scripts/tsgo-core-test-shards.test.ts b/test/scripts/tsgo-core-test-shards.test.ts new file mode 100644 index 000000000000..68abef32e3eb --- /dev/null +++ b/test/scripts/tsgo-core-test-shards.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { findTsgoCoreTestShardViolations } from "../../scripts/lib/tsgo-core-test-shards.mts"; + +describe("tsgo core test shards", () => { + it("accepts an exact once-only partition within the root budget", () => { + expect( + findTsgoCoreTestShardViolations({ + canonicalRoots: ["src/a.test.ts", "src/b.test.ts"], + maxRoots: 1, + shards: [ + { name: "a", roots: ["src/a.test.ts"] }, + { name: "b", roots: ["src/b.test.ts"] }, + ], + }), + ).toEqual([]); + }); + + it("reports missing, duplicate, extra, and oversized shard roots", () => { + expect( + findTsgoCoreTestShardViolations({ + canonicalRoots: ["src/a.test.ts", "src/b.test.ts", "src/missing.test.ts"], + maxRoots: 1, + shards: [ + { name: "first", roots: ["src/a.test.ts", "src/b.test.ts"] }, + { name: "second", roots: ["src/b.test.ts", "src/extra.test.ts"] }, + ], + }), + ).toEqual([ + "first: 2 test roots exceeds the 1 limit", + "second: 2 test roots exceeds the 1 limit", + "assigned 2 times (first, second): src/b.test.ts", + "unassigned: src/missing.test.ts", + "not in the canonical core-test graph (second): src/extra.test.ts", + ]); + }); +}); diff --git a/test/tsconfig/tsconfig.core.test.agents-other.json b/test/tsconfig/tsconfig.core.test.agents-other.json new file mode 100644 index 000000000000..0e111a061f1c --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.agents-other.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-agents-other.tsbuildinfo" + }, + "include": [ + "../../src/agents/**/*.test.ts", + "../../src/agents/**/*.test.tsx" + ], + "exclude": [ + "../../node_modules", + "../../dist", + "../../**/dist/**", + "../../src/agents/*.test.ts", + "../../src/agents/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.agents-root.json b/test/tsconfig/tsconfig.core.test.agents-root.json new file mode 100644 index 000000000000..1a99894cb7da --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.agents-root.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-agents-root.tsbuildinfo" + }, + "include": [ + "../../src/agents/*.test.ts", + "../../src/agents/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.agents.json b/test/tsconfig/tsconfig.core.test.agents.json deleted file mode 100644 index a36789122078..000000000000 --- a/test/tsconfig/tsconfig.core.test.agents.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.test.json", - "include": [ - "../../src/**/*.d.ts", - "../../src/agents/**/*.test.ts", - "../../src/agents/**/*.test.tsx", - "../../ui/**/*.d.ts", - "../../packages/**/*.d.ts" - ] -} diff --git a/test/tsconfig/tsconfig.core.test.commands.json b/test/tsconfig/tsconfig.core.test.commands.json new file mode 100644 index 000000000000..b804abfff43b --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.commands.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-commands.tsbuildinfo" + }, + "include": [ + "../../src/commands/**/*.test.ts", + "../../src/commands/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.config-cli.json b/test/tsconfig/tsconfig.core.test.config-cli.json new file mode 100644 index 000000000000..556e33b0769e --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.config-cli.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-config-cli.tsbuildinfo" + }, + "include": [ + "../../src/config/**/*.test.ts", + "../../src/config/**/*.test.tsx", + "../../src/cli/**/*.test.ts", + "../../src/cli/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.gateway.json b/test/tsconfig/tsconfig.core.test.gateway.json new file mode 100644 index 000000000000..81977b71996b --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.gateway.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-gateway.tsbuildinfo" + }, + "include": [ + "../../src/gateway/**/*.test.ts", + "../../src/gateway/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.infra.json b/test/tsconfig/tsconfig.core.test.infra.json new file mode 100644 index 000000000000..3b349cacae1e --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.infra.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-infra.tsbuildinfo" + }, + "include": [ + "../../src/infra/**/*.test.ts", + "../../src/infra/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.messaging.json b/test/tsconfig/tsconfig.core.test.messaging.json new file mode 100644 index 000000000000..21952f137858 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.messaging.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-messaging.tsbuildinfo" + }, + "include": [ + "../../src/auto-reply/**/*.test.ts", + "../../src/auto-reply/**/*.test.tsx", + "../../src/channels/**/*.test.ts", + "../../src/channels/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.non-agents.json b/test/tsconfig/tsconfig.core.test.non-agents.json deleted file mode 100644 index 5ecf4d956749..000000000000 --- a/test/tsconfig/tsconfig.core.test.non-agents.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.core.test.json", - "exclude": [ - "../../node_modules", - "../../dist", - "../../**/dist/**", - "../../src/agents/**/*.test.ts", - "../../src/agents/**/*.test.tsx" - ] -} diff --git a/test/tsconfig/tsconfig.core.test.other.json b/test/tsconfig/tsconfig.core.test.other.json new file mode 100644 index 000000000000..5941919cff6c --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.other.json @@ -0,0 +1,37 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-other.tsbuildinfo" + }, + "include": [ + "../../src/**/*.test.ts", + "../../src/**/*.test.tsx" + ], + "exclude": [ + "../../node_modules", + "../../dist", + "../../**/dist/**", + "../../src/agents/**", + "../../src/gateway/**", + "../../src/infra/**", + "../../src/commands/**", + "../../src/plugins/**", + "../../src/config/**", + "../../src/cli/**", + "../../src/auto-reply/**", + "../../src/channels/**", + "../../src/cron/**", + "../../src/plugin-sdk/**", + "../../src/skills/**", + "../../src/secrets/**", + "../../src/shared/**", + "../../src/security/**", + "../../src/acp/**", + "../../src/tui/**", + "../../src/media/**", + "../../src/system-agent/**", + "../../src/logging/**", + "../../src/hooks/**", + "../../src/daemon/**" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.plugins-platform.json b/test/tsconfig/tsconfig.core.test.plugins-platform.json new file mode 100644 index 000000000000..083aaece02e4 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.plugins-platform.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-plugins-platform.tsbuildinfo" + }, + "include": [ + "../../src/plugins/**/*.test.ts", + "../../src/plugins/**/*.test.tsx", + "../../src/security/**/*.test.ts", + "../../src/security/**/*.test.tsx", + "../../src/acp/**/*.test.ts", + "../../src/acp/**/*.test.tsx", + "../../src/tui/**/*.test.ts", + "../../src/tui/**/*.test.tsx", + "../../src/media/**/*.test.ts", + "../../src/media/**/*.test.tsx", + "../../src/system-agent/**/*.test.ts", + "../../src/system-agent/**/*.test.tsx", + "../../src/logging/**/*.test.ts", + "../../src/logging/**/*.test.tsx", + "../../src/hooks/**/*.test.ts", + "../../src/hooks/**/*.test.tsx", + "../../src/daemon/**/*.test.ts", + "../../src/daemon/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.projects.json b/test/tsconfig/tsconfig.core.test.projects.json new file mode 100644 index 000000000000..4244b729c7dc --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.projects.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.core.test.src.projects.json" }, + { "path": "./tsconfig.core.test.ui.projects.json" }, + { "path": "./tsconfig.test.packages.json" } + ] +} diff --git a/test/tsconfig/tsconfig.core.test.services.json b/test/tsconfig/tsconfig.core.test.services.json new file mode 100644 index 000000000000..c65e926247f4 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.services.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-services.tsbuildinfo" + }, + "include": [ + "../../src/cron/**/*.test.ts", + "../../src/cron/**/*.test.tsx", + "../../src/plugin-sdk/**/*.test.ts", + "../../src/plugin-sdk/**/*.test.tsx", + "../../src/skills/**/*.test.ts", + "../../src/skills/**/*.test.tsx", + "../../src/secrets/**/*.test.ts", + "../../src/secrets/**/*.test.tsx", + "../../src/shared/**/*.test.ts", + "../../src/shared/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.shard.json b/test/tsconfig/tsconfig.core.test.shard.json new file mode 100644 index 000000000000..3214eb74db7f --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.shard.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.core.test.json", + "files": [ + "../../src/config/sessions/session-entry.test-compat.d.ts", + "../../src/infra/host-env-security-policy.d.ts", + "../../src/state/openclaw-agent-db.generated.d.ts", + "../../src/state/openclaw-state-db.generated.d.ts", + "../../src/types/agent-sessions.d.ts", + "../../src/types/microsoft-teams-sdk.d.ts", + "../../src/types/node-runtime-globals.d.ts", + "../../src/types/qrcode.d.ts", + "../../ui/src/css.d.ts", + "../../ui/src/i18n/virtual-locale.d.ts", + "../../ui/src/markdown-it-task-lists.d.ts", + "../../ui/src/types/highlight-js-subpaths.d.ts", + "../../ui/src/types/novnc.d.ts" + ], + "include": [] +} diff --git a/test/tsconfig/tsconfig.core.test.src.projects.json b/test/tsconfig/tsconfig.core.test.src.projects.json new file mode 100644 index 000000000000..9cd057b6e612 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.src.projects.json @@ -0,0 +1,15 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.core.test.agents-root.json" }, + { "path": "./tsconfig.core.test.agents-other.json" }, + { "path": "./tsconfig.core.test.gateway.json" }, + { "path": "./tsconfig.core.test.infra.json" }, + { "path": "./tsconfig.core.test.commands.json" }, + { "path": "./tsconfig.core.test.plugins-platform.json" }, + { "path": "./tsconfig.core.test.config-cli.json" }, + { "path": "./tsconfig.core.test.messaging.json" }, + { "path": "./tsconfig.core.test.services.json" }, + { "path": "./tsconfig.core.test.other.json" } + ] +} diff --git a/test/tsconfig/tsconfig.core.test.ui-other.json b/test/tsconfig/tsconfig.core.test.ui-other.json new file mode 100644 index 000000000000..4849682c3c62 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.ui-other.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-ui-other.tsbuildinfo" + }, + "include": [ + "../../ui/src/main.ts", + "../../ui/**/*.test.ts", + "../../ui/**/*.test.tsx" + ], + "exclude": [ + "../../node_modules", + "../../dist", + "../../**/dist/**", + "../../ui/src/pages/**", + "../../ui/src/e2e/**" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.ui-pages-e2e.json b/test/tsconfig/tsconfig.core.test.ui-pages-e2e.json new file mode 100644 index 000000000000..6ff5ccf2d528 --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.ui-pages-e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/core-test-ui-pages-e2e.tsbuildinfo" + }, + "include": [ + "../../ui/src/main.ts", + "../../ui/src/pages/**/*.test.ts", + "../../ui/src/pages/**/*.test.tsx", + "../../ui/src/e2e/**/*.test.ts", + "../../ui/src/e2e/**/*.test.tsx" + ] +} diff --git a/test/tsconfig/tsconfig.core.test.ui.projects.json b/test/tsconfig/tsconfig.core.test.ui.projects.json new file mode 100644 index 000000000000..43c229a30d8b --- /dev/null +++ b/test/tsconfig/tsconfig.core.test.ui.projects.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.core.test.ui-pages-e2e.json" }, + { "path": "./tsconfig.core.test.ui-other.json" } + ] +} diff --git a/test/tsconfig/tsconfig.test.packages.json b/test/tsconfig/tsconfig.test.packages.json index d7e59173a72e..5c725e402be4 100644 --- a/test/tsconfig/tsconfig.test.packages.json +++ b/test/tsconfig/tsconfig.test.packages.json @@ -1,10 +1,9 @@ { - "extends": "./tsconfig.test.json", + "extends": "./tsconfig.core.test.shard.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.artifacts/tsgo-cache/test-packages.tsbuildinfo" + }, "include": [ - "../../src/**/*.d.ts", - "../../ui/**/*.d.ts", - "../../extensions/**/*.d.ts", - "../../packages/**/*.d.ts", "../../packages/**/*.test.ts", "../../packages/**/*.test.tsx" ] diff --git a/test/tsconfig/tsconfig.test.src.json b/test/tsconfig/tsconfig.test.src.json deleted file mode 100644 index d646b46b6951..000000000000 --- a/test/tsconfig/tsconfig.test.src.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.test.json", - "compilerOptions": { - // Retained plain-Node script exceptions are imported by their owner tests. - "allowJs": true - }, - "include": [ - "../../src/**/*.d.ts", - "../../src/**/*.test.ts", - "../../src/**/*.test.tsx", - "../../ui/**/*.d.ts", - "../../ui/src/main.ts", - "../../extensions/**/*.d.ts", - "../../packages/**/*.d.ts" - ] -} diff --git a/test/tsconfig/tsconfig.test.ui.json b/test/tsconfig/tsconfig.test.ui.json deleted file mode 100644 index d7f168d83a4b..000000000000 --- a/test/tsconfig/tsconfig.test.ui.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.test.json", - "include": [ - "../../src/**/*.d.ts", - "../../ui/**/*.d.ts", - "../../ui/**/*.test.ts", - "../../ui/**/*.test.tsx", - "../../extensions/**/*.d.ts", - "../../packages/**/*.d.ts" - ] -} diff --git a/tsconfig.core.projects.json b/tsconfig.core.projects.json index d66561f85d03..32510eae4e01 100644 --- a/tsconfig.core.projects.json +++ b/tsconfig.core.projects.json @@ -3,6 +3,6 @@ "references": [ { "path": "./tsconfig.core.json" }, { "path": "./tsconfig.ui.json" }, - { "path": "./test/tsconfig/tsconfig.core.test.json" } + { "path": "./test/tsconfig/tsconfig.core.test.projects.json" } ] }