@@ -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 {
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 ( / ^ s h a 2 5 6 : [ 0 - 9 a - 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