fix(plugins): preserve hosted catalog metadata entries

This commit is contained in:
Gio Della-Libera
2026-06-27 12:38:55 -07:00
parent 47ce2ac9a6
commit 76da9328af
2 changed files with 72 additions and 2 deletions
@@ -258,6 +258,59 @@ describe("official external plugin catalog", () => {
}
});
it("keeps live ClawHub metadata-only entries after hosted feed loading", async () => {
const body = JSON.stringify({
schemaVersion: 2,
id: "clawhub-official",
generatedAt: "2026-06-25T01:19:39.629Z",
sequence: 11,
entries: [
{
type: "plugin",
id: "@expediagroup/expedia-openclaw",
title: "Expedia Travel",
version: "1.0.4",
state: "available",
publisher: {
id: "expediagroup",
trust: "official",
},
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@expediagroup/expedia-openclaw",
version: "1.0.4",
integrity:
"sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9",
},
],
},
},
],
});
const result = await loadHostedOfficialExternalPluginCatalogEntries({
fetchImpl: vi.fn(
async () =>
new Response(body, {
status: 200,
headers: {
"content-length": String(new TextEncoder().encode(body).byteLength),
},
}),
),
});
expect(result.source).toBe("hosted");
expect(result.entries).toHaveLength(1);
expect(result.entries[0]).toMatchObject({
id: "@expediagroup/expedia-openclaw",
title: "Expedia Travel",
version: "1.0.4",
});
});
it("falls back to the bundled catalog when hosted feed validation fails", async () => {
const result = await loadHostedOfficialExternalPluginCatalogEntries({
fetchImpl: vi.fn(
@@ -343,8 +343,7 @@ function dedupeOfficialExternalPluginCatalogEntries(
): OfficialExternalPluginCatalogEntry[] {
const resolved = new Map<string, OfficialExternalPluginCatalogEntry>();
for (const entry of entries) {
const pluginId = resolveOfficialExternalPluginId(entry);
const key = pluginId ? `${entry.kind ?? "plugin"}:${pluginId}` : (entry.name ?? "");
const key = resolveOfficialExternalPluginCatalogEntryKey(entry);
if (key && !resolved.has(key)) {
resolved.set(key, entry);
}
@@ -352,6 +351,24 @@ function dedupeOfficialExternalPluginCatalogEntries(
return [...resolved.values()];
}
function resolveOfficialExternalPluginCatalogEntryKey(
entry: OfficialExternalPluginCatalogEntry,
): string | undefined {
const pluginId = resolveOfficialExternalPluginId(entry);
if (pluginId) {
return `${normalizeOptionalString(entry.kind) ?? "plugin"}:${pluginId}`;
}
const name = normalizeOptionalString(entry.name);
if (name) {
return name;
}
const id = normalizeOptionalString(entry.id);
if (id) {
return `${normalizeOptionalString(entry.kind) ?? normalizeOptionalString(entry.type) ?? "plugin"}:${id}`;
}
return undefined;
}
function bundledFallbackResult(
error: unknown,
metadata?: HostedOfficialExternalPluginCatalogLoadResult["metadata"],