@@ -3,7 +3,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
33import type { EmbeddingProviderAdapter } from "openclaw/plugin-sdk/embedding-providers" ;
44import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings" ;
55import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
6- import { createEmbeddingProvider , resolveEmbeddingProviderFallbackModel } from "./embeddings.js" ;
6+ import {
7+ createEmbeddingProvider ,
8+ resolveEmbeddingProviderFallbackModel ,
9+ resolveEmbeddingProviderIndexIdentity ,
10+ } from "./embeddings.js" ;
711
812const mockEmbeddingRegistry = vi . hoisted ( ( ) => ( {
913 genericAdapters : [ ] as EmbeddingProviderAdapter [ ] ,
@@ -297,6 +301,55 @@ describe("createEmbeddingProvider", () => {
297301 expect ( result . provider ?. model ) . toBe ( "text-embedding-3-large" ) ;
298302 } ) ;
299303
304+ it ( "upgrade safety: backfill aligns provider.model with identity resolution for existing indexes" , async ( ) => {
305+ // #90042 upgrade scenario: pre-fix, an adapter with defaultModel whose
306+ // create() returned provider.model="" would propagate the empty model
307+ // to every downstream consumer (identity checks, meta writes, chunk
308+ // model tags, vector-search filters). Post-fix, createWithAdapter
309+ // backfills the resolved model so provider.model matches what
310+ // resolveEmbeddingProviderIndexIdentity independently computes.
311+ //
312+ // Impact on existing indexes: stored meta was written by the identity
313+ // path (which already used resolveProviderModel), so it carries the
314+ // correct model. After upgrade, provider.model now matches the stored
315+ // meta → no spurious mismatch → no unnecessary reindex on first boot.
316+ registerMemoryEmbeddingProvider ( {
317+ id : "openai" ,
318+ transport : "remote" ,
319+ defaultModel : "text-embedding-3-small" ,
320+ resolveIndexIdentity : ( options ) => ( {
321+ model : options . model ,
322+ cacheKeyData : { } ,
323+ } ) ,
324+ create : async ( ) => ( {
325+ provider : {
326+ id : "openai" ,
327+ model : "" ,
328+ embedQuery : async ( ) => [ 1 ] ,
329+ embedBatch : async ( texts ) => texts . map ( ( ) => [ 1 ] ) ,
330+ } ,
331+ } ) ,
332+ } ) ;
333+
334+ const result = await createEmbeddingProvider ( {
335+ ...createOptions ( "openai" ) ,
336+ model : "" ,
337+ } ) ;
338+
339+ expect ( result . provider ?. model ) . toBe ( "text-embedding-3-small" ) ;
340+
341+ const identity = resolveEmbeddingProviderIndexIdentity ( {
342+ ...createOptions ( "openai" ) ,
343+ model : "" ,
344+ } ) ;
345+ expect ( identity ?. provider . model ) . toBe ( "text-embedding-3-small" ) ;
346+
347+ // Post-upgrade invariant: provider.model and identity model are
348+ // consistent, preventing the "index was built for model X, expected "
349+ // mismatch that #90042 and #91691 describe.
350+ expect ( result . provider ?. model ) . toBe ( identity ?. provider . model ) ;
351+ } ) ;
352+
300353 it ( "uses config-scoped lookup for generic fallback model resolution" , ( ) => {
301354 registerGenericEmbeddingProvider ( {
302355 id : "openai-compatible" ,
0 commit comments