@@ -19,6 +19,7 @@ let currentPluginMetadataSnapshotMock: ReturnType<typeof vi.fn<(...args: unknown
1919let loadPluginMetadataSnapshotMock : ReturnType < typeof vi . fn < ( ...args : unknown [ ] ) => unknown > > ;
2020let readFileMock : ReturnType < typeof vi . fn < ( pathname : string ) => Promise < string > > > ;
2121let buildAgentModelCatalogCacheKeyMock : ReturnType < typeof vi . fn > ;
22+ let buildModelsJsonSourceFingerprintMock : ReturnType < typeof vi . fn > ;
2223let readCachedAgentModelCatalogMock : ReturnType < typeof vi . fn > ;
2324let writeCachedAgentModelCatalogMock : ReturnType < typeof vi . fn > ;
2425
@@ -239,10 +240,19 @@ describe("loadModelCatalog", () => {
239240 readFile : readFileMock ,
240241 } ) ) ;
241242 ensureOpenClawModelsJsonMock = vi . fn ( ) . mockResolvedValue ( { agentDir : "/tmp" , wrote : false } ) ;
243+ buildModelsJsonSourceFingerprintMock = vi . fn ( ) . mockResolvedValue ( {
244+ agentDir : "/tmp/openclaw" ,
245+ fingerprint : "source-fingerprint" ,
246+ workspaceDir : "/tmp/openclaw-workspace" ,
247+ } ) ;
242248 vi . doMock ( "./models-config.js" , ( ) => ( {
249+ buildModelsJsonSourceFingerprint : buildModelsJsonSourceFingerprintMock ,
243250 ensureOpenClawModelsJson : ensureOpenClawModelsJsonMock ,
244251 } ) ) ;
245- buildAgentModelCatalogCacheKeyMock = vi . fn ( ( ) => "test-cache-key" ) ;
252+ buildAgentModelCatalogCacheKeyMock = vi . fn (
253+ ( input : { cacheScope ?: { sourceFingerprint ?: string } } ) =>
254+ `test-cache-key:${ input . cacheScope ?. sourceFingerprint ?? "none" } ` ,
255+ ) ;
246256 readCachedAgentModelCatalogMock = vi . fn ( ( ) => undefined ) ;
247257 writeCachedAgentModelCatalogMock = vi . fn ( ) ;
248258 vi . doMock ( "./model-catalog-state-cache.js" , ( ) => ( {
@@ -317,6 +327,12 @@ describe("loadModelCatalog", () => {
317327 currentPluginMetadataSnapshotMock . mockReturnValue ( undefined ) ;
318328 loadPluginMetadataSnapshotMock . mockReset ( ) ;
319329 loadPluginMetadataSnapshotMock . mockReturnValue ( emptyPluginMetadataSnapshot ( ) ) ;
330+ buildModelsJsonSourceFingerprintMock . mockClear ( ) ;
331+ buildModelsJsonSourceFingerprintMock . mockResolvedValue ( {
332+ agentDir : "/tmp/openclaw" ,
333+ fingerprint : "source-fingerprint" ,
334+ workspaceDir : "/tmp/openclaw-workspace" ,
335+ } ) ;
320336 buildAgentModelCatalogCacheKeyMock . mockClear ( ) ;
321337 readCachedAgentModelCatalogMock . mockReset ( ) ;
322338 readCachedAgentModelCatalogMock . mockReturnValue ( undefined ) ;
@@ -407,7 +423,7 @@ describe("loadModelCatalog", () => {
407423 expect ( result ) . toEqual ( cached ) ;
408424 expect ( readCachedAgentModelCatalogMock ) . toHaveBeenCalledWith ( {
409425 agentDir : "/tmp/openclaw" ,
410- catalogKey : "test-cache-key" ,
426+ catalogKey : "test-cache-key:source-fingerprint " ,
411427 } ) ;
412428 expect ( ensureOpenClawModelsJsonMock ) . not . toHaveBeenCalled ( ) ;
413429 expect ( importAgentDiscoveryModule ) . not . toHaveBeenCalled ( ) ;
@@ -426,7 +442,7 @@ describe("loadModelCatalog", () => {
426442 expect ( readCachedAgentModelCatalogMock ) . not . toHaveBeenCalled ( ) ;
427443 expect ( writeCachedAgentModelCatalogMock ) . toHaveBeenCalledWith ( {
428444 agentDir : "/tmp/openclaw" ,
429- catalogKey : "test-cache-key" ,
445+ catalogKey : "test-cache-key:source-fingerprint " ,
430446 entries : result ,
431447 } ) ;
432448 } ) ;
@@ -439,11 +455,49 @@ describe("loadModelCatalog", () => {
439455 expect ( result ) . toEqual ( [ { id : "runtime-fast" , name : "Runtime Fast" , provider : "openai" } ] ) ;
440456 expect ( writeCachedAgentModelCatalogMock ) . toHaveBeenCalledWith ( {
441457 agentDir : "/tmp/openclaw" ,
442- catalogKey : "test-cache-key" ,
458+ catalogKey : "test-cache-key:source-fingerprint " ,
443459 entries : result ,
444460 } ) ;
445461 } ) ;
446462
463+ it ( "misses the state cached catalog when source freshness changes" , async ( ) => {
464+ buildModelsJsonSourceFingerprintMock
465+ . mockResolvedValueOnce ( {
466+ agentDir : "/tmp/openclaw" ,
467+ fingerprint : "old-source" ,
468+ workspaceDir : "/tmp/openclaw-workspace" ,
469+ } )
470+ . mockResolvedValueOnce ( {
471+ agentDir : "/tmp/openclaw" ,
472+ fingerprint : "new-source" ,
473+ workspaceDir : "/tmp/openclaw-workspace" ,
474+ } ) ;
475+ readCachedAgentModelCatalogMock . mockImplementation ( ( { catalogKey } : { catalogKey : string } ) =>
476+ catalogKey . endsWith ( "old-source" )
477+ ? [ { id : "cached-stale" , name : "Cached Stale" , provider : "openai" } ]
478+ : undefined ,
479+ ) ;
480+ mockAgentDiscoveryModels ( [ { id : "fresh-fast" , name : "Fresh Fast" , provider : "openai" } ] ) ;
481+
482+ await expect ( loadModelCatalog ( { config : { } as OpenClawConfig } ) ) . resolves . toEqual ( [
483+ { id : "cached-stale" , name : "Cached Stale" , provider : "openai" } ,
484+ ] ) ;
485+ resetModelCatalogCacheForTest ( ) ;
486+ mockAgentDiscoveryModels ( [ { id : "fresh-fast" , name : "Fresh Fast" , provider : "openai" } ] ) ;
487+ await expect ( loadModelCatalog ( { config : { } as OpenClawConfig } ) ) . resolves . toEqual ( [
488+ { id : "fresh-fast" , name : "Fresh Fast" , provider : "openai" } ,
489+ ] ) ;
490+
491+ expect ( readCachedAgentModelCatalogMock ) . toHaveBeenNthCalledWith ( 1 , {
492+ agentDir : "/tmp/openclaw" ,
493+ catalogKey : "test-cache-key:old-source" ,
494+ } ) ;
495+ expect ( readCachedAgentModelCatalogMock ) . toHaveBeenNthCalledWith ( 2 , {
496+ agentDir : "/tmp/openclaw" ,
497+ catalogKey : "test-cache-key:new-source" ,
498+ } ) ;
499+ } ) ;
500+
447501 it ( "reloads dynamic registry entries after clearing the cache" , async ( ) => {
448502 const models = [ { id : "existing" , name : "Existing" , provider : "ollama" } ] ;
449503 mockAgentDiscoveryModels ( models ) ;
0 commit comments