@@ -6,9 +6,13 @@ import { resetLogger, setLoggerOverride } from "../logging/logger.js";
66import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js" ;
77import {
88 clearCurrentPluginMetadataSnapshot ,
9+ resolvePluginMetadataControlPlaneFingerprint ,
910 setCurrentPluginMetadataSnapshot ,
1011} from "../plugins/current-plugin-metadata-snapshot.js" ;
12+ import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js" ;
13+ import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js" ;
1114import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js" ;
15+ import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js" ;
1216import { CommandLaneTaskTimeoutError } from "../process/command-queue.js" ;
1317import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js" ;
1418import type { AuthProfileStore } from "./auth-profiles/types.js" ;
@@ -164,10 +168,7 @@ let authTempRoot = "";
164168let authTempCounter = 0 ;
165169
166170beforeAll ( ( ) => {
167- setCurrentPluginMetadataSnapshot ( loadPluginMetadataSnapshot ( { config : { } , env : process . env } ) , {
168- config : { } ,
169- env : process . env ,
170- } ) ;
171+ setDefaultPluginMetadataSnapshot ( ) ;
171172} ) ;
172173
173174afterAll ( ( ) => {
@@ -181,6 +182,73 @@ function resetModelFallbackTestState(): void {
181182 authSourceCheckMock . hasAnyAuthProfileStoreSource . mockReset ( ) . mockReturnValue ( false ) ;
182183}
183184
185+ function setDefaultPluginMetadataSnapshot ( ) : void {
186+ setCurrentPluginMetadataSnapshot ( loadPluginMetadataSnapshot ( { config : { } , env : process . env } ) , {
187+ config : { } ,
188+ env : process . env ,
189+ } ) ;
190+ }
191+
192+ function createModelNormalizerSnapshot ( params : {
193+ manifestHash : string ;
194+ prefix : string ;
195+ } ) : PluginMetadataSnapshot {
196+ const policyHash = resolveInstalledPluginIndexPolicyHash ( { } ) ;
197+ const index : InstalledPluginIndex = {
198+ version : 1 ,
199+ hostContractVersion : "test-host" ,
200+ compatRegistryVersion : "test-compat" ,
201+ migrationVersion : 1 ,
202+ policyHash,
203+ generatedAtMs : 0 ,
204+ installRecords : { } ,
205+ plugins : [
206+ {
207+ pluginId : "fallback-normalizer" ,
208+ manifestPath : `/tmp/fallback-normalizer-${ params . manifestHash } /openclaw.plugin.json` ,
209+ manifestHash : params . manifestHash ,
210+ source : `/tmp/fallback-normalizer-${ params . manifestHash } /index.ts` ,
211+ rootDir : `/tmp/fallback-normalizer-${ params . manifestHash } ` ,
212+ origin : "global" ,
213+ enabled : true ,
214+ startup : {
215+ sidecar : false ,
216+ memory : false ,
217+ deferConfiguredChannelFullLoadUntilAfterListen : false ,
218+ agentHarnesses : [ ] ,
219+ } ,
220+ compat : [ ] ,
221+ } ,
222+ ] ,
223+ diagnostics : [ ] ,
224+ } ;
225+ return {
226+ policyHash,
227+ configFingerprint : resolvePluginMetadataControlPlaneFingerprint (
228+ { } ,
229+ {
230+ env : process . env ,
231+ index,
232+ policyHash,
233+ } ,
234+ ) ,
235+ index,
236+ registryDiagnostics : [ ] ,
237+ plugins : [
238+ {
239+ id : "fallback-normalizer" ,
240+ modelIdNormalization : {
241+ providers : {
242+ demo : {
243+ prefixWhenBare : params . prefix ,
244+ } ,
245+ } ,
246+ } ,
247+ } ,
248+ ] ,
249+ } as unknown as PluginMetadataSnapshot ;
250+ }
251+
184252afterEach ( resetModelFallbackTestState ) ;
185253
186254beforeEach ( ( ) => {
@@ -227,6 +295,31 @@ function makeProviderFallbackCfg(provider: string): OpenClawConfig {
227295 } ) ;
228296}
229297
298+ function makeProviderOrderFallbackCfg (
299+ entries : Array < [ provider : string , model : string ] > ,
300+ ) : OpenClawConfig {
301+ return {
302+ agents : {
303+ defaults : {
304+ model : {
305+ fallbacks : [ ] ,
306+ } ,
307+ } ,
308+ } ,
309+ models : {
310+ providers : Object . fromEntries (
311+ entries . map ( ( [ provider , model ] ) => [
312+ provider ,
313+ {
314+ baseUrl : `https://${ provider } .example.test` ,
315+ models : [ { id : model } ] ,
316+ } ,
317+ ] ) ,
318+ ) ,
319+ } ,
320+ } as unknown as OpenClawConfig ;
321+ }
322+
230323async function withTempAuthStore < T > (
231324 store : AuthProfileStore ,
232325 run : ( tempDir : string ) => Promise < T > ,
@@ -1969,6 +2062,82 @@ describe("runWithModelFallback", () => {
19692062 ] ) ;
19702063 } ) ;
19712064
2065+ it ( "does not reuse provider-order-sensitive configured fallback candidates" , ( ) => {
2066+ const anthropicFirst = makeProviderOrderFallbackCfg ( [
2067+ [ "anthropic" , "claude-sonnet-4" ] ,
2068+ [ "ollama" , "llama3" ] ,
2069+ ] ) ;
2070+ const ollamaFirst = makeProviderOrderFallbackCfg ( [
2071+ [ "ollama" , "llama3" ] ,
2072+ [ "anthropic" , "claude-sonnet-4" ] ,
2073+ ] ) ;
2074+
2075+ expect (
2076+ testing . resolveFallbackCandidates ( {
2077+ cfg : anthropicFirst ,
2078+ provider : "" ,
2079+ model : "" ,
2080+ fallbacksOverride : [ ] ,
2081+ } ) ,
2082+ ) . toEqual ( [ { provider : "anthropic" , model : "claude-sonnet-4" } ] ) ;
2083+ expect (
2084+ testing . resolveFallbackCandidates ( {
2085+ cfg : ollamaFirst ,
2086+ provider : "" ,
2087+ model : "" ,
2088+ fallbacksOverride : [ ] ,
2089+ } ) ,
2090+ ) . toEqual ( [ { provider : "ollama" , model : "llama3" } ] ) ;
2091+ } ) ;
2092+
2093+ it ( "does not reuse fallback candidate cache entries across manifest normalization snapshots" , ( ) => {
2094+ const cfg = makeCfg ( {
2095+ agents : {
2096+ defaults : {
2097+ model : {
2098+ fallbacks : [ ] ,
2099+ } ,
2100+ } ,
2101+ } ,
2102+ } ) ;
2103+
2104+ try {
2105+ setCurrentPluginMetadataSnapshot (
2106+ createModelNormalizerSnapshot ( {
2107+ manifestHash : "alpha" ,
2108+ prefix : "alpha" ,
2109+ } ) ,
2110+ { config : { } , env : process . env } ,
2111+ ) ;
2112+ expect (
2113+ testing . resolveFallbackCandidates ( {
2114+ cfg,
2115+ provider : "demo" ,
2116+ model : "demo-model" ,
2117+ fallbacksOverride : [ ] ,
2118+ } ) ,
2119+ ) . toEqual ( [ { provider : "demo" , model : "alpha/demo-model" } ] ) ;
2120+
2121+ setCurrentPluginMetadataSnapshot (
2122+ createModelNormalizerSnapshot ( {
2123+ manifestHash : "bravo" ,
2124+ prefix : "bravo" ,
2125+ } ) ,
2126+ { config : { } , env : process . env } ,
2127+ ) ;
2128+ expect (
2129+ testing . resolveFallbackCandidates ( {
2130+ cfg,
2131+ provider : "demo" ,
2132+ model : "demo-model" ,
2133+ fallbacksOverride : [ ] ,
2134+ } ) ,
2135+ ) . toEqual ( [ { provider : "demo" , model : "bravo/demo-model" } ] ) ;
2136+ } finally {
2137+ setDefaultPluginMetadataSnapshot ( ) ;
2138+ }
2139+ } ) ;
2140+
19722141 it ( "defaults provider/model when missing (regression #946)" , ( ) => {
19732142 const cfg = makeCfg ( {
19742143 agents : {
0 commit comments