mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
test(plugins): extend external install contract coverage
This commit is contained in:
@@ -213,6 +213,38 @@ function assertMarketplaceRecords() {
|
||||
}
|
||||
}
|
||||
|
||||
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 ClawHub openclaw peer symlink: ${openclawPeerPath}`);
|
||||
}
|
||||
if (!fs.lstatSync(openclawPeerPath).isSymbolicLink()) {
|
||||
throw new Error(`ClawHub 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 ClawHub 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 ClawHub isolated dependency: ${dependencyPackagePath}`);
|
||||
}
|
||||
assertRealPathInside(installPath, dependencyPackagePath, "ClawHub isolated dependency");
|
||||
}
|
||||
|
||||
function assertMarketplaceUpdated() {
|
||||
const data = readJson("/tmp/plugins-marketplace-updated.json");
|
||||
const inspect = readJson("/tmp/plugins-marketplace-updated-inspect.json");
|
||||
@@ -322,6 +354,7 @@ function assertClawHubInstalled() {
|
||||
if (!fs.existsSync(installPath)) {
|
||||
throw new Error(`ClawHub install path missing on disk: ${installPath}`);
|
||||
}
|
||||
assertClawHubExternalInstallContract(installPath);
|
||||
fs.writeFileSync("/tmp/plugins-clawhub-install-path.txt", installPath, "utf8");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const PLUGIN_PRERELEASE_REQUIRED_SURFACES = Object.freeze([
|
||||
"config-round-trip",
|
||||
"gateway-bootstrap",
|
||||
"sdk-compatibility",
|
||||
"external-install-boundary",
|
||||
"status-diagnostics",
|
||||
"npm-registry-plugin",
|
||||
"clawhub-registry-plugin",
|
||||
@@ -40,13 +41,19 @@ const pluginPrereleaseDockerLanes = Object.freeze([
|
||||
},
|
||||
{
|
||||
lane: "plugins",
|
||||
surfaces: ["external-plugins", "sdk-compatibility", "status-diagnostics"],
|
||||
surfaces: [
|
||||
"external-plugins",
|
||||
"sdk-compatibility",
|
||||
"external-install-boundary",
|
||||
"status-diagnostics",
|
||||
],
|
||||
},
|
||||
{
|
||||
lane: "kitchen-sink-plugin",
|
||||
surfaces: [
|
||||
"external-plugins",
|
||||
"sdk-compatibility",
|
||||
"external-install-boundary",
|
||||
"status-diagnostics",
|
||||
"npm-registry-plugin",
|
||||
"clawhub-registry-plugin",
|
||||
|
||||
@@ -2799,6 +2799,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
function writePluginWithPeerDeps(
|
||||
pluginDir: string,
|
||||
peerDependencies: Record<string, string>,
|
||||
dependencies?: Record<string, string>,
|
||||
): void {
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -2807,6 +2808,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
name: "peer-dep-plugin",
|
||||
version: "1.0.0",
|
||||
openclaw: { extensions: ["index.js"] },
|
||||
...(dependencies ? { dependencies } : {}),
|
||||
peerDependencies,
|
||||
}),
|
||||
"utf-8",
|
||||
@@ -2836,6 +2838,31 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the openclaw peer symlink when plugin package dependencies are installed", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
const run = vi.mocked(runCommandWithTimeout);
|
||||
mockSuccessfulCommandRun(run);
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
|
||||
writePluginWithPeerDeps(pluginDir, { openclaw: "*" }, { "is-number": "7.0.0" });
|
||||
|
||||
const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
expectSingleNpmInstallIgnoreScriptsCall({
|
||||
calls: run.mock.calls as Array<[unknown, { cwd?: string } | undefined]>,
|
||||
expectedTargetDir: result.targetDir,
|
||||
});
|
||||
const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw");
|
||||
expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
|
||||
});
|
||||
|
||||
it("does not create a symlink when peerDependencies is empty", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
resolveRootMock.mockReturnValue(suiteTempRootTracker.makeTempDir());
|
||||
|
||||
@@ -163,12 +163,25 @@ function createPluginSdkAliasTargetFixture(params?: {
|
||||
distFile: "channel-runtime.js",
|
||||
packageExports: {
|
||||
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||
"./plugin-sdk/plugin-entry": { default: "./dist/plugin-sdk/plugin-entry.js" },
|
||||
},
|
||||
});
|
||||
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
||||
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
||||
const sourcePluginEntryPath = path.join(fixture.root, "src", "plugin-sdk", "plugin-entry.ts");
|
||||
const distPluginEntryPath = path.join(fixture.root, "dist", "plugin-sdk", "plugin-entry.js");
|
||||
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
||||
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
|
||||
fs.writeFileSync(
|
||||
sourcePluginEntryPath,
|
||||
"export const definePluginEntry = (entry) => entry;\n",
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
distPluginEntryPath,
|
||||
"export const definePluginEntry = (entry) => entry;\n",
|
||||
"utf-8",
|
||||
);
|
||||
return {
|
||||
fixture,
|
||||
sourceRootAlias,
|
||||
@@ -180,6 +193,8 @@ function createPluginSdkAliasTargetFixture(params?: {
|
||||
`channel-runtime${sourceChannelRuntimeExtension}`,
|
||||
),
|
||||
distChannelRuntimePath: path.join(fixture.root, "dist", "plugin-sdk", "channel-runtime.js"),
|
||||
sourcePluginEntryPath,
|
||||
distPluginEntryPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,16 +206,25 @@ function writePluginEntry(root: string, relativePath: string) {
|
||||
}
|
||||
|
||||
function createUserInstalledPluginSdkAliasFixture() {
|
||||
const { fixture, sourceRootAlias, sourceChannelRuntimePath } =
|
||||
const { fixture, sourcePluginEntryPath, sourceRootAlias, sourceChannelRuntimePath } =
|
||||
createPluginSdkAliasTargetFixture();
|
||||
const externalPluginRoot = path.join(makeTempDir(), ".openclaw", "extensions", "demo");
|
||||
const externalPluginEntry = path.join(externalPluginRoot, "index.ts");
|
||||
mkdirSafeDir(externalPluginRoot);
|
||||
fs.writeFileSync(externalPluginEntry, 'export const plugin = "demo";\n', "utf-8");
|
||||
fs.writeFileSync(
|
||||
externalPluginEntry,
|
||||
[
|
||||
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
|
||||
'export default definePluginEntry({ id: "demo", register() {} });',
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
return {
|
||||
externalPluginEntry,
|
||||
externalPluginRoot,
|
||||
fixture,
|
||||
sourcePluginEntryPath,
|
||||
sourceRootAlias,
|
||||
sourceChannelRuntimePath,
|
||||
};
|
||||
@@ -251,6 +275,7 @@ function expectPluginSdkAliasTargets(
|
||||
params: {
|
||||
rootAliasPath: string;
|
||||
channelRuntimePath?: string;
|
||||
pluginEntryPath?: string;
|
||||
},
|
||||
) {
|
||||
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
||||
@@ -267,6 +292,14 @@ function expectPluginSdkAliasTargets(
|
||||
fs.realpathSync(params.channelRuntimePath),
|
||||
);
|
||||
}
|
||||
if (params.pluginEntryPath) {
|
||||
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/plugin-entry"] ?? "")).toBe(
|
||||
fs.realpathSync(params.pluginEntryPath),
|
||||
);
|
||||
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk/plugin-entry"] ?? "")).toBe(
|
||||
fs.realpathSync(params.pluginEntryPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function expectPluginSdkAliasResolution(params: {
|
||||
@@ -798,6 +831,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
externalPluginEntry,
|
||||
externalPluginRoot,
|
||||
fixture,
|
||||
sourcePluginEntryPath,
|
||||
sourceRootAlias,
|
||||
sourceChannelRuntimePath,
|
||||
} = createUserInstalledPluginSdkAliasFixture();
|
||||
@@ -811,6 +845,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
expectPluginSdkAliasTargets(aliases, {
|
||||
rootAliasPath: sourceRootAlias,
|
||||
channelRuntimePath: sourceChannelRuntimePath,
|
||||
pluginEntryPath: sourcePluginEntryPath,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -819,6 +854,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
externalPluginEntry,
|
||||
externalPluginRoot,
|
||||
fixture,
|
||||
sourcePluginEntryPath,
|
||||
sourceRootAlias,
|
||||
sourceChannelRuntimePath,
|
||||
} = createUserInstalledPluginSdkAliasFixture();
|
||||
@@ -850,6 +886,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
expectPluginSdkAliasTargets(aliases, {
|
||||
rootAliasPath: sourceRootAlias,
|
||||
channelRuntimePath: sourceChannelRuntimePath,
|
||||
pluginEntryPath: sourcePluginEntryPath,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -121,6 +121,29 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
expect(sweepScript).toContain("scan_logs_for_unexpected_errors");
|
||||
});
|
||||
|
||||
it("keeps the generic plugin Docker lane as an external install contract canary", () => {
|
||||
const lane = findLaneByName("plugins");
|
||||
const sweepScript = readFileSync("scripts/e2e/lib/plugins/sweep.sh", "utf8");
|
||||
const clawhubScript = readFileSync("scripts/e2e/lib/plugins/clawhub.sh", "utf8");
|
||||
const assertionsScript = readFileSync("scripts/e2e/lib/plugins/assertions.mjs", "utf8");
|
||||
const prereleasePlan = createPluginPrereleaseTestPlan();
|
||||
|
||||
expect(lane).toEqual(
|
||||
expect.objectContaining({
|
||||
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins",
|
||||
name: "plugins",
|
||||
resources: expect.arrayContaining(["npm"]),
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
);
|
||||
expect(prereleasePlan.surfaces).toContain("external-install-boundary");
|
||||
expect(sweepScript).toContain("run_plugins_clawhub_scenario");
|
||||
expect(clawhubScript).toContain('plugins install "$CLAWHUB_PLUGIN_SPEC"');
|
||||
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
|
||||
expect(assertionsScript).toContain('node_modules", "openclaw');
|
||||
expect(assertionsScript).toContain('node_modules", "is-number');
|
||||
});
|
||||
|
||||
it("wires the full plugin prerelease plan into its release workflow", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const preflight = workflow.jobs.preflight;
|
||||
|
||||
Reference in New Issue
Block a user