Skip to content

Commit 12685ee

Browse files
authored
Add hosted catalog snapshot fallback (#95877)
Merged via squash. Prepared head SHA: 5f71635 Co-authored-by: giodl73-repo <[email protected]> Co-authored-by: giodl73-repo <[email protected]> Reviewed-by: @giodl73-repo
1 parent 3672201 commit 12685ee

2 files changed

Lines changed: 482 additions & 23 deletions

File tree

src/plugins/official-external-plugin-catalog.test.ts

Lines changed: 303 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { describe, expect, it, vi } from "vitest";
33
import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
44
import {
55
type OfficialExternalPluginCatalogEntry,
6+
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
7+
createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore,
68
getOfficialExternalPluginCatalogEntry,
79
isOfficialExternalPluginCatalogFeed,
810
listOfficialExternalPluginCatalogEntries,
@@ -342,7 +344,7 @@ describe("official external plugin catalog", () => {
342344

343345
expect(result.source).toBe("bundled-fallback");
344346
if (result.source === "bundled-fallback") {
345-
expect(result.error).toContain("without a cached snapshot");
347+
expect(result.error).toContain("HTTP 304");
346348
expect(result.metadata).toMatchObject({
347349
status: 304,
348350
etag: '"same"',
@@ -351,6 +353,306 @@ describe("official external plugin catalog", () => {
351353
}
352354
});
353355

356+
it("writes a validated hosted feed snapshot after a successful fetch", async () => {
357+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore();
358+
const writeSpy = vi.spyOn(snapshotStore, "write");
359+
const body = JSON.stringify({
360+
schemaVersion: 1,
361+
id: "openclaw-official-external-plugins",
362+
generatedAt: "2026-06-22T00:00:00.000Z",
363+
sequence: 3,
364+
entries: [
365+
{
366+
name: "@openclaw/snapshot-write-proof",
367+
kind: "plugin",
368+
openclaw: { plugin: { id: "snapshot-write-proof" } },
369+
},
370+
],
371+
});
372+
373+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
374+
snapshotStore,
375+
now: () => new Date("2026-06-22T01:02:03.000Z"),
376+
fetchImpl: vi.fn(
377+
async () =>
378+
new Response(body, {
379+
status: 200,
380+
headers: { etag: '"fresh"' },
381+
}),
382+
),
383+
});
384+
385+
expect(result.source).toBe("hosted");
386+
expect(writeSpy).toHaveBeenCalledTimes(1);
387+
const snapshot = await snapshotStore.read(
388+
result.source === "hosted" ? result.metadata.url : "",
389+
);
390+
expect(snapshot).toMatchObject({
391+
body,
392+
savedAt: "2026-06-22T01:02:03.000Z",
393+
metadata: { etag: '"fresh"' },
394+
});
395+
expect(snapshot?.metadata.checksum).toMatch(/^sha256:[0-9a-f]{64}$/);
396+
});
397+
398+
it("uses the last known good snapshot when the hosted feed returns HTTP 304", async () => {
399+
const body = JSON.stringify({
400+
schemaVersion: 1,
401+
id: "openclaw-official-external-plugins",
402+
generatedAt: "2026-06-22T00:00:00.000Z",
403+
sequence: 4,
404+
entries: [
405+
{
406+
name: "@openclaw/snapshot-proof",
407+
kind: "plugin",
408+
openclaw: { plugin: { id: "snapshot-proof" } },
409+
},
410+
],
411+
});
412+
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
413+
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
414+
fetchImpl: vi.fn(
415+
async () =>
416+
new Response(body, {
417+
status: 200,
418+
headers: { etag: '"snapshot-v1"' },
419+
}),
420+
),
421+
});
422+
if (seeded.source !== "hosted") {
423+
throw new Error("expected seeded hosted feed");
424+
}
425+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
426+
{
427+
body,
428+
metadata: seeded.metadata,
429+
savedAt: "2026-06-22T01:02:03.000Z",
430+
},
431+
]);
432+
433+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
434+
snapshotStore,
435+
ifNoneMatch: '"snapshot-v1"',
436+
fetchImpl: vi.fn(
437+
async () =>
438+
new Response(null, {
439+
status: 304,
440+
headers: { etag: '"snapshot-v1"' },
441+
}),
442+
),
443+
});
444+
445+
expect(result.source).toBe("hosted-snapshot");
446+
expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/snapshot-proof"]);
447+
if (result.source === "hosted-snapshot") {
448+
expect(result.error).toContain("HTTP 304");
449+
expect(result.snapshot.savedAt).toBe("2026-06-22T01:02:03.000Z");
450+
expect(result.metadata.checksum).toBe(seeded.metadata.checksum);
451+
}
452+
});
453+
454+
it("does not use a stale snapshot when HTTP 304 validators do not match", async () => {
455+
const body = JSON.stringify({
456+
schemaVersion: 1,
457+
id: "openclaw-official-external-plugins",
458+
generatedAt: "2026-06-22T00:00:00.000Z",
459+
sequence: 4,
460+
entries: [
461+
{
462+
name: "@openclaw/stale-snapshot-proof",
463+
kind: "plugin",
464+
openclaw: { plugin: { id: "stale-snapshot-proof" } },
465+
},
466+
],
467+
});
468+
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
469+
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
470+
fetchImpl: vi.fn(
471+
async () =>
472+
new Response(body, {
473+
status: 200,
474+
headers: { etag: '"snapshot-v1"' },
475+
}),
476+
),
477+
});
478+
if (seeded.source !== "hosted") {
479+
throw new Error("expected seeded hosted feed");
480+
}
481+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
482+
{
483+
body,
484+
metadata: seeded.metadata,
485+
savedAt: "2026-06-22T01:02:03.000Z",
486+
},
487+
]);
488+
489+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
490+
snapshotStore,
491+
ifNoneMatch: '"snapshot-v2"',
492+
fetchImpl: vi.fn(
493+
async () =>
494+
new Response(null, {
495+
status: 304,
496+
headers: { etag: '"snapshot-v2"' },
497+
}),
498+
),
499+
});
500+
501+
expect(result.source).toBe("bundled-fallback");
502+
if (result.source === "bundled-fallback") {
503+
expect(result.error).toContain("snapshot fallback failed");
504+
expect(result.error).toContain("ETag");
505+
}
506+
});
507+
508+
it("uses a valid snapshot before bundled fallback when hosted validation fails", async () => {
509+
const body = JSON.stringify({
510+
schemaVersion: 1,
511+
id: "openclaw-official-external-plugins",
512+
generatedAt: "2026-06-22T00:00:00.000Z",
513+
sequence: 5,
514+
entries: [
515+
{
516+
name: "@openclaw/snapshot-validation-proof",
517+
kind: "plugin",
518+
openclaw: { plugin: { id: "snapshot-validation-proof" } },
519+
},
520+
],
521+
});
522+
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
523+
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
524+
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
525+
});
526+
if (seeded.source !== "hosted") {
527+
throw new Error("expected seeded hosted feed");
528+
}
529+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
530+
{ body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" },
531+
]);
532+
533+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
534+
snapshotStore,
535+
fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })),
536+
});
537+
538+
expect(result.source).toBe("hosted-snapshot");
539+
expect(result.entries.map((entry) => entry.name)).toEqual([
540+
"@openclaw/snapshot-validation-proof",
541+
]);
542+
if (result.source === "hosted-snapshot") {
543+
expect(result.error).toContain("JSON");
544+
}
545+
});
546+
547+
it("does not use a stale snapshot when hosted validation fails with unmatched validators", async () => {
548+
const body = JSON.stringify({
549+
schemaVersion: 1,
550+
id: "openclaw-official-external-plugins",
551+
generatedAt: "2026-06-22T00:00:00.000Z",
552+
sequence: 5,
553+
entries: [
554+
{
555+
name: "@openclaw/stale-validation-snapshot-proof",
556+
kind: "plugin",
557+
openclaw: { plugin: { id: "stale-validation-snapshot-proof" } },
558+
},
559+
],
560+
});
561+
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
562+
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
563+
fetchImpl: vi.fn(
564+
async () => new Response(body, { status: 200, headers: { etag: '"snapshot-v1"' } }),
565+
),
566+
});
567+
if (seeded.source !== "hosted") {
568+
throw new Error("expected seeded hosted feed");
569+
}
570+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
571+
{ body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" },
572+
]);
573+
574+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
575+
snapshotStore,
576+
ifNoneMatch: '"snapshot-v2"',
577+
fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })),
578+
});
579+
580+
expect(result.source).toBe("bundled-fallback");
581+
if (result.source === "bundled-fallback") {
582+
expect(result.error).toContain("snapshot fallback failed");
583+
expect(result.error).toContain("ETag");
584+
}
585+
});
586+
587+
it("falls back to bundled entries when the snapshot is invalid", async () => {
588+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
589+
{
590+
body: JSON.stringify({
591+
schemaVersion: 1,
592+
id: "openclaw-official-external-plugins",
593+
generatedAt: "2026-06-22T00:00:00.000Z",
594+
sequence: 1,
595+
entries: [],
596+
}),
597+
metadata: {
598+
url: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
599+
status: 200,
600+
checksum: "sha256:not-current",
601+
},
602+
savedAt: "2026-06-22T01:02:03.000Z",
603+
},
604+
]);
605+
606+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
607+
snapshotStore,
608+
fetchImpl: vi.fn(async () => new Response(null, { status: 304 })),
609+
});
610+
611+
expect(result.source).toBe("bundled-fallback");
612+
if (result.source === "bundled-fallback") {
613+
expect(result.error).toContain("snapshot fallback failed");
614+
expect(result.error).toContain("checksum mismatch");
615+
}
616+
});
617+
618+
it("does not use a snapshot that violates the expected checksum", async () => {
619+
const body = JSON.stringify({
620+
schemaVersion: 1,
621+
id: "openclaw-official-external-plugins",
622+
generatedAt: "2026-06-22T00:00:00.000Z",
623+
sequence: 6,
624+
entries: [
625+
{
626+
name: "@openclaw/snapshot-pin-proof",
627+
kind: "plugin",
628+
openclaw: { plugin: { id: "snapshot-pin-proof" } },
629+
},
630+
],
631+
});
632+
const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
633+
snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(),
634+
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
635+
});
636+
if (seeded.source !== "hosted") {
637+
throw new Error("expected seeded hosted feed");
638+
}
639+
const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([
640+
{ body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" },
641+
]);
642+
643+
const result = await loadHostedOfficialExternalPluginCatalogEntries({
644+
snapshotStore,
645+
expectedSha256: "sha256:not-current",
646+
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
647+
});
648+
649+
expect(result.source).toBe("bundled-fallback");
650+
if (result.source === "bundled-fallback") {
651+
expect(result.error).toContain("snapshot fallback failed");
652+
expect(result.error).toContain("expected checksum");
653+
}
654+
});
655+
354656
it("falls back to the bundled catalog on checksum mismatch and oversized bodies", async () => {
355657
const mismatch = await loadHostedOfficialExternalPluginCatalogEntries({
356658
expectedSha256: "sha256:not-current",

0 commit comments

Comments
 (0)