1- import { describe , expect , it } from "vitest" ;
1+ import { readFileSync } from "node:fs" ;
2+ import { describe , expect , it , vi } from "vitest" ;
23import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type : "json" } ;
34import {
45 type OfficialExternalPluginCatalogEntry ,
56 getOfficialExternalPluginCatalogEntry ,
67 isOfficialExternalPluginCatalogFeed ,
78 listOfficialExternalPluginCatalogEntries ,
9+ loadHostedOfficialExternalPluginCatalogEntries ,
810 parseOfficialExternalPluginCatalogEntries ,
911 resolveOfficialExternalProviderContractPluginIds ,
1012 resolveOfficialExternalProviderPluginIds ,
@@ -23,6 +25,16 @@ function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry {
2325}
2426
2527describe ( "official external plugin catalog" , ( ) => {
28+ it ( "keeps hosted fetch guard loading lazy for bundled catalog import paths" , ( ) => {
29+ const source = readFileSync (
30+ new URL ( "./official-external-plugin-catalog.ts" , import . meta. url ) ,
31+ "utf8" ,
32+ ) ;
33+
34+ expect ( source ) . not . toMatch ( / f r o m [ " ' ] \. \. \/ i n f r a \/ n e t \/ f e t c h - g u a r d \. j s [ " ' ] / ) ;
35+ expect ( source ) . toContain ( 'await import("../infra/net/fetch-guard.js")' ) ;
36+ } ) ;
37+
2638 it ( "ships the official plugin catalog as a feed-shaped bundled fallback" , ( ) => {
2739 expect ( isOfficialExternalPluginCatalogFeed ( officialExternalPluginCatalog ) ) . toBe ( true ) ;
2840 expect ( officialExternalPluginCatalog ) . toMatchObject ( {
@@ -45,7 +57,7 @@ describe("official external plugin catalog", () => {
4557 ) . toBe ( false ) ;
4658 expect (
4759 isOfficialExternalPluginCatalogFeed ( {
48- schemaVersion : 2 ,
60+ schemaVersion : 3 ,
4961 id : "openclaw-official-external-plugins" ,
5062 generatedAt : "2026-06-22T00:00:00.000Z" ,
5163 sequence : 1 ,
@@ -54,10 +66,135 @@ describe("official external plugin catalog", () => {
5466 ) . toBe ( false ) ;
5567 } ) ;
5668
69+ it ( "accepts the live ClawHub feed schema version" , ( ) => {
70+ expect (
71+ isOfficialExternalPluginCatalogFeed ( {
72+ schemaVersion : 2 ,
73+ id : "clawhub-official" ,
74+ generatedAt : "2026-06-25T01:19:39.629Z" ,
75+ sequence : 11 ,
76+ entries : [ ] ,
77+ } ) ,
78+ ) . toBe ( true ) ;
79+ } ) ;
80+
81+ it ( "keeps live ClawHub marketplace entries as metadata-only feed entries" , ( ) => {
82+ const [ entry ] = parseOfficialExternalPluginCatalogEntries ( {
83+ schemaVersion : 2 ,
84+ id : "clawhub-official" ,
85+ generatedAt : "2026-06-25T01:19:39.629Z" ,
86+ sequence : 11 ,
87+ entries : [
88+ {
89+ type : "plugin" ,
90+ id : "@expediagroup/expedia-openclaw" ,
91+ title : "Expedia Travel" ,
92+ version : "1.0.4" ,
93+ state : "available" ,
94+ publisher : {
95+ id : "expediagroup" ,
96+ trust : "official" ,
97+ } ,
98+ install : {
99+ candidates : [
100+ {
101+ sourceRef : "public-clawhub" ,
102+ package : "@expediagroup/expedia-openclaw" ,
103+ version : "1.0.4" ,
104+ integrity :
105+ "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9" ,
106+ } ,
107+ ] ,
108+ } ,
109+ } ,
110+ ] ,
111+ } ) ;
112+
113+ if ( entry === undefined ) {
114+ throw new Error ( "Expected hosted ClawHub feed entry to parse" ) ;
115+ }
116+
117+ expect ( entry ) . toMatchObject ( {
118+ id : "@expediagroup/expedia-openclaw" ,
119+ title : "Expedia Travel" ,
120+ version : "1.0.4" ,
121+ } ) ;
122+ expect ( resolveOfficialExternalPluginId ( entry ) ) . toBeUndefined ( ) ;
123+ expect ( resolveOfficialExternalPluginInstall ( entry ) ) . toBeNull ( ) ;
124+ } ) ;
125+
126+ it ( "does not synthesize trusted installs for unavailable or untrusted hosted entries" , ( ) => {
127+ const entries = parseOfficialExternalPluginCatalogEntries ( {
128+ schemaVersion : 2 ,
129+ id : "clawhub-official" ,
130+ generatedAt : "2026-06-25T01:19:39.629Z" ,
131+ sequence : 11 ,
132+ entries : [
133+ {
134+ type : "plugin" ,
135+ id : "@example/unavailable" ,
136+ title : "Unavailable" ,
137+ version : "1.0.0" ,
138+ state : "disabled" ,
139+ publisher : { id : "example" , trust : "official" } ,
140+ install : {
141+ candidates : [
142+ {
143+ sourceRef : "public-clawhub" ,
144+ package : "@example/unavailable" ,
145+ version : "1.0.0" ,
146+ } ,
147+ ] ,
148+ } ,
149+ } ,
150+ {
151+ type : "plugin" ,
152+ id : "@example/community" ,
153+ title : "Community" ,
154+ version : "1.0.0" ,
155+ state : "available" ,
156+ publisher : { id : "example" , trust : "community" } ,
157+ install : {
158+ candidates : [
159+ {
160+ sourceRef : "public-clawhub" ,
161+ package : "@example/community" ,
162+ version : "1.0.0" ,
163+ } ,
164+ ] ,
165+ } ,
166+ } ,
167+ {
168+ type : "plugin" ,
169+ id : "@example/private-source" ,
170+ title : "Private Source" ,
171+ version : "1.0.0" ,
172+ state : "available" ,
173+ publisher : { id : "example" , trust : "official" } ,
174+ install : {
175+ candidates : [
176+ {
177+ sourceRef : "private-feed" ,
178+ package : "@example/private-source" ,
179+ version : "1.0.0" ,
180+ } ,
181+ ] ,
182+ } ,
183+ } ,
184+ ] ,
185+ } ) ;
186+
187+ expect ( entries ) . toHaveLength ( 3 ) ;
188+ for ( const entry of entries ) {
189+ expect ( resolveOfficialExternalPluginId ( entry ) ) . toBeUndefined ( ) ;
190+ expect ( resolveOfficialExternalPluginInstall ( entry ) ) . toBeNull ( ) ;
191+ }
192+ } ) ;
193+
57194 it ( "keeps unsupported versioned feed wrappers out of legacy catalog parsing" , ( ) => {
58195 expect (
59196 parseOfficialExternalPluginCatalogEntries ( {
60- schemaVersion : 2 ,
197+ schemaVersion : 3 ,
61198 id : "future-feed" ,
62199 generatedAt : "2026-06-22T00:00:00.000Z" ,
63200 sequence : 1 ,
@@ -71,6 +208,182 @@ describe("official external plugin catalog", () => {
71208 ) . toEqual ( [ { name : "legacy-catalog-entry" } ] ) ;
72209 } ) ;
73210
211+ it ( "loads a hosted feed with conditional headers and checksum metadata" , async ( ) => {
212+ const body = JSON . stringify ( {
213+ schemaVersion : 1 ,
214+ id : "openclaw-official-external-plugins" ,
215+ generatedAt : "2026-06-22T00:00:00.000Z" ,
216+ sequence : 2 ,
217+ entries : [
218+ {
219+ name : "@openclaw/hosted-proof" ,
220+ kind : "plugin" ,
221+ openclaw : {
222+ plugin : { id : "hosted-proof" , label : "Hosted Proof" } ,
223+ install : { npmSpec : "@openclaw/hosted-proof" , defaultChoice : "npm" } ,
224+ } ,
225+ } ,
226+ ] ,
227+ } ) ;
228+ const fetchImpl = vi . fn ( async ( _url : RequestInfo | URL , init ?: RequestInit ) => {
229+ const headers = new Headers ( init ?. headers ) ;
230+ expect ( headers . get ( "if-none-match" ) ) . toBe ( '"old"' ) ;
231+ expect ( headers . get ( "if-modified-since" ) ) . toBe ( "Mon, 22 Jun 2026 00:00:00 GMT" ) ;
232+ return new Response ( body , {
233+ status : 200 ,
234+ headers : {
235+ etag : '"next"' ,
236+ "last-modified" : "Mon, 22 Jun 2026 01:00:00 GMT" ,
237+ "content-length" : String ( new TextEncoder ( ) . encode ( body ) . byteLength ) ,
238+ } ,
239+ } ) ;
240+ } ) ;
241+
242+ const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
243+ fetchImpl,
244+ ifNoneMatch : '"old"' ,
245+ ifModifiedSince : "Mon, 22 Jun 2026 00:00:00 GMT" ,
246+ } ) ;
247+
248+ expect ( result . source ) . toBe ( "hosted" ) ;
249+ expect ( result . entries . map ( ( entry ) => entry . name ) ) . toEqual ( [ "@openclaw/hosted-proof" ] ) ;
250+ if ( result . source === "hosted" ) {
251+ expect ( result . feed . sequence ) . toBe ( 2 ) ;
252+ expect ( result . metadata ) . toMatchObject ( {
253+ status : 200 ,
254+ etag : '"next"' ,
255+ lastModified : "Mon, 22 Jun 2026 01:00:00 GMT" ,
256+ } ) ;
257+ expect ( result . metadata . checksum ) . toMatch ( / ^ s h a 2 5 6 : [ 0 - 9 a - f ] { 64 } $ / ) ;
258+ }
259+ } ) ;
260+
261+ it ( "keeps live ClawHub metadata-only entries after hosted feed loading" , async ( ) => {
262+ const body = JSON . stringify ( {
263+ schemaVersion : 2 ,
264+ id : "clawhub-official" ,
265+ generatedAt : "2026-06-25T01:19:39.629Z" ,
266+ sequence : 11 ,
267+ entries : [
268+ {
269+ type : "plugin" ,
270+ id : "@expediagroup/expedia-openclaw" ,
271+ title : "Expedia Travel" ,
272+ version : "1.0.4" ,
273+ state : "available" ,
274+ publisher : {
275+ id : "expediagroup" ,
276+ trust : "official" ,
277+ } ,
278+ install : {
279+ candidates : [
280+ {
281+ sourceRef : "public-clawhub" ,
282+ package : "@expediagroup/expedia-openclaw" ,
283+ version : "1.0.4" ,
284+ integrity :
285+ "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9" ,
286+ } ,
287+ ] ,
288+ } ,
289+ } ,
290+ ] ,
291+ } ) ;
292+
293+ const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
294+ fetchImpl : vi . fn (
295+ async ( ) =>
296+ new Response ( body , {
297+ status : 200 ,
298+ headers : {
299+ "content-length" : String ( new TextEncoder ( ) . encode ( body ) . byteLength ) ,
300+ } ,
301+ } ) ,
302+ ) ,
303+ } ) ;
304+
305+ expect ( result . source ) . toBe ( "hosted" ) ;
306+ expect ( result . entries ) . toHaveLength ( 1 ) ;
307+ expect ( result . entries [ 0 ] ) . toMatchObject ( {
308+ id : "@expediagroup/expedia-openclaw" ,
309+ title : "Expedia Travel" ,
310+ version : "1.0.4" ,
311+ } ) ;
312+ } ) ;
313+
314+ it ( "falls back to the bundled catalog when hosted feed validation fails" , async ( ) => {
315+ const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
316+ fetchImpl : vi . fn (
317+ async ( ) =>
318+ new Response ( JSON . stringify ( { schemaVersion : 1 , id : " " , entries : [ ] } ) , {
319+ status : 200 ,
320+ } ) ,
321+ ) ,
322+ } ) ;
323+
324+ expect ( result . source ) . toBe ( "bundled-fallback" ) ;
325+ expect ( result . entries . length ) . toBe ( listOfficialExternalPluginCatalogEntries ( ) . length ) ;
326+ if ( result . source === "bundled-fallback" ) {
327+ expect ( result . error ) . toContain ( "supported schema version" ) ;
328+ expect ( result . metadata ?. checksum ) . toMatch ( / ^ s h a 2 5 6 : [ 0 - 9 a - f ] { 64 } $ / ) ;
329+ }
330+ } ) ;
331+
332+ it ( "falls back to the bundled catalog on HTTP 304 until a snapshot cache exists" , async ( ) => {
333+ const result = await loadHostedOfficialExternalPluginCatalogEntries ( {
334+ fetchImpl : vi . fn (
335+ async ( ) =>
336+ new Response ( null , {
337+ status : 304 ,
338+ headers : { etag : '"same"' , "last-modified" : "Mon, 22 Jun 2026 01:00:00 GMT" } ,
339+ } ) ,
340+ ) ,
341+ } ) ;
342+
343+ expect ( result . source ) . toBe ( "bundled-fallback" ) ;
344+ if ( result . source === "bundled-fallback" ) {
345+ expect ( result . error ) . toContain ( "without a cached snapshot" ) ;
346+ expect ( result . metadata ) . toMatchObject ( {
347+ status : 304 ,
348+ etag : '"same"' ,
349+ lastModified : "Mon, 22 Jun 2026 01:00:00 GMT" ,
350+ } ) ;
351+ }
352+ } ) ;
353+
354+ it ( "falls back to the bundled catalog on checksum mismatch and oversized bodies" , async ( ) => {
355+ const mismatch = await loadHostedOfficialExternalPluginCatalogEntries ( {
356+ expectedSha256 : "sha256:not-current" ,
357+ fetchImpl : vi . fn (
358+ async ( ) =>
359+ new Response (
360+ JSON . stringify ( {
361+ schemaVersion : 1 ,
362+ id : "openclaw-official-external-plugins" ,
363+ generatedAt : "2026-06-22T00:00:00.000Z" ,
364+ sequence : 1 ,
365+ entries : [ ] ,
366+ } ) ,
367+ { status : 200 } ,
368+ ) ,
369+ ) ,
370+ } ) ;
371+ expect ( mismatch . source ) . toBe ( "bundled-fallback" ) ;
372+ if ( mismatch . source === "bundled-fallback" ) {
373+ expect ( mismatch . error ) . toContain ( "checksum mismatch" ) ;
374+ expect ( mismatch . metadata ?. checksum ) . toMatch ( / ^ s h a 2 5 6 : [ 0 - 9 a - f ] { 64 } $ / ) ;
375+ }
376+
377+ const oversized = await loadHostedOfficialExternalPluginCatalogEntries ( {
378+ maxBytes : 4 ,
379+ fetchImpl : vi . fn ( async ( ) => new Response ( "12345" , { status : 200 } ) ) ,
380+ } ) ;
381+ expect ( oversized . source ) . toBe ( "bundled-fallback" ) ;
382+ if ( oversized . source === "bundled-fallback" ) {
383+ expect ( oversized . error ) . toContain ( "exceeds 4 bytes" ) ;
384+ }
385+ } ) ;
386+
74387 it ( "lists the externalized provider and capability plugins with install metadata" , ( ) => {
75388 const providers = [
76389 [ "arcee" , "@openclaw/arcee-provider" ] ,
0 commit comments