From ed650b652f9b15835f01a464318f699e73f8a5e5 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 05:17:39 -0700 Subject: [PATCH] fix(test): detect partial sparse core roots --- scripts/lib/tsgo-sparse-guard.mjs | 69 ++++++++++++++++++++++++++++--- test/scripts/run-tsgo.test.ts | 41 +++++++++++++++++- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/scripts/lib/tsgo-sparse-guard.mjs b/scripts/lib/tsgo-sparse-guard.mjs index 93e63c06bc6..28e2a670bda 100644 --- a/scripts/lib/tsgo-sparse-guard.mjs +++ b/scripts/lib/tsgo-sparse-guard.mjs @@ -11,6 +11,7 @@ const CORE_TEST_CONFIGS = new Set([ const CORE_PROD_CONFIGS = new Set(["tsconfig.core.json"]); const TSGO_SPARSE_SKIP_ENV_KEY = "OPENCLAW_TSGO_SPARSE_SKIP"; +const CORE_SPARSE_ROOTS = ["packages", "ui/src"]; const CORE_PROD_REQUIRED_PATHS = [ { @@ -53,7 +54,12 @@ export function createSparseTsgoSkipEnv(baseEnv = process.env) { export function getSparseTsgoGuardError( args, - { cwd = process.cwd(), fileExists = fs.existsSync, isSparseCheckoutEnabled } = {}, + { + cwd = process.cwd(), + fileExists = fs.existsSync, + isSparseCheckoutEnabled, + sparseCheckoutPatterns, + } = {}, ) { const projectPath = readProjectFlag(args); const projectName = projectPath ? path.basename(projectPath) : null; @@ -71,20 +77,33 @@ export function getSparseTsgoGuardError( return null; } - const missingPaths = getRequiredPathsForProject(projectName, cwd, fileExists).filter( - (relativePath) => !fileExists(path.join(cwd, relativePath)), - ); + const sparsePatterns = sparseCheckoutPatterns ?? getSparseCheckoutPatterns({ cwd }); + const missingPaths = [ + ...getRequiredSparseRootsForProject(projectName).filter((relativePath) => + sparsePatterns ? !isSparseRootCovered(relativePath, sparsePatterns) : false, + ), + ...getRequiredPathsForProject(projectName, cwd, fileExists).filter( + (relativePath) => !fileExists(path.join(cwd, relativePath)), + ), + ]; if (missingPaths.length === 0) { return null; } return [ - `${projectName} cannot be typechecked from this sparse checkout because tracked project inputs are missing:`, + `${projectName} 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"); } +function getRequiredSparseRootsForProject(projectName) { + if (CORE_PROD_CONFIGS.has(projectName) || CORE_TEST_CONFIGS.has(projectName)) { + return CORE_SPARSE_ROOTS; + } + return []; +} + function getRequiredPathsForProject(projectName, cwd, fileExists) { const requiredPaths = []; if (CORE_PROD_CONFIGS.has(projectName)) { @@ -117,6 +136,46 @@ function getGitBooleanConfig(name, { cwd }) { return (result.stdout ?? "").trim() === "true"; } +function getSparseCheckoutPatterns({ cwd }) { + const result = spawnSync("git", ["sparse-checkout", "list"], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + shell: process.platform === "win32", + }); + + if (result.error || (result.status ?? 1) !== 0) { + return null; + } + + return (result.stdout ?? "") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); +} + +function isSparseRootCovered(relativeRoot, patterns) { + const root = normalizeSparsePattern(relativeRoot); + return patterns.some((pattern) => { + if (pattern.startsWith("!")) { + return false; + } + + const normalized = normalizeSparsePattern(pattern); + return normalized === root || (normalized.length > 0 && root.startsWith(`${normalized}/`)); + }); +} + +function normalizeSparsePattern(pattern) { + return pattern + .trim() + .replaceAll("\\", "/") + .replace(/^!/, "") + .replace(/^\/+/, "") + .replace(/\/\*\*$/, "") + .replace(/\/+$/, ""); +} + function readProjectFlag(args) { return readFlagValue(args, "-p") ?? readFlagValue(args, "--project"); } diff --git a/test/scripts/run-tsgo.test.ts b/test/scripts/run-tsgo.test.ts index 1d457d23806..094860cb97e 100644 --- a/test/scripts/run-tsgo.test.ts +++ b/test/scripts/run-tsgo.test.ts @@ -65,10 +65,47 @@ describe("run-tsgo sparse guard", () => { getSparseTsgoGuardError(["-p", "tsconfig.core.test.non-agents.json"], { cwd, isSparseCheckoutEnabled: () => true, + sparseCheckoutPatterns: ["/packages/", "/ui/src/"], }), ).toBeNull(); }); + it("rejects sparse core worktrees that include only selected ui and package files", () => { + const cwd = createTempDir("openclaw-run-tsgo-"); + const requiredPaths = [ + "packages/plugin-package-contract/src/index.ts", + "ui/src/i18n/lib/registry.ts", + "ui/src/i18n/lib/types.ts", + "ui/src/ui/app-settings.ts", + "ui/src/ui/gateway.ts", + ]; + + for (const relativePath of requiredPaths) { + const absolutePath = path.join(cwd, relativePath); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, "", "utf8"); + } + + expect( + getSparseTsgoGuardError(["-p", "tsconfig.core.test.json"], { + cwd, + isSparseCheckoutEnabled: () => true, + sparseCheckoutPatterns: [ + "/packages/plugin-package-contract/src/index.ts", + "/ui/src/i18n/lib/registry.ts", + "/ui/src/i18n/lib/types.ts", + "/ui/src/ui/app-settings.ts", + "/ui/src/ui/gateway.ts", + ], + }), + ).toMatchInlineSnapshot(` + "tsconfig.core.test.json cannot be typechecked from this sparse checkout because tracked project inputs are missing or only partially included: + - packages + - ui/src + Expand this worktree's sparse checkout to include those paths, or rerun in a full worktree." + `); + }); + it("returns a helpful message for sparse core worktrees missing transitive project files", () => { const cwd = createTempDir("openclaw-run-tsgo-"); const uiToolDisplay = path.join(cwd, "ui/src/ui/tool-display.ts"); @@ -81,7 +118,7 @@ describe("run-tsgo sparse guard", () => { isSparseCheckoutEnabled: () => true, }), ).toMatchInlineSnapshot(` - "tsconfig.core.json cannot be typechecked from this sparse checkout because tracked project inputs are missing: + "tsconfig.core.json cannot be typechecked from this sparse checkout because tracked project inputs are missing or only partially included: - apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json Expand this worktree's sparse checkout to include those paths, or rerun in a full worktree." `); @@ -96,7 +133,7 @@ describe("run-tsgo sparse guard", () => { isSparseCheckoutEnabled: () => true, }), ).toMatchInlineSnapshot(` - "tsconfig.core.test.json cannot be typechecked from this sparse checkout because tracked project inputs are missing: + "tsconfig.core.test.json cannot be typechecked from this sparse checkout because tracked project inputs are missing or only partially included: - packages/plugin-package-contract/src/index.ts - ui/src/i18n/lib/registry.ts - ui/src/i18n/lib/types.ts