diff --git a/src/plugin-sdk/index.bundle.test.ts b/src/plugin-sdk/index.bundle.test.ts new file mode 100644 index 00000000000..1f3afc8ab3a --- /dev/null +++ b/src/plugin-sdk/index.bundle.test.ts @@ -0,0 +1,106 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import { createRequire } from "node:module"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; +import { + buildPluginSdkEntrySources, + buildPluginSdkPackageExports, + buildPluginSdkSpecifiers, + pluginSdkEntrypoints, +} from "./entrypoints.js"; + +const pluginSdkSpecifiers = buildPluginSdkSpecifiers(); +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const tsdownModuleUrl = pathToFileURL(require.resolve("tsdown")).href; + +describe("plugin-sdk bundled exports", () => { + it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => { + const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-")); + const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-")); + + try { + const buildScriptPath = path.join(fixtureDir, "build-plugin-sdk.mjs"); + await fs.writeFile( + buildScriptPath, + `import { build } from ${JSON.stringify(tsdownModuleUrl)}; +await build(${JSON.stringify({ + clean: true, + config: false, + dts: false, + entry: buildPluginSdkEntrySources(), + env: { NODE_ENV: "production" }, + fixedExtension: false, + logLevel: "error", + outDir, + platform: "node", + })}); +`, + ); + await execFileAsync(process.execPath, [buildScriptPath], { + cwd: process.cwd(), + }); + await fs.symlink( + path.join(process.cwd(), "node_modules"), + path.join(outDir, "node_modules"), + "dir", + ); + + for (const entry of pluginSdkEntrypoints) { + const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href); + expect(module).toBeTypeOf("object"); + } + + const packageDir = path.join(fixtureDir, "openclaw"); + const consumerDir = path.join(fixtureDir, "consumer"); + const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs"); + + await fs.mkdir(path.join(packageDir, "dist"), { recursive: true }); + await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir"); + // Mirror the installed package layout so subpaths can resolve root deps. + await fs.symlink( + path.join(process.cwd(), "node_modules"), + path.join(packageDir, "node_modules"), + "dir", + ); + await fs.writeFile( + path.join(packageDir, "package.json"), + JSON.stringify( + { + exports: buildPluginSdkPackageExports(), + name: "openclaw", + type: "module", + }, + null, + 2, + ), + ); + + await fs.mkdir(path.join(consumerDir, "node_modules"), { recursive: true }); + await fs.symlink(packageDir, path.join(consumerDir, "node_modules", "openclaw"), "dir"); + await fs.writeFile( + consumerEntry, + [ + `const specifiers = ${JSON.stringify(pluginSdkSpecifiers)};`, + "const results = {};", + "for (const specifier of specifiers) {", + " results[specifier] = typeof (await import(specifier));", + "}", + "export default results;", + ].join("\n"), + ); + + const { default: importResults } = await import(pathToFileURL(consumerEntry).href); + expect(importResults).toEqual( + Object.fromEntries(pluginSdkSpecifiers.map((specifier: string) => [specifier, "object"])), + ); + } finally { + await fs.rm(outDir, { recursive: true, force: true }); + await fs.rm(fixtureDir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/plugin-sdk/index.test.ts b/src/plugin-sdk/index.test.ts index 89ca3901ff3..30040416729 100644 --- a/src/plugin-sdk/index.test.ts +++ b/src/plugin-sdk/index.test.ts @@ -1,24 +1,9 @@ -import { execFile } from "node:child_process"; import fs from "node:fs/promises"; -import { createRequire } from "node:module"; -import os from "node:os"; import path from "node:path"; -import { pathToFileURL } from "node:url"; -import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; -import { - buildPluginSdkEntrySources, - buildPluginSdkPackageExports, - buildPluginSdkSpecifiers, - pluginSdkEntrypoints, -} from "./entrypoints.js"; +import { buildPluginSdkPackageExports } from "./entrypoints.js"; import * as sdk from "./index.js"; -const pluginSdkSpecifiers = buildPluginSdkSpecifiers(); -const execFileAsync = promisify(execFile); -const require = createRequire(import.meta.url); -const tsdownModuleUrl = pathToFileURL(require.resolve("tsdown")).href; - describe("plugin-sdk exports", () => { it("does not expose runtime modules", () => { const forbidden = [ @@ -70,91 +55,6 @@ describe("plugin-sdk exports", () => { expect(Object.prototype.hasOwnProperty.call(sdk, "isDangerousNameMatchingEnabled")).toBe(false); }); - it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => { - const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-")); - const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-")); - - try { - const buildScriptPath = path.join(fixtureDir, "build-plugin-sdk.mjs"); - await fs.writeFile( - buildScriptPath, - `import { build } from ${JSON.stringify(tsdownModuleUrl)}; -await build(${JSON.stringify({ - clean: true, - config: false, - dts: false, - entry: buildPluginSdkEntrySources(), - env: { NODE_ENV: "production" }, - fixedExtension: false, - logLevel: "error", - outDir, - platform: "node", - })}); -`, - ); - await execFileAsync(process.execPath, [buildScriptPath], { - cwd: process.cwd(), - }); - await fs.symlink( - path.join(process.cwd(), "node_modules"), - path.join(outDir, "node_modules"), - "dir", - ); - - for (const entry of pluginSdkEntrypoints) { - const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href); - expect(module).toBeTypeOf("object"); - } - - const packageDir = path.join(fixtureDir, "openclaw"); - const consumerDir = path.join(fixtureDir, "consumer"); - const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs"); - - await fs.mkdir(path.join(packageDir, "dist"), { recursive: true }); - await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir"); - // Mirror the installed package layout so subpaths can resolve root deps. - await fs.symlink( - path.join(process.cwd(), "node_modules"), - path.join(packageDir, "node_modules"), - "dir", - ); - await fs.writeFile( - path.join(packageDir, "package.json"), - JSON.stringify( - { - exports: buildPluginSdkPackageExports(), - name: "openclaw", - type: "module", - }, - null, - 2, - ), - ); - - await fs.mkdir(path.join(consumerDir, "node_modules"), { recursive: true }); - await fs.symlink(packageDir, path.join(consumerDir, "node_modules", "openclaw"), "dir"); - await fs.writeFile( - consumerEntry, - [ - `const specifiers = ${JSON.stringify(pluginSdkSpecifiers)};`, - "const results = {};", - "for (const specifier of specifiers) {", - " results[specifier] = typeof (await import(specifier));", - "}", - "export default results;", - ].join("\n"), - ); - - const { default: importResults } = await import(pathToFileURL(consumerEntry).href); - expect(importResults).toEqual( - Object.fromEntries(pluginSdkSpecifiers.map((specifier: string) => [specifier, "object"])), - ); - } finally { - await fs.rm(outDir, { recursive: true, force: true }); - await fs.rm(fixtureDir, { recursive: true, force: true }); - } - }); - it("keeps package.json plugin-sdk exports synced with the manifest", async () => { const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as { diff --git a/test/fixtures/test-parallel.behavior.json b/test/fixtures/test-parallel.behavior.json index f0585bd0249..bc23a5ab88c 100644 --- a/test/fixtures/test-parallel.behavior.json +++ b/test/fixtures/test-parallel.behavior.json @@ -63,6 +63,10 @@ "file": "src/plugin-sdk/index.test.ts", "reason": "Plugin SDK index coverage retained a broad export graph in unit-fast and is safer outside the shared lane." }, + { + "file": "src/plugin-sdk/index.bundle.test.ts", + "reason": "Plugin SDK bundle validation builds and imports the full bundled export graph and is safer outside the shared lane." + }, { "file": "src/config/sessions.cache.test.ts", "reason": "Session cache coverage retained a large config/session graph in unit-fast on Linux CI."