@@ -9,7 +9,10 @@ import {
99} from "../config/runtime-snapshot.js" ;
1010import { resolveGatewayStartupPluginActivationConfig } from "../gateway/plugin-activation-runtime-config.js" ;
1111import { resetPluginStateStoreForTests } from "../plugin-state/plugin-state-store.js" ;
12- import { loadGatewayStartupPluginPlan } from "../plugins/gateway-startup-plugin-ids.js" ;
12+ import {
13+ collectUnregisteredConfiguredMemoryEmbeddingProviders ,
14+ loadGatewayStartupPluginPlan ,
15+ } from "../plugins/gateway-startup-plugin-ids.js" ;
1316import { createEmptyPluginRegistry } from "../plugins/registry-empty.js" ;
1417import { resetPluginRuntimeStateForTest , setActivePluginRegistry } from "../plugins/runtime.js" ;
1518import { withStateDirEnv } from "../test-helpers/state-dir-env.js" ;
@@ -36,7 +39,14 @@ vi.mock("../plugins/status.js", async (importOriginal) => {
3639// eager importer in the graph keeps working.
3740vi . mock ( "../plugins/gateway-startup-plugin-ids.js" , async ( importOriginal ) => {
3841 const actual = await importOriginal < typeof import ( "../plugins/gateway-startup-plugin-ids.js" ) > ( ) ;
39- return { ...actual , loadGatewayStartupPluginPlan : vi . fn ( ) } as typeof actual ;
42+ return {
43+ ...actual ,
44+ loadGatewayStartupPluginPlan : vi . fn ( ) ,
45+ // Default to no unregistered providers so the should-run tests are unaffected; the
46+ // memory-provider tests below override per case. collectRegisteredEmbeddingProviderIds
47+ // stays real (it just reads the seeded registry + core embedding registry).
48+ collectUnregisteredConfiguredMemoryEmbeddingProviders : vi . fn ( ( ) => [ ] ) ,
49+ } as typeof actual ;
4050} ) ;
4151// The startup-plan activation assembly is the gateway's own shared helper; mock it to
4252// identity (return the runtime config) so the status wiring stays deterministic. The helper's
@@ -62,11 +72,17 @@ const loadGatewayStartupPluginPlanMock = vi.mocked(loadGatewayStartupPluginPlan)
6272const resolveGatewayStartupPluginActivationConfigMock = vi . mocked (
6373 resolveGatewayStartupPluginActivationConfig ,
6474) ;
75+ const collectUnregisteredConfiguredMemoryEmbeddingProvidersMock = vi . mocked (
76+ collectUnregisteredConfiguredMemoryEmbeddingProviders ,
77+ ) ;
6578
6679afterEach ( ( ) => {
6780 resolveReadOnlyChannelPluginsForConfigMock . mockReset ( ) ;
6881 loadGatewayStartupPluginPlanMock . mockReset ( ) ;
6982 resolveGatewayStartupPluginActivationConfigMock . mockClear ( ) ;
83+ collectUnregisteredConfiguredMemoryEmbeddingProvidersMock . mockReset ( ) ;
84+ // Re-establish the empty default so the next test starts with no unregistered providers.
85+ collectUnregisteredConfiguredMemoryEmbeddingProvidersMock . mockReturnValue ( [ ] ) ;
7086 clearRuntimeConfigSnapshot ( ) ;
7187 resetPluginRuntimeStateForTest ( ) ;
7288 resetPluginStateStoreForTests ( ) ;
@@ -166,3 +182,87 @@ describe("installed plugin health should-run drift", () => {
166182 } ) ;
167183 } ) ;
168184} ) ;
185+
186+ describe ( "installed plugin health unregistered memory embedding providers" , ( ) => {
187+ it ( "surfaces configured memory embedding providers the runtime registry does not register" , async ( ) => {
188+ await withStateDirEnv ( "openclaw-status-memory-embed-" , async ( ) => {
189+ resolveReadOnlyChannelPluginsForConfigMock . mockReturnValue ( {
190+ loadFailures : [ ] ,
191+ missingConfiguredChannelIds : [ ] ,
192+ } as never ) ;
193+ loadGatewayStartupPluginPlanMock . mockReturnValue ( {
194+ channelPluginIds : [ ] ,
195+ configuredDeferredChannelPluginIds : [ ] ,
196+ pluginIds : [ ] ,
197+ } ) ;
198+ collectUnregisteredConfiguredMemoryEmbeddingProvidersMock . mockReturnValue ( [
199+ { configuredId : "custom-embed" , source : "provider" } ,
200+ ] ) ;
201+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) , "empty" , "default" , "/tmp/ws" ) ;
202+
203+ const snapshot = await collectInstalledPluginHealthSnapshot ( {
204+ config : { } as never ,
205+ workspaceDir : "/tmp/ws" ,
206+ } ) ;
207+
208+ expect ( snapshot . unregisteredMemoryEmbeddingProviders ) . toEqual ( [
209+ { configuredId : "custom-embed" , source : "provider" } ,
210+ ] ) ;
211+ // The mismatch is checked against the live registry's embedding providers (collected
212+ // into a Set), so a CLI/empty-registry process can never false-report "unregistered".
213+ expect ( collectUnregisteredConfiguredMemoryEmbeddingProvidersMock ) . toHaveBeenCalledWith (
214+ expect . objectContaining ( { registeredProviderIds : expect . any ( Set ) } ) ,
215+ ) ;
216+ expect ( formatDetailedPluginHealth ( snapshot ) ) . toContain (
217+ "Configured memory provider not registered: 1 (custom-embed (memorySearch.provider))" ,
218+ ) ;
219+ } ) ;
220+ } ) ;
221+
222+ it ( "skips the check and renders no line when no runtime registry is active" , async ( ) => {
223+ await withStateDirEnv ( "openclaw-status-memory-embed-no-registry-" , async ( ) => {
224+ // No active runtime registry (a fresh CLI process that never started a gateway).
225+ resetPluginRuntimeStateForTest ( ) ;
226+ resolveReadOnlyChannelPluginsForConfigMock . mockReturnValue ( {
227+ loadFailures : [ ] ,
228+ missingConfiguredChannelIds : [ ] ,
229+ } as never ) ;
230+ loadGatewayStartupPluginPlanMock . mockReturnValue ( {
231+ channelPluginIds : [ ] ,
232+ configuredDeferredChannelPluginIds : [ ] ,
233+ pluginIds : [ ] ,
234+ } ) ;
235+ // Even if the resolver would report something, the null-registry guard must skip it
236+ // (a CLI/empty-registry process must never false-report "unregistered").
237+ collectUnregisteredConfiguredMemoryEmbeddingProvidersMock . mockReturnValue ( [
238+ { configuredId : "custom-embed" , source : "provider" } ,
239+ ] ) ;
240+
241+ const snapshot = await collectInstalledPluginHealthSnapshot ( {
242+ config : { } as never ,
243+ workspaceDir : "/tmp/ws" ,
244+ } ) ;
245+
246+ expect ( snapshot . unregisteredMemoryEmbeddingProviders ) . toBeUndefined ( ) ;
247+ expect ( collectUnregisteredConfiguredMemoryEmbeddingProvidersMock ) . not . toHaveBeenCalled ( ) ;
248+ expect ( formatDetailedPluginHealth ( snapshot ) ) . not . toContain (
249+ "Configured memory provider not registered:" ,
250+ ) ;
251+ } ) ;
252+ } ) ;
253+
254+ it ( "omits the check when no config is provided" , async ( ) => {
255+ await withStateDirEnv ( "openclaw-status-memory-embed-no-config-" , async ( ) => {
256+ resolveReadOnlyChannelPluginsForConfigMock . mockReturnValue ( {
257+ loadFailures : [ ] ,
258+ missingConfiguredChannelIds : [ ] ,
259+ } as never ) ;
260+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) , "empty" , "default" , "/tmp/ws" ) ;
261+
262+ const snapshot = await collectInstalledPluginHealthSnapshot ( { workspaceDir : "/tmp/ws" } ) ;
263+
264+ expect ( snapshot . unregisteredMemoryEmbeddingProviders ) . toBeUndefined ( ) ;
265+ expect ( collectUnregisteredConfiguredMemoryEmbeddingProvidersMock ) . not . toHaveBeenCalled ( ) ;
266+ } ) ;
267+ } ) ;
268+ } ) ;
0 commit comments