1- import { readFileSync } from "node:fs" ;
1+ import { mkdtempSync , readFileSync , rmSync } from "node:fs" ;
2+ import os from "node:os" ;
3+ import path from "node:path" ;
24import { describe , expect , it , vi } from "vitest" ;
35import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type : "json" } ;
6+ import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js" ;
7+ import { createSqliteHostedOfficialExternalPluginCatalogSnapshotStore } from "./official-external-plugin-catalog-snapshot-store.js" ;
48import {
59 type OfficialExternalPluginCatalogEntry ,
610 DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL ,
@@ -245,6 +249,7 @@ describe("official external plugin catalog", () => {
245249 fetchImpl,
246250 ifNoneMatch : '"old"' ,
247251 ifModifiedSince : "Mon, 22 Jun 2026 00:00:00 GMT" ,
252+ snapshotStore : null ,
248253 } ) ;
249254
250255 expect ( result . source ) . toBe ( "hosted" ) ;
@@ -315,6 +320,7 @@ describe("official external plugin catalog", () => {
315320
316321 it ( "falls back to the bundled catalog when hosted feed validation fails" , async ( ) => {
317322 const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
323+ snapshotStore : null ,
318324 fetchImpl : vi . fn (
319325 async ( ) =>
320326 new Response ( JSON . stringify ( { schemaVersion : 1 , id : " " , entries : [ ] } ) , {
@@ -333,6 +339,7 @@ describe("official external plugin catalog", () => {
333339
334340 it ( "falls back to the bundled catalog on HTTP 304 until a snapshot cache exists" , async ( ) => {
335341 const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
342+ snapshotStore : null ,
336343 fetchImpl : vi . fn (
337344 async ( ) =>
338345 new Response ( null , {
@@ -395,6 +402,108 @@ describe("official external plugin catalog", () => {
395402 expect ( snapshot ?. metadata . checksum ) . toMatch ( / ^ s h a 2 5 6 : [ 0 - 9 a - f ] { 64 } $ / ) ;
396403 } ) ;
397404
405+ it ( "persists hosted feed snapshots in OpenClaw state for HTTP 304 reuse" , async ( ) => {
406+ const stateDir = mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-hosted-catalog-" ) ) ;
407+ try {
408+ const body = JSON . stringify ( {
409+ schemaVersion : 1 ,
410+ id : "openclaw-official-external-plugins" ,
411+ generatedAt : "2026-06-22T00:00:00.000Z" ,
412+ sequence : 7 ,
413+ entries : [
414+ {
415+ name : "@openclaw/sqlite-snapshot-proof" ,
416+ kind : "plugin" ,
417+ openclaw : { plugin : { id : "sqlite-snapshot-proof" } } ,
418+ } ,
419+ ] ,
420+ } ) ;
421+
422+ const seeded = await loadHostedOfficialExternalPluginCatalogEntries ( {
423+ stateDir,
424+ now : ( ) => new Date ( "2026-06-22T02:03:04.000Z" ) ,
425+ fetchImpl : vi . fn (
426+ async ( ) =>
427+ new Response ( body , {
428+ status : 200 ,
429+ headers : {
430+ etag : '"sqlite"' ,
431+ "last-modified" : "Mon, 22 Jun 2026 02:00:00 GMT" ,
432+ } ,
433+ } ) ,
434+ ) ,
435+ } ) ;
436+ if ( seeded . source !== "hosted" ) {
437+ throw new Error ( "expected seeded hosted feed" ) ;
438+ }
439+ closeOpenClawStateDatabaseForTest ( ) ;
440+
441+ const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
442+ stateDir,
443+ fetchImpl : vi . fn ( async ( ) => new Response ( null , { status : 304 } ) ) ,
444+ } ) ;
445+
446+ expect ( result . source ) . toBe ( "hosted-snapshot" ) ;
447+ expect ( result . entries . map ( ( entry ) => entry . name ) ) . toEqual ( [
448+ "@openclaw/sqlite-snapshot-proof" ,
449+ ] ) ;
450+ if ( result . source === "hosted-snapshot" ) {
451+ expect ( result . snapshot . savedAt ) . toBe ( "2026-06-22T02:03:04.000Z" ) ;
452+ expect ( result . metadata . checksum ) . toBe ( seeded . metadata . checksum ) ;
453+ }
454+ } finally {
455+ closeOpenClawStateDatabaseForTest ( ) ;
456+ rmSync ( stateDir , { recursive : true , force : true } ) ;
457+ }
458+ } ) ;
459+
460+ it ( "reads and updates hosted catalog snapshots in the SQLite store" , async ( ) => {
461+ const stateDir = mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-hosted-store-" ) ) ;
462+ try {
463+ const store = createSqliteHostedOfficialExternalPluginCatalogSnapshotStore ( { stateDir } ) ;
464+ const url = "https://register.openclaw.ai/official-external-plugin-catalog.json" ;
465+
466+ const firstBody = JSON . stringify ( { entries : [ ] } ) ;
467+ const secondBody = JSON . stringify ( { entries : [ { } ] } ) ;
468+
469+ await expect ( store . read ( url ) ) . resolves . toBeNull ( ) ;
470+ await store . write ( {
471+ body : firstBody ,
472+ metadata : {
473+ url,
474+ status : 200 ,
475+ etag : '"first"' ,
476+ checksum : "sha256:first" ,
477+ } ,
478+ savedAt : "2026-06-22T02:03:04.000Z" ,
479+ } ) ;
480+ await store . write ( {
481+ body : secondBody ,
482+ metadata : {
483+ url,
484+ status : 200 ,
485+ lastModified : "Mon, 22 Jun 2026 03:00:00 GMT" ,
486+ checksum : "sha256:second" ,
487+ } ,
488+ savedAt : "2026-06-22T03:04:05.000Z" ,
489+ } ) ;
490+
491+ await expect ( store . read ( url ) ) . resolves . toMatchObject ( {
492+ body : secondBody ,
493+ metadata : {
494+ url,
495+ status : 200 ,
496+ lastModified : "Mon, 22 Jun 2026 03:00:00 GMT" ,
497+ checksum : "sha256:second" ,
498+ } ,
499+ savedAt : "2026-06-22T03:04:05.000Z" ,
500+ } ) ;
501+ } finally {
502+ closeOpenClawStateDatabaseForTest ( ) ;
503+ rmSync ( stateDir , { recursive : true , force : true } ) ;
504+ }
505+ } ) ;
506+
398507 it ( "uses the last known good snapshot when the hosted feed returns HTTP 304" , async ( ) => {
399508 const body = JSON . stringify ( {
400509 schemaVersion : 1 ,
@@ -655,6 +764,7 @@ describe("official external plugin catalog", () => {
655764
656765 it ( "falls back to the bundled catalog on checksum mismatch and oversized bodies" , async ( ) => {
657766 const mismatch = await loadHostedOfficialExternalPluginCatalogEntries ( {
767+ snapshotStore : null ,
658768 expectedSha256 : "sha256:not-current" ,
659769 fetchImpl : vi . fn (
660770 async ( ) =>
@@ -677,6 +787,7 @@ describe("official external plugin catalog", () => {
677787 }
678788
679789 const oversized = await loadHostedOfficialExternalPluginCatalogEntries ( {
790+ snapshotStore : null ,
680791 maxBytes : 4 ,
681792 fetchImpl : vi . fn ( async ( ) => new Response ( "12345" , { status : 200 } ) ) ,
682793 } ) ;
0 commit comments