diff --git a/src/plugins/contracts/extension-package-project-boundaries.test.ts b/src/plugins/contracts/extension-package-project-boundaries.test.ts index 3689b4ff7f7..d64d561606f 100644 --- a/src/plugins/contracts/extension-package-project-boundaries.test.ts +++ b/src/plugins/contracts/extension-package-project-boundaries.test.ts @@ -1,6 +1,7 @@ -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; import { relative, resolve } from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { collectExtensionsWithTsconfig, collectOptInExtensionPackageBoundaries, @@ -18,6 +19,7 @@ const EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG = "extensions/tsconfig.package-boundary.paths.json" as const; const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG = "extensions/tsconfig.package-boundary.base.json" as const; +const trackedCodeFilesByRoot = new Map(); type TsConfigJson = { extends?: unknown; @@ -71,13 +73,41 @@ const MEMORY_HOST_SDK_RUNTIME_ADAPTER_FILES = [ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test helper lets assertions ascribe JSON file shape. function readJsonFile(relativePath: string): T { - return JSON.parse(readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T; + return JSON.parse(fs.readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T; +} + +function listTrackedCodeFiles(relativeDir: string): string[] | null { + if (trackedCodeFilesByRoot.has(relativeDir)) { + const files = trackedCodeFilesByRoot.get(relativeDir); + return files ? [...files] : null; + } + const result = spawnSync("git", ["ls-files", "--", relativeDir], { + cwd: REPO_ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + trackedCodeFilesByRoot.set(relativeDir, null); + return null; + } + const files = result.stdout + .split("\n") + .map((line) => line.trim().replaceAll("\\", "/")) + .filter((line) => line.length > 0 && /\.(?:[cm]?ts|tsx|mts|cts)$/u.test(line)) + .toSorted(); + trackedCodeFilesByRoot.set(relativeDir, files); + return [...files]; } function collectCodeFiles(relativeDir: string): string[] { + const trackedFiles = listTrackedCodeFiles(relativeDir); + if (trackedFiles) { + return trackedFiles; + } + const dir = resolve(REPO_ROOT, relativeDir); const files: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const nextPath = resolve(dir, entry.name); if (entry.isDirectory()) { files.push(...collectCodeFiles(relative(REPO_ROOT, nextPath).replaceAll("\\", "/"))); @@ -92,19 +122,33 @@ function collectCodeFiles(relativeDir: string): string[] { function collectCoreReferenceFiles(relativeDir: string): string[] { return collectCodeFiles(relativeDir).filter((file) => { - const source = readFileSync(resolve(REPO_ROOT, file), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8"); return source.includes("../../../../src/") || source.includes("../../../src/"); }); } function collectOpenClawRuntimeDirectImportFiles(relativeDir: string): string[] { return collectCodeFiles(relativeDir).filter((file) => { - const source = readFileSync(resolve(REPO_ROOT, file), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8"); return source.includes('"./openclaw-runtime.js"'); }); } describe("opt-in extension package boundaries", () => { + it("lists package boundary code files from git without walking package roots", () => { + const readDir = vi.spyOn(fs, "readdirSync"); + try { + const memoryHostFiles = collectCodeFiles("packages/memory-host-sdk/src"); + const packageContractFiles = collectCodeFiles("packages/plugin-package-contract/src"); + + expect(memoryHostFiles.length).toBeGreaterThan(0); + expect(packageContractFiles.length).toBeGreaterThan(0); + expect(readDir).not.toHaveBeenCalled(); + } finally { + readDir.mockRestore(); + } + }); + it("keeps path aliases in a dedicated shared config", () => { const pathsConfig = readJsonFile(EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG); expect(pathsConfig.extends).toBe("../tsconfig.json"); @@ -244,7 +288,7 @@ describe("opt-in extension package boundaries", () => { "./dist/src/plugin-sdk/text-runtime.d.ts", ); expect(packageJson.exports?.["./zod"]?.types).toBe("./dist/src/plugin-sdk/zod.d.ts"); - expect(existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe( + expect(fs.existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe( false, ); }); @@ -265,7 +309,10 @@ describe("opt-in extension package boundaries", () => { if (!target) { throw new Error(`Missing memory-host-sdk export target for ${exportPath}`); } - const source = readFileSync(resolve(REPO_ROOT, "packages/memory-host-sdk", target), "utf8"); + const source = fs.readFileSync( + resolve(REPO_ROOT, "packages/memory-host-sdk", target), + "utf8", + ); expect(source, target).not.toContain("src/memory-host-sdk/"); }