mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
fix(plugins): link host declared as direct dependency (#117738)
This commit is contained in:
committed by
GitHub
parent
78193deff3
commit
b2da7aa106
@@ -196,7 +196,7 @@ export async function validatePackagePluginInstallSource(params: {
|
||||
? { setup: ocManifestResult.manifest.setup }
|
||||
: {}),
|
||||
hasRuntimeDependencies: hasPackageRuntimeDependencies(manifest),
|
||||
peerDependencies: manifest.peerDependencies ?? {},
|
||||
peerDependencies: { ...manifest.dependencies, ...manifest.peerDependencies },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+78
-50
@@ -3820,6 +3820,26 @@ describe("installPluginFromDir", () => {
|
||||
|
||||
describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
const resolveRootMock = vi.mocked(resolveOpenClawPackageRootSync);
|
||||
const hostDependencyDeclarations: Array<{
|
||||
declaration: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
dependencies?: Record<string, string>;
|
||||
}> = [
|
||||
{
|
||||
declaration: "peerDependencies",
|
||||
peerDependencies: { openclaw: "*" },
|
||||
},
|
||||
{
|
||||
declaration: "dependencies",
|
||||
peerDependencies: {},
|
||||
dependencies: { openclaw: "*" },
|
||||
},
|
||||
{
|
||||
declaration: "dependencies alongside an unrelated peer dependency",
|
||||
peerDependencies: { "unrelated-host": "^1.0.0" },
|
||||
dependencies: { openclaw: "*" },
|
||||
},
|
||||
];
|
||||
|
||||
function writePluginWithPeerDeps(
|
||||
pluginDir: string,
|
||||
@@ -3841,27 +3861,29 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n", "utf-8");
|
||||
}
|
||||
|
||||
it("creates a node_modules/openclaw symlink when peerDependencies declares openclaw", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
const run = vi.mocked(runCommandWithTimeout);
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
it.each(hostDependencyDeclarations)(
|
||||
"creates a host-targeted node_modules/openclaw symlink for $declaration",
|
||||
async ({ peerDependencies, dependencies }) => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
const run = vi.mocked(runCommandWithTimeout);
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
|
||||
writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
|
||||
writePluginWithPeerDeps(pluginDir, peerDependencies, dependencies);
|
||||
|
||||
const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw");
|
||||
const stat = fs.lstatSync(symlinkPath);
|
||||
expect(stat.isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw");
|
||||
expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the openclaw peer symlink when a local plugin already has dependencies", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
@@ -3890,33 +3912,36 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces a copied local openclaw package with the host peer symlink", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
it.each(hostDependencyDeclarations)(
|
||||
"replaces a copied local openclaw package with the host symlink for $declaration",
|
||||
async ({ peerDependencies, dependencies }) => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
|
||||
writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
|
||||
fs.mkdirSync(path.join(pluginDir, "node_modules", "openclaw"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "node_modules", "openclaw", "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.5.31" }),
|
||||
"utf-8",
|
||||
);
|
||||
writePluginWithPeerDeps(pluginDir, peerDependencies, dependencies);
|
||||
fs.mkdirSync(path.join(pluginDir, "node_modules", "openclaw"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "node_modules", "openclaw", "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.5.31" }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(warnings).toHaveLength(0);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.ok).toBe(true);
|
||||
expect(warnings).toHaveLength(0);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw");
|
||||
expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
|
||||
});
|
||||
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 () => {
|
||||
it("does not create a symlink when neither dependency map declares openclaw", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
resolveRootMock.mockReturnValue(suiteTempRootTracker.makeTempDir());
|
||||
|
||||
@@ -3961,19 +3986,22 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects when resolveOpenClawPackageRootSync returns null", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
resolveRootMock.mockReturnValue(null);
|
||||
it.each(hostDependencyDeclarations)(
|
||||
"rejects $declaration when the host package root cannot be resolved",
|
||||
async ({ peerDependencies, dependencies }) => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
resolveRootMock.mockReturnValue(null);
|
||||
|
||||
writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
|
||||
writePluginWithPeerDeps(pluginDir, peerDependencies, dependencies);
|
||||
|
||||
const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("plugin-local node_modules/openclaw link");
|
||||
}
|
||||
expectWarningIncludes(warnings, "Could not locate openclaw package root");
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("plugin-local node_modules/openclaw link");
|
||||
}
|
||||
expectWarningIncludes(warnings, "Could not locate openclaw package root");
|
||||
},
|
||||
);
|
||||
});
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
Reference in New Issue
Block a user