// Extension import boundary checker tests cover bounded source reads. import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createExtensionImportBoundaryChecker } from "../../scripts/lib/extension-import-boundary-checker.mts"; import { listGeneratedExtensionAssetSources } from "../../scripts/lib/static-extension-assets.mts"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); describe("extension import boundary checker", () => { it("rejects oversized TypeScript source files before scanning imports", async () => { const root = tempDirs.make("openclaw-extension-boundary-"); const sourcePath = path.join(root, "large.ts"); writeFileSync(sourcePath, "x".repeat(33), "utf8"); const checker = createExtensionImportBoundaryChecker({ boundaryLabel: "test", cleanMessage: "clean", inventoryTitle: "inventory", maxSourceBytes: 32, roots: [path.relative(process.cwd(), root)], }); await expect(checker.collectInventory()).rejects.toThrow( "extension import boundary source file exceeds 32 byte limit", ); }); it("skips declared generated bundles without admitting oversized handwritten JavaScript", async () => { const root = tempDirs.make("openclaw-extension-boundary-"); const pluginRoot = path.join(root, "extensions", "generated-plugin"); const assetRoot = path.join(pluginRoot, "assets"); const generatedPath = path.join(assetRoot, "generated.js"); const handwrittenPath = path.join(assetRoot, "handwritten.js"); const oversizedBytes = 2 * 1024 * 1024 + 1; mkdirSync(assetRoot, { recursive: true }); writeFileSync( path.join(pluginRoot, "package.json"), JSON.stringify({ openclaw: { assetScripts: { build: "node generate.mjs" }, build: { staticAssets: [{ source: "./assets/generated.js", output: "assets/generated.js" }], }, }, }), "utf8", ); // Match CI ordering: the build materializes its declared bundle before source guards run. writeFileSync(generatedPath, "/".repeat(oversizedBytes), "utf8"); const generatedSources = new Set( listGeneratedExtensionAssetSources({ rootDir: root }).map((source) => path.relative(process.cwd(), path.join(root, source)).replaceAll(path.sep, "/"), ), ); const createChecker = () => createExtensionImportBoundaryChecker({ boundaryLabel: "test", cleanMessage: "clean", inventoryTitle: "inventory", roots: [path.relative(process.cwd(), path.join(root, "extensions"))], shouldSkipFile: (relativeFile: string) => generatedSources.has(relativeFile), sourceOptions: { fileExtensions: [".js"] }, }); await expect(createChecker().collectInventory()).resolves.toEqual([]); const targetPath = path.join(process.cwd(), "extensions", "security-proof", "private.js"); const specifier = path.relative(assetRoot, targetPath).replaceAll(path.sep, "/"); writeFileSync( handwrittenPath, "import " + JSON.stringify(specifier) + ";\n" + "/".repeat(oversizedBytes), "utf8", ); await expect(createChecker().collectInventory()).rejects.toThrow( "extension import boundary source file exceeds 2097152 byte limit", ); }); it.each([ { name: "comment-obscured side-effect import", createSource: (specifier: string) => "import /* gap */ " + JSON.stringify(specifier), kind: "import", }, { name: "dynamic import with attributes", createSource: (specifier: string) => "import /* gap */ (" + JSON.stringify(specifier) + ', { with: { type: "json" } })', kind: "dynamic-import", }, { name: "CommonJS require", createSource: (specifier: string) => "require(" + JSON.stringify(specifier) + ")", kind: "commonjs-require", }, { name: "import.meta URL", createSource: (specifier: string) => "new URL(" + JSON.stringify(specifier) + ", import.meta.url)", kind: "import-meta-url", }, { name: "comment-obscured namespace export", createSource: (specifier: string) => "export /* gap */ * /* gap */ as privateModule from " + JSON.stringify(specifier), kind: "export", }, { name: "escaped plugin-path separator", createSource: (specifier: string) => "import " + JSON.stringify(specifier).replace("extensions/", "extensions\\/"), kind: "import", }, { name: "Unicode-escaped plugin-path character", createSource: (specifier: string) => "import " + JSON.stringify(specifier).replace("extensions/", "exten\\u0073ions/"), kind: "import", }, { name: "hex-escaped plugin-path character", createSource: (specifier: string) => "import " + JSON.stringify(specifier).replace("extensions/", "\\x65xtensions/"), kind: "import", }, { name: "plugin-path line continuation", createSource: (specifier: string) => "import " + JSON.stringify(specifier).replace("extensions/", "exten\\\nsions/"), kind: "import", }, ])("rejects $name crossing the real plugin boundary", async ({ createSource, kind }) => { const root = tempDirs.make("openclaw-extension-boundary-"); const sourcePath = path.join(root, "guarded.ts"); const targetPath = path.join(process.cwd(), "extensions", "security-proof", "private.js"); const specifier = path.relative(root, targetPath).split(path.sep).join("/"); writeFileSync(sourcePath, createSource(specifier), "utf8"); const checker = createExtensionImportBoundaryChecker({ boundaryLabel: "test", cleanMessage: "clean", inventoryTitle: "inventory", roots: [path.relative(process.cwd(), root)], skipSourcesWithoutBundledPluginPrefix: true, }); await expect(checker.collectInventory()).resolves.toEqual([ expect.objectContaining({ kind, resolvedPath: "extensions/security-proof/private.js", specifier, }), ]); }); });