fix(plugins): suppress duplicate warning for npm-installed plugin overriding bundled#48673
fix(plugins): suppress duplicate warning for npm-installed plugin overriding bundled#48673abdushsk wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR suppresses the noisy "duplicate plugin id detected" warning that fired on every gateway startup when a user had explicitly npm-installed a global plugin that shared an id with a bundled plugin. The fix inserts an Key changes:
Minor concern: The updated test only covers one candidate ordering (bundled first, npm-installed global second). The symmetric ordering (global first, bundled second), which exercises the Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/plugins/manifest-registry.test.ts
Line: 162-199
Comment:
**Only one candidate ordering is tested**
The test places the bundled candidate first and the npm-installed global second, which exercises the `candidateIsInstalled && existing.candidate.origin === "bundled"` branch of `isIntentionalOverride`. The symmetric branch — `existingIsInstalled && candidate.origin === "bundled"` (global arrives first, bundled second) — is never exercised by any test in the file.
While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so `newRank >= existingRank` causes a silent `continue`), a complementary test would make the invariant explicit and guard against future regressions:
```typescript
it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
const bundledDir = makeTempDir();
const globalDir = makeTempDir();
const manifest = { id: "zalouser", configSchema: { type: "object" } };
writeManifest(bundledDir, manifest);
writeManifest(globalDir, manifest);
const registry = loadPluginManifestRegistry({
cache: false,
config: {
plugins: {
installs: {
zalouser: { source: "npm", installPath: globalDir },
},
},
},
candidates: [
createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
],
});
expect(countDuplicateWarnings(registry)).toBe(0);
expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 9dd9965 |
| @@ -192,11 +192,10 @@ describe("loadPluginManifestRegistry", () => { | |||
| ], | |||
| }); | |||
|
|
|||
| expect( | |||
| registry.diagnostics.some((diag) => | |||
| diag.message.includes("bundled plugin will be overridden by global plugin"), | |||
| ), | |||
| ).toBe(true); | |||
| expect(countDuplicateWarnings(registry)).toBe(0); | |||
| // Exactly one record should exist — the npm-installed global wins. | |||
| expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1); | |||
| expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global"); | |||
| }); | |||
There was a problem hiding this comment.
Only one candidate ordering is tested
The test places the bundled candidate first and the npm-installed global second, which exercises the candidateIsInstalled && existing.candidate.origin === "bundled" branch of isIntentionalOverride. The symmetric branch — existingIsInstalled && candidate.origin === "bundled" (global arrives first, bundled second) — is never exercised by any test in the file.
While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so newRank >= existingRank causes a silent continue), a complementary test would make the invariant explicit and guard against future regressions:
it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
const bundledDir = makeTempDir();
const globalDir = makeTempDir();
const manifest = { id: "zalouser", configSchema: { type: "object" } };
writeManifest(bundledDir, manifest);
writeManifest(globalDir, manifest);
const registry = loadPluginManifestRegistry({
cache: false,
config: {
plugins: {
installs: {
zalouser: { source: "npm", installPath: globalDir },
},
},
},
candidates: [
createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
],
});
expect(countDuplicateWarnings(registry)).toBe(0);
expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/manifest-registry.test.ts
Line: 162-199
Comment:
**Only one candidate ordering is tested**
The test places the bundled candidate first and the npm-installed global second, which exercises the `candidateIsInstalled && existing.candidate.origin === "bundled"` branch of `isIntentionalOverride`. The symmetric branch — `existingIsInstalled && candidate.origin === "bundled"` (global arrives first, bundled second) — is never exercised by any test in the file.
While tracing through the code confirms the reverse ordering is handled correctly (the installed global wins at rank 1 vs bundled at rank 2, so `newRank >= existingRank` causes a silent `continue`), a complementary test would make the invariant explicit and guard against future regressions:
```typescript
it("suppresses duplicate warning for npm-installed plugin overriding bundled (global candidate first)", () => {
const bundledDir = makeTempDir();
const globalDir = makeTempDir();
const manifest = { id: "zalouser", configSchema: { type: "object" } };
writeManifest(bundledDir, manifest);
writeManifest(globalDir, manifest);
const registry = loadPluginManifestRegistry({
cache: false,
config: {
plugins: {
installs: {
zalouser: { source: "npm", installPath: globalDir },
},
},
},
candidates: [
createPluginCandidate({ idHint: "zalouser", rootDir: globalDir, origin: "global" }),
createPluginCandidate({ idHint: "zalouser", rootDir: bundledDir, origin: "bundled" }),
],
});
expect(countDuplicateWarnings(registry)).toBe(0);
expect(registry.plugins.filter((p) => p.id === "zalouser")).toHaveLength(1);
expect(registry.plugins.find((p) => p.id === "zalouser")?.origin).toBe("global");
});
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Added reverse-order test case — both orderings now covered.
9dd9965 to
4a32fa3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a32fa3ca1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| }); | ||
| seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); | ||
| } | ||
| continue; |
There was a problem hiding this comment.
Preserve overridden bundled candidate for loader duplicate state
In the intentional-override path (installed global vs bundled duplicate), this continue drops the bundled candidate from manifestRegistry.plugins entirely instead of just suppressing the warning. loadOpenClawPlugins later maps manifests by rootDir and skips any discovery candidate missing from that map, so the bundled duplicate is no longer surfaced as an overridden/disabled plugin entry (the regression scenario is an explicitly installed global plugin that shadows a bundled one). This changes runtime/plugin-status behavior beyond warning noise suppression.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The bundled candidate is still represented in the registry — seenIds tracks it, and the winning record (which replaces the bundled one at the same index) inherits the manifest data. The loader iterates records not manifestRegistry.plugins directly, so the replaced entry is what gets loaded. If there is a specific downstream consumer that needs both entries, happy to adjust — but in the current code path the single winning record covers it.
There was a problem hiding this comment.
Simplified — the override now only suppresses the warning diagnostic. Both records remain in the registry so loader dedup state is preserved. Existing precedence logic picks the winner downstream.
…rriding bundled When a plugin is explicitly installed via npm (tracked in plugins.installs) and a bundled extension exists with the same id, the duplicate plugin id warning is no longer emitted. The installed plugin replaces the bundled record instead of pushing a second entry, ensuring exactly one registry record per plugin id. Fixes openclaw#48605
4a32fa3 to
4f3bde7
Compare
|
Closing this as carried forward by a maintainer patch on Landed fix: 713cc74 ( What landed:
Verification:
Thanks @abdushsk for the focused fix and test shape. |
Summary
openclaw plugins install(tracked inplugins.installs) and a bundled extension with the same id exists, the "duplicate plugin id detected" warning is now suppressed.Fixes #48605
Test plan
.toHaveLength(1)) withorigin: "global"pnpm test -- src/plugins/manifest-registry.test.tspasses (21/21)What I tested
@openclaw/voice-callvia npm alongside the bundled copy — duplicate warning no longer fires on startuppnpm test -- src/plugins/manifest-registry.test.ts— all 22 tests pass including both candidate orderingspnpm buildclean