mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
fix(plugins): ignore invalid managed runtime shadows
This commit is contained in:
@@ -748,7 +748,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] });
|
||||
});
|
||||
|
||||
it("warns but still loads source-only TypeScript entries for installed package plugins", async () => {
|
||||
it("skips source-only TypeScript entries for installed package plugins", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "extensions", "source-only-pack");
|
||||
mkdirSafe(path.join(pluginDir, "src"));
|
||||
@@ -762,17 +762,66 @@ describe("discoverOpenClawPlugins", () => {
|
||||
|
||||
const result = await discoverWithStateDir(stateDir, {});
|
||||
|
||||
expectCandidateIds(result.candidates, { includes: ["source-only-pack"] });
|
||||
expectCandidateIds(result.candidates, { excludes: ["source-only-pack"] });
|
||||
expect(
|
||||
result.diagnostics.some(
|
||||
(entry) =>
|
||||
entry.level === "warn" &&
|
||||
entry.pluginId === "source-only-pack" &&
|
||||
entry.message.includes("requires compiled runtime output") &&
|
||||
entry.message.includes("./dist/index.js"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("lets a valid bundled plugin win when a managed package is source-only TypeScript", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const bundledDir = path.join(stateDir, "bundled");
|
||||
const bundledPluginDir = path.join(bundledDir, "discord");
|
||||
const installedPluginDir = path.join(stateDir, "extensions", "discord");
|
||||
mkdirSafe(bundledPluginDir);
|
||||
mkdirSafe(path.join(installedPluginDir, "src"));
|
||||
|
||||
writePluginPackageManifest({
|
||||
packageDir: bundledPluginDir,
|
||||
packageName: "@openclaw/discord",
|
||||
extensions: ["./index.js"],
|
||||
});
|
||||
writePluginManifest({ pluginDir: bundledPluginDir, id: "discord" });
|
||||
writePluginEntry(path.join(bundledPluginDir, "index.js"));
|
||||
|
||||
writePluginPackageManifest({
|
||||
packageDir: installedPluginDir,
|
||||
packageName: "@openclaw/discord",
|
||||
extensions: ["./src/index.ts"],
|
||||
});
|
||||
writePluginManifest({ pluginDir: installedPluginDir, id: "discord" });
|
||||
writePluginEntry(path.join(installedPluginDir, "src", "index.ts"));
|
||||
|
||||
const result = discoverOpenClawPlugins({
|
||||
env: buildDiscoveryEnvWithOverrides(stateDir, {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
|
||||
}),
|
||||
});
|
||||
|
||||
const discordCandidates = result.candidates.filter(
|
||||
(candidate) => candidate.idHint === "discord",
|
||||
);
|
||||
expect(discordCandidates).toEqual([
|
||||
expect.objectContaining({
|
||||
origin: "bundled",
|
||||
source: fs.realpathSync(path.join(bundledPluginDir, "index.js")),
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
result.diagnostics.some(
|
||||
(entry) =>
|
||||
entry.pluginId === "discord" &&
|
||||
entry.message.includes("requires compiled runtime output"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("reuses one filesystem realpath lookup per package root within a discovery run", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const packageDir = path.join(stateDir, "extensions", "pack");
|
||||
|
||||
@@ -487,6 +487,26 @@ function deriveIdHint(params: {
|
||||
return `${normalizedPackageId}/${base}`;
|
||||
}
|
||||
|
||||
function derivePackagePluginIdHint(params: {
|
||||
manifestId?: string;
|
||||
packageName?: string;
|
||||
}): string | undefined {
|
||||
const rawManifestId = params.manifestId?.trim();
|
||||
if (rawManifestId) {
|
||||
return rawManifestId;
|
||||
}
|
||||
const rawPackageName = params.packageName?.trim();
|
||||
if (!rawPackageName) {
|
||||
return undefined;
|
||||
}
|
||||
const unscoped = rawPackageName.includes("/")
|
||||
? (rawPackageName.split("/").pop() ?? rawPackageName)
|
||||
: rawPackageName;
|
||||
return unscoped.endsWith("-provider") && unscoped.length > "-provider".length
|
||||
? unscoped.slice(0, -"-provider".length)
|
||||
: unscoped;
|
||||
}
|
||||
|
||||
function resolveIdHintManifestId(
|
||||
rootDir: string,
|
||||
rejectHardlinks: boolean,
|
||||
@@ -706,6 +726,7 @@ function discoverInDirectory(params: {
|
||||
manifest,
|
||||
extensions,
|
||||
origin: params.origin,
|
||||
pluginIdHint: derivePackagePluginIdHint({ manifestId, packageName: manifest?.name }),
|
||||
sourceLabel: fullPath,
|
||||
diagnostics: params.diagnostics,
|
||||
rejectHardlinks,
|
||||
@@ -911,6 +932,7 @@ function discoverFromPath(params: {
|
||||
manifest,
|
||||
extensions,
|
||||
origin: params.origin,
|
||||
pluginIdHint: derivePackagePluginIdHint({ manifestId, packageName: manifest?.name }),
|
||||
sourceLabel: resolved,
|
||||
diagnostics: params.diagnostics,
|
||||
rejectHardlinks,
|
||||
|
||||
@@ -63,6 +63,7 @@ function createPluginCandidate(params: {
|
||||
format?: "openclaw" | "bundle";
|
||||
bundleFormat?: "codex" | "claude" | "cursor";
|
||||
packageName?: string;
|
||||
packageVersion?: string;
|
||||
packageManifest?: OpenClawPackageManifest;
|
||||
packageDir?: string;
|
||||
bundledManifest?: PluginCandidate["bundledManifest"];
|
||||
@@ -76,6 +77,7 @@ function createPluginCandidate(params: {
|
||||
format: params.format,
|
||||
bundleFormat: params.bundleFormat,
|
||||
packageName: params.packageName,
|
||||
packageVersion: params.packageVersion,
|
||||
packageManifest: params.packageManifest,
|
||||
packageDir: params.packageDir,
|
||||
bundledManifest: params.bundledManifest,
|
||||
@@ -1945,6 +1947,33 @@ describe("loadPluginManifestRegistry", () => {
|
||||
expect(countDuplicateWarnings(loadRegistry(candidates))).toBe(0);
|
||||
});
|
||||
|
||||
it("suppresses duplicate warning when global candidates come from the same package artifact", () => {
|
||||
const firstDir = makeTempDir();
|
||||
const secondDir = makeTempDir();
|
||||
const manifest = { id: "opik-openclaw", configSchema: { type: "object" } };
|
||||
writeManifest(firstDir, manifest);
|
||||
writeManifest(secondDir, manifest);
|
||||
|
||||
const candidates: PluginCandidate[] = [
|
||||
createPluginCandidate({
|
||||
idHint: "opik-openclaw",
|
||||
rootDir: firstDir,
|
||||
origin: "global",
|
||||
packageName: "@opik/opik-openclaw",
|
||||
packageVersion: "0.2.14",
|
||||
}),
|
||||
createPluginCandidate({
|
||||
idHint: "opik-openclaw",
|
||||
rootDir: secondDir,
|
||||
origin: "global",
|
||||
packageName: "@opik/opik-openclaw",
|
||||
packageVersion: "0.2.14",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(countDuplicateWarnings(loadRegistry(candidates))).toBe(0);
|
||||
});
|
||||
|
||||
it("does not warn for id hint mismatches when manifest id is authoritative", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, { id: "openai", configSchema: { type: "object" } });
|
||||
|
||||
@@ -713,6 +713,22 @@ function isIntentionalInstalledBundledDuplicate(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function isSameGlobalPackageDuplicate(left: PluginCandidate, right: PluginCandidate): boolean {
|
||||
if (left.origin !== "global" || right.origin !== "global") {
|
||||
return false;
|
||||
}
|
||||
const leftPackageName = normalizeOptionalString(left.packageName);
|
||||
const rightPackageName = normalizeOptionalString(right.packageName);
|
||||
if (!leftPackageName || leftPackageName !== rightPackageName) {
|
||||
return false;
|
||||
}
|
||||
const leftPackageVersion = normalizeOptionalString(left.packageVersion);
|
||||
const rightPackageVersion = normalizeOptionalString(right.packageVersion);
|
||||
return Boolean(
|
||||
leftPackageVersion && rightPackageVersion && leftPackageVersion === rightPackageVersion,
|
||||
);
|
||||
}
|
||||
|
||||
export function loadPluginManifestRegistry(
|
||||
params: {
|
||||
config?: OpenClawConfig;
|
||||
@@ -906,6 +922,9 @@ export function loadPluginManifestRegistry(
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (isSameGlobalPackageDuplicate(candidate, existing.candidate)) {
|
||||
continue;
|
||||
}
|
||||
diagnostics.push({
|
||||
level: "warn",
|
||||
pluginId: manifest.id,
|
||||
|
||||
@@ -456,6 +456,7 @@ function resolvePackageRuntimeEntrySource(params: {
|
||||
entryPath: string;
|
||||
runtimeEntryPath?: string;
|
||||
runtimeEntryLabel?: string;
|
||||
pluginIdHint?: string;
|
||||
origin: PluginOrigin;
|
||||
sourceLabel: string;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
@@ -523,6 +524,7 @@ function resolvePackageRuntimeEntrySource(params: {
|
||||
) {
|
||||
params.diagnostics.push({
|
||||
level: "warn",
|
||||
...(params.pluginIdHint ? { pluginId: params.pluginIdHint } : {}),
|
||||
message: missingCompiledRuntimeEntryMessage({
|
||||
label: "installed plugin package",
|
||||
entry: safeEntry.relativePath,
|
||||
@@ -530,6 +532,7 @@ function resolvePackageRuntimeEntrySource(params: {
|
||||
}),
|
||||
source: params.sourceLabel,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +574,7 @@ export function resolvePackageSetupSource(params: {
|
||||
entryPath: setupEntryPath,
|
||||
runtimeEntryPath: normalizeOptionalString(packageManifest?.runtimeSetupEntry),
|
||||
runtimeEntryLabel: "runtime setup entry",
|
||||
pluginIdHint: packageManifest?.plugin?.id ?? packageManifest?.channel?.id,
|
||||
origin: params.origin,
|
||||
sourceLabel: params.sourceLabel,
|
||||
diagnostics: params.diagnostics,
|
||||
@@ -584,6 +588,7 @@ export function resolvePackageRuntimeExtensionSources(params: {
|
||||
manifest: PackageManifest | null;
|
||||
extensions: readonly string[];
|
||||
origin: PluginOrigin;
|
||||
pluginIdHint?: string;
|
||||
sourceLabel: string;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
rejectHardlinks?: boolean;
|
||||
@@ -610,6 +615,7 @@ export function resolvePackageRuntimeExtensionSources(params: {
|
||||
entryPath,
|
||||
runtimeEntryPath: runtimeResolution.runtimeExtensions[index],
|
||||
runtimeEntryLabel: "runtime extension entry",
|
||||
pluginIdHint: params.pluginIdHint,
|
||||
origin: params.origin,
|
||||
sourceLabel: params.sourceLabel,
|
||||
diagnostics: params.diagnostics,
|
||||
|
||||
Reference in New Issue
Block a user