



























@@ -3,6 +3,8 @@ import { describe, expect, it, vi } from "vitest";
33import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
44import {
55type OfficialExternalPluginCatalogEntry,
6+DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
7+createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore,
68getOfficialExternalPluginCatalogEntry,
79isOfficialExternalPluginCatalogFeed,
810listOfficialExternalPluginCatalogEntries,
@@ -342,7 +344,7 @@ describe("official external plugin catalog", () => {
342344343345expect(result.source).toBe("bundled-fallback");
344346if (result.source === "bundled-fallback") {
345-expect(result.error).toContain("without a cached snapshot");
347+expect(result.error).toContain("HTTP 304");
346348expect(result.metadata).toMatchObject({
347349status: 304,
348350etag: '"same"',
@@ -351,6 +353,306 @@ describe("official external plugin catalog", () => {
351353}
352354});
353355356+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+354656it("falls back to the bundled catalog on checksum mismatch and oversized bodies", async () => {
355657const mismatch = await loadHostedOfficialExternalPluginCatalogEntries({
356658expectedSha256: "sha256:not-current",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。