fix(doctor): reinstall missing configured plugin payloads

This commit is contained in:
Vincent Koc
2026-05-02 17:21:04 -07:00
parent 10c9200f75
commit 4026af1a8b
2 changed files with 124 additions and 11 deletions

View File

@@ -791,7 +791,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(result.changes).toEqual(['Repaired missing configured plugin "demo".']);
});
it("reinstalls a known configured plugin when its recorded install path is missing", async () => {
it("reinstalls a known configured plugin from the catalog when its recorded install path is missing", async () => {
const records = {
discord: {
source: "npm",
@@ -809,6 +809,109 @@ describe("repairMissingConfiguredPluginInstalls", () => {
],
diagnostics: [],
});
mocks.listChannelPluginCatalogEntries.mockReturnValue([
{
id: "discord",
pluginId: "discord",
meta: { label: "Discord" },
install: {
npmSpec: "@openclaw/discord",
},
},
]);
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
ok: true,
pluginId: "discord",
targetDir: "/tmp/openclaw-plugins/discord",
version: "1.2.3",
npmResolution: {
name: "@openclaw/discord",
version: "1.2.3",
resolvedSpec: "@openclaw/discord@1.2.3",
integrity: "sha512-discord",
resolvedAt: "2026-05-01T00:00:00.000Z",
},
});
mocks.updateNpmInstalledPlugins.mockResolvedValue({
changed: false,
config: {
plugins: {
installs: records,
},
},
outcomes: [
{
pluginId: "discord",
status: "skipped",
message: "No update applied.",
},
],
});
const { repairMissingConfiguredPluginInstalls } =
await import("./missing-configured-plugin-install.js");
const result = await repairMissingConfiguredPluginInstalls({
cfg: {
plugins: {
entries: {
discord: { enabled: true },
},
},
channels: {
discord: { enabled: true },
},
},
env: {},
});
expect(mocks.updateNpmInstalledPlugins).toHaveBeenCalledWith(
expect.objectContaining({
pluginIds: ["discord"],
config: expect.objectContaining({
plugins: expect.objectContaining({ installs: records }),
}),
}),
);
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/discord",
expectedPluginId: "discord",
}),
);
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
expect.objectContaining({
discord: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/discord" }),
}),
{ env: {} },
);
expect(result.changes).toEqual([
'Installed missing configured plugin "discord" from @openclaw/discord.',
]);
});
it("updates a known configured plugin when its installed manifest path still exists", async () => {
const records = {
discord: {
source: "npm",
spec: "@openclaw/discord",
installPath: process.cwd(),
},
};
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue(records);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
plugins: [
{
id: "discord",
channels: ["discord"],
},
],
diagnostics: [
{
pluginId: "discord",
message: "manifest without channelConfigs metadata",
},
],
});
mocks.updateNpmInstalledPlugins.mockResolvedValue({
changed: true,
config: {
@@ -817,7 +920,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
discord: {
source: "npm",
spec: "@openclaw/discord",
installPath: "/tmp/openclaw-plugins/discord",
installPath: process.cwd(),
},
},
},
@@ -857,7 +960,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
);
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
expect.objectContaining({
discord: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/discord" }),
discord: expect.objectContaining({ installPath: process.cwd() }),
}),
{ env: {} },
);
@@ -907,7 +1010,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
discord: {
source: "npm",
spec: "@openclaw/discord",
installPath: "/tmp/openclaw-plugins/discord",
installPath: process.cwd(),
},
},
},
@@ -949,7 +1052,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
);
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
expect.objectContaining({
discord: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/discord" }),
discord: expect.objectContaining({ installPath: process.cwd() }),
}),
{ env: {} },
);
@@ -999,7 +1102,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
brave: {
source: "npm",
spec: "@openclaw/brave-plugin@beta",
installPath: "/tmp/openclaw-plugins/brave",
installPath: process.cwd(),
},
},
},
@@ -1038,7 +1141,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
);
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
expect.objectContaining({
brave: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/brave" }),
brave: expect.objectContaining({ installPath: process.cwd() }),
}),
{ env: {} },
);

View File

@@ -495,9 +495,13 @@ async function repairMissingPluginInstalls(params: {
}
const missingPluginIds = new Set(
[...params.pluginIds].filter(
(pluginId) => !knownIds.has(pluginId) && !Object.hasOwn(nextRecords, pluginId),
),
[...params.pluginIds].filter((pluginId) => {
const hasRecord = Object.hasOwn(nextRecords, pluginId);
return (
(!knownIds.has(pluginId) && !hasRecord) ||
(hasRecord && isInstalledRecordMissingOnDisk(nextRecords[pluginId], env))
);
}),
);
for (const candidate of collectDownloadableInstallCandidates({
cfg: params.cfg,
@@ -507,7 +511,13 @@ async function repairMissingPluginInstalls(params: {
configuredChannelIds: params.channelIds,
blockedPluginIds: params.blockedPluginIds,
})) {
if (knownIds.has(candidate.pluginId) || Object.hasOwn(nextRecords, candidate.pluginId)) {
const hasUsableRecord =
Object.hasOwn(nextRecords, candidate.pluginId) &&
!isInstalledRecordMissingOnDisk(nextRecords[candidate.pluginId], env);
if (knownIds.has(candidate.pluginId) && hasUsableRecord) {
continue;
}
if (hasUsableRecord) {
continue;
}
const installed = await installCandidate({ candidate, records: nextRecords });