@@ -226,6 +226,155 @@ describe('MetadataManager', () => {
226226 } ) ;
227227 } ) ;
228228
229+ describe ( 'register — loader protocol filtering' , ( ) => {
230+ it ( 'should persist to datasource: protocol loaders' , async ( ) => {
231+ const dbLoader : MetadataLoader = {
232+ contract : { name : 'database' , protocol : 'datasource:' as const , capabilities : { read : true , write : true , watch : false , list : true } } ,
233+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
234+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
235+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
236+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
237+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
238+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
239+ } ;
240+
241+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ dbLoader ] } ) ;
242+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
243+
244+ expect ( dbLoader . save ) . toHaveBeenCalledWith ( 'object' , 'account' , { name : 'account' } ) ;
245+ } ) ;
246+
247+ it ( 'should NOT persist to file: protocol loaders' , async ( ) => {
248+ const fsLoader : MetadataLoader = {
249+ contract : { name : 'filesystem' , protocol : 'file:' as const , capabilities : { read : true , write : true , watch : true , list : true } } ,
250+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
251+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
252+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
253+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
254+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
255+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
256+ } ;
257+
258+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ fsLoader ] } ) ;
259+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
260+
261+ expect ( fsLoader . save ) . not . toHaveBeenCalled ( ) ;
262+ } ) ;
263+
264+ it ( 'should NOT persist to memory: protocol loaders' , async ( ) => {
265+ const memLoader : MetadataLoader = {
266+ contract : { name : 'memory' , protocol : 'memory:' as const , capabilities : { read : true , write : true , watch : false , list : true } } ,
267+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
268+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
269+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
270+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
271+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
272+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
273+ } ;
274+
275+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ memLoader ] } ) ;
276+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
277+
278+ expect ( memLoader . save ) . not . toHaveBeenCalled ( ) ;
279+ } ) ;
280+
281+ it ( 'should NOT persist to datasource: protocol loaders with write: false' , async ( ) => {
282+ const readOnlyDbLoader : MetadataLoader = {
283+ contract : { name : 'database-ro' , protocol : 'datasource:' as const , capabilities : { read : true , write : false , watch : false , list : true } } ,
284+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
285+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
286+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
287+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
288+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
289+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
290+ } ;
291+
292+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ readOnlyDbLoader ] } ) ;
293+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
294+
295+ expect ( readOnlyDbLoader . save ) . not . toHaveBeenCalled ( ) ;
296+ } ) ;
297+
298+ it ( 'should still store in in-memory registry regardless of loaders' , async ( ) => {
299+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ ] } ) ;
300+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
301+
302+ const result = await m . get ( 'object' , 'account' ) ;
303+ expect ( result ) . toEqual ( { name : 'account' } ) ;
304+ } ) ;
305+ } ) ;
306+
307+ describe ( 'unregister — loader protocol filtering' , ( ) => {
308+ it ( 'should delete from datasource: protocol loaders' , async ( ) => {
309+ const deleteFn = vi . fn ( ) ;
310+ const dbLoader : MetadataLoader = {
311+ contract : { name : 'database' , protocol : 'datasource:' as const , capabilities : { read : true , write : true , watch : false , list : true } } ,
312+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
313+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
314+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
315+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
316+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
317+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
318+ delete : deleteFn ,
319+ } as any ;
320+
321+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ dbLoader ] } ) ;
322+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
323+ await m . unregister ( 'object' , 'account' ) ;
324+
325+ expect ( deleteFn ) . toHaveBeenCalledWith ( 'object' , 'account' ) ;
326+ } ) ;
327+
328+ it ( 'should NOT delete from file: protocol loaders' , async ( ) => {
329+ const deleteFn = vi . fn ( ) ;
330+ const fsLoader : MetadataLoader = {
331+ contract : { name : 'filesystem' , protocol : 'file:' as const , capabilities : { read : true , write : true , watch : true , list : true } } ,
332+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
333+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
334+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
335+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
336+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
337+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
338+ delete : deleteFn ,
339+ } as any ;
340+
341+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ fsLoader ] } ) ;
342+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
343+ await m . unregister ( 'object' , 'account' ) ;
344+
345+ expect ( deleteFn ) . not . toHaveBeenCalled ( ) ;
346+ } ) ;
347+
348+ it ( 'should NOT delete from datasource: protocol loaders with write: false' , async ( ) => {
349+ const deleteFn = vi . fn ( ) ;
350+ const readOnlyDbLoader : MetadataLoader = {
351+ contract : { name : 'database-ro' , protocol : 'datasource:' as const , capabilities : { read : true , write : false , watch : false , list : true } } ,
352+ load : vi . fn ( ) . mockResolvedValue ( { data : null } ) ,
353+ loadMany : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
354+ exists : vi . fn ( ) . mockResolvedValue ( false ) ,
355+ stat : vi . fn ( ) . mockResolvedValue ( null ) ,
356+ list : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
357+ save : vi . fn ( ) . mockResolvedValue ( { success : true } ) ,
358+ delete : deleteFn ,
359+ } as any ;
360+
361+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ readOnlyDbLoader ] } ) ;
362+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
363+ await m . unregister ( 'object' , 'account' ) ;
364+
365+ expect ( deleteFn ) . not . toHaveBeenCalled ( ) ;
366+ } ) ;
367+
368+ it ( 'should remove from in-memory registry' , async ( ) => {
369+ const m = new MetadataManager ( { formats : [ 'json' ] , loaders : [ ] } ) ;
370+ await m . register ( 'object' , 'account' , { name : 'account' } ) ;
371+ expect ( await m . get ( 'object' , 'account' ) ) . toEqual ( { name : 'account' } ) ;
372+
373+ await m . unregister ( 'object' , 'account' ) ;
374+ expect ( await m . get ( 'object' , 'account' ) ) . toBeUndefined ( ) ;
375+ } ) ;
376+ } ) ;
377+
229378 describe ( 'registerLoader' , ( ) => {
230379 it ( 'should register a new loader' , async ( ) => {
231380 const newLoader = new MemoryLoader ( ) ;
@@ -340,6 +489,7 @@ describe('MetadataPlugin', () => {
340489 registerLoader = vi . fn ( ) ;
341490 stopWatching = vi . fn ( ) ;
342491 setTypeRegistry = vi . fn ( ) ;
492+ setDatabaseDriver = vi . fn ( ) ;
343493 register = vi . fn ( ) ;
344494 } ;
345495 return { NodeMetadataManager : MockNodeMetadataManager } ;
@@ -382,6 +532,83 @@ describe('MetadataPlugin', () => {
382532 // start should call logger.info at least once
383533 expect ( ctx . logger . info ) . toHaveBeenCalled ( ) ;
384534 } ) ;
535+
536+ it ( 'should bridge driver service to MetadataManager in start()' , async ( ) => {
537+ const { MetadataPlugin } = await import ( './plugin.js' ) ;
538+ const plugin = new MetadataPlugin ( { rootDir : '/tmp/test' , watch : false } ) ;
539+
540+ const mockDriver = { name : 'mock-driver' , find : vi . fn ( ) , create : vi . fn ( ) } ;
541+ const services = new Map < string , any > ( ) ;
542+ services . set ( 'driver.mock-driver' , mockDriver ) ;
543+
544+ const ctx = createMockPluginContext ( ) ;
545+ ctx . getServices = vi . fn ( ) . mockReturnValue ( services ) ;
546+
547+ await plugin . init ( ctx ) ;
548+ await plugin . start ( ctx ) ;
549+
550+ // Verify setDatabaseDriver was called on the manager with the driver
551+ const manager = ( plugin as any ) . manager ;
552+ expect ( manager . setDatabaseDriver ) . toHaveBeenCalledWith ( mockDriver ) ;
553+ } ) ;
554+
555+ it ( 'should bridge driver AFTER filesystem metadata loading' , async ( ) => {
556+ const { MetadataPlugin } = await import ( './plugin.js' ) ;
557+ const plugin = new MetadataPlugin ( { rootDir : '/tmp/test' , watch : false } ) ;
558+
559+ const callOrder : string [ ] = [ ] ;
560+ const mockDriver = { name : 'mock-driver' , find : vi . fn ( ) , create : vi . fn ( ) } ;
561+ const services = new Map < string , any > ( ) ;
562+ services . set ( 'driver.mock-driver' , mockDriver ) ;
563+
564+ const manager = ( plugin as any ) . manager ;
565+ manager . loadMany = vi . fn ( ) . mockImplementation ( async ( ) => {
566+ callOrder . push ( 'loadMany' ) ;
567+ return [ ] ;
568+ } ) ;
569+ manager . setDatabaseDriver = vi . fn ( ) . mockImplementation ( ( ) => {
570+ callOrder . push ( 'setDatabaseDriver' ) ;
571+ } ) ;
572+
573+ const ctx = createMockPluginContext ( ) ;
574+ ctx . getServices = vi . fn ( ) . mockReturnValue ( services ) ;
575+
576+ await plugin . init ( ctx ) ;
577+ await plugin . start ( ctx ) ;
578+
579+ // setDatabaseDriver must be called after all loadMany calls
580+ const lastLoad = callOrder . lastIndexOf ( 'loadMany' ) ;
581+ const driverIdx = callOrder . indexOf ( 'setDatabaseDriver' ) ;
582+ expect ( driverIdx ) . toBeGreaterThan ( lastLoad ) ;
583+ } ) ;
584+
585+ it ( 'should not fail when no driver service is available' , async ( ) => {
586+ const { MetadataPlugin } = await import ( './plugin.js' ) ;
587+ const plugin = new MetadataPlugin ( { rootDir : '/tmp/test' , watch : false } ) ;
588+
589+ const ctx = createMockPluginContext ( ) ;
590+ ctx . getServices = vi . fn ( ) . mockReturnValue ( new Map ( ) ) ;
591+
592+ await plugin . init ( ctx ) ;
593+ // Should not throw
594+ await expect ( plugin . start ( ctx ) ) . resolves . not . toThrow ( ) ;
595+
596+ // setDatabaseDriver should not have been called
597+ const manager = ( plugin as any ) . manager ;
598+ expect ( manager . setDatabaseDriver ) . not . toHaveBeenCalled ( ) ;
599+ } ) ;
600+
601+ it ( 'should gracefully handle getServices errors' , async ( ) => {
602+ const { MetadataPlugin } = await import ( './plugin.js' ) ;
603+ const plugin = new MetadataPlugin ( { rootDir : '/tmp/test' , watch : false } ) ;
604+
605+ const ctx = createMockPluginContext ( ) ;
606+ ctx . getServices = vi . fn ( ) . mockImplementation ( ( ) => { throw new Error ( 'services unavailable' ) ; } ) ;
607+
608+ await plugin . init ( ctx ) ;
609+ // Should not throw even when getServices fails
610+ await expect ( plugin . start ( ctx ) ) . resolves . not . toThrow ( ) ;
611+ } ) ;
385612} ) ;
386613
387614// ---------- Helpers ----------
0 commit comments