diff --git a/scripts/e2e/lib/clawhub-fixture-server.cjs b/scripts/e2e/lib/clawhub-fixture-server.cjs index f02ba6340e0..6a20cc1e4c1 100644 --- a/scripts/e2e/lib/clawhub-fixture-server.cjs +++ b/scripts/e2e/lib/clawhub-fixture-server.cjs @@ -17,12 +17,36 @@ const profiles = { packageJson: { name: packageName, version: "0.1.3", + type: "module", + dependencies: { + "is-number": "7.0.0", + }, + peerDependencies: { + openclaw: ">=2026.4.11", + }, + peerDependenciesMeta: { + openclaw: { + optional: true, + }, + }, openclaw: { extensions: ["./index.js"] }, }, - indexJs: `module.exports = { + indexJs: `import isNumber from "is-number"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; + +const dependencyUrl = import.meta.resolve("is-number"); +const expectedDependencyBaseUrl = new URL("./node_modules/is-number/", import.meta.url).href; +if (!dependencyUrl.startsWith(expectedDependencyBaseUrl)) { + throw new Error(\`kitchen-sink dependency resolved outside plugin root: \${dependencyUrl}\`); +} + +export default definePluginEntry({ id: "${pluginId}", name: "OpenClaw Kitchen Sink", register(api) { + if (!isNumber(42)) { + throw new Error("kitchen-sink dependency sentinel did not load"); + } api.registerProvider({ id: "kitchen-sink-provider", label: "Kitchen Sink Provider", @@ -48,7 +72,7 @@ const profiles = { }, }); }, -}; +}); `, manifest: { id: pluginId, diff --git a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs index e1018328bbe..336d7257f35 100644 --- a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs +++ b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs @@ -119,6 +119,38 @@ const expectIncludes = (listValue, expected, field) => { } }; +function assertRealPathInside(parentPath, childPath, label) { + const parentRealPath = fs.realpathSync(parentPath); + const childRealPath = fs.realpathSync(childPath); + if ( + childRealPath !== parentRealPath && + !childRealPath.startsWith(`${parentRealPath}${path.sep}`) + ) { + throw new Error(`${label} resolved outside ${parentPath}: ${childRealPath}`); + } +} + +function assertClawHubExternalInstallContract(installPath) { + const openclawPeerPath = path.join(installPath, "node_modules", "openclaw"); + if (!fs.existsSync(openclawPeerPath)) { + throw new Error(`missing kitchen-sink openclaw peer symlink: ${openclawPeerPath}`); + } + if (!fs.lstatSync(openclawPeerPath).isSymbolicLink()) { + throw new Error(`kitchen-sink openclaw peer is not a symlink: ${openclawPeerPath}`); + } + const hostRoot = fs.realpathSync(process.cwd()); + const linkedHostRoot = fs.realpathSync(openclawPeerPath); + if (linkedHostRoot !== hostRoot) { + throw new Error(`expected kitchen-sink openclaw peer ${linkedHostRoot} to target ${hostRoot}`); + } + + const dependencyPackagePath = path.join(installPath, "node_modules", "is-number", "package.json"); + if (!fs.existsSync(dependencyPackagePath)) { + throw new Error(`missing kitchen-sink isolated dependency: ${dependencyPackagePath}`); + } + assertRealPathInside(installPath, dependencyPackagePath, "kitchen-sink isolated dependency"); +} + function assertInstalled() { const pluginId = process.env.KITCHEN_SINK_ID; const spec = process.env.KITCHEN_SINK_SPEC; @@ -270,6 +302,9 @@ function assertInstalled() { if (!fs.existsSync(installPath)) { throw new Error(`kitchen-sink install path missing: ${record.installPath}`); } + if (source === "clawhub") { + assertClawHubExternalInstallContract(installPath); + } fs.writeFileSync(`/tmp/kitchen-sink-${label}-install-path.txt`, installPath, "utf8"); } diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 771b760b8b4..1ea45b75b63 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -112,7 +112,11 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(sweepScript).toContain("run_failure_scenario"); expect(assertionsScript).toContain("record.source !== source"); expect(assertionsScript).toContain("record.clawhubPackage !== packageName"); + expect(assertionsScript).toContain("assertClawHubExternalInstallContract"); expect(assertionsScript).toContain("expectedErrorMessages"); + expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain( + 'from "openclaw/plugin-sdk/plugin-entry"', + ); expect(script).toContain("docker stats --no-stream"); expect(sweepScript).toContain("scan_logs_for_unexpected_errors"); });