@@ -822,4 +822,174 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
822822 expect ( singleCalls ) . toContain ( 'nb__item' ) ;
823823 } ) ;
824824 } ) ;
825+
826+ describe ( 'Cold-Start Metadata Restoration' , ( ) => {
827+ it ( 'should restore metadata from sys_metadata via protocol.loadMetaFromDb on start' , async ( ) => {
828+ // Arrange — a driver whose find() returns persisted metadata records
829+ const findCalls : Array < { object : string ; query : any } > = [ ] ;
830+ const mockDriver = {
831+ name : 'restore-driver' ,
832+ version : '1.0.0' ,
833+ connect : async ( ) => { } ,
834+ disconnect : async ( ) => { } ,
835+ find : async ( object : string , query : any ) => {
836+ findCalls . push ( { object, query } ) ;
837+ if ( object === 'sys_metadata' ) {
838+ return [
839+ {
840+ id : '1' ,
841+ type : 'apps' ,
842+ name : 'custom_crm' ,
843+ state : 'active' ,
844+ metadata : JSON . stringify ( { name : 'custom_crm' , label : 'Custom CRM' } ) ,
845+ } ,
846+ {
847+ id : '2' ,
848+ type : 'object' ,
849+ name : 'invoice' ,
850+ state : 'active' ,
851+ metadata : JSON . stringify ( {
852+ name : 'invoice' ,
853+ label : 'Invoice' ,
854+ fields : { amount : { name : 'amount' , label : 'Amount' , type : 'number' } } ,
855+ } ) ,
856+ packageId : 'user_pkg' ,
857+ } ,
858+ ] ;
859+ }
860+ return [ ] ;
861+ } ,
862+ findOne : async ( ) => null ,
863+ create : async ( _o : string , d : any ) => d ,
864+ update : async ( _o : string , _i : any , d : any ) => d ,
865+ delete : async ( ) => true ,
866+ syncSchema : async ( ) => { } ,
867+ } ;
868+
869+ await kernel . use ( {
870+ name : 'mock-restore-driver' ,
871+ type : 'driver' ,
872+ version : '1.0.0' ,
873+ init : async ( ctx ) => {
874+ ctx . registerService ( 'driver.restore' , mockDriver ) ;
875+ } ,
876+ } ) ;
877+
878+ const plugin = new ObjectQLPlugin ( ) ;
879+ await kernel . use ( plugin ) ;
880+
881+ // Act
882+ await kernel . bootstrap ( ) ;
883+
884+ // Assert — sys_metadata should have been queried
885+ const metaQuery = findCalls . find ( ( c ) => c . object === 'sys_metadata' ) ;
886+ expect ( metaQuery ) . toBeDefined ( ) ;
887+ expect ( metaQuery ! . query . where ) . toEqual ( { state : 'active' } ) ;
888+
889+ // Assert — items should be restored into the registry
890+ const registry = ( kernel . getService ( 'objectql' ) as any ) . registry ;
891+ expect ( registry . getAllApps ( ) ) . toContainEqual ( {
892+ name : 'custom_crm' ,
893+ label : 'Custom CRM' ,
894+ } ) ;
895+ } ) ;
896+
897+ it ( 'should not throw when protocol.loadMetaFromDb fails (graceful degradation)' , async ( ) => {
898+ // Arrange — driver that throws on find('sys_metadata')
899+ const mockDriver = {
900+ name : 'failing-db-driver' ,
901+ version : '1.0.0' ,
902+ connect : async ( ) => { } ,
903+ disconnect : async ( ) => { } ,
904+ find : async ( object : string ) => {
905+ if ( object === 'sys_metadata' ) {
906+ throw new Error ( 'SQLITE_ERROR: no such table: sys_metadata' ) ;
907+ }
908+ return [ ] ;
909+ } ,
910+ findOne : async ( ) => null ,
911+ create : async ( _o : string , d : any ) => d ,
912+ update : async ( _o : string , _i : any , d : any ) => d ,
913+ delete : async ( ) => true ,
914+ syncSchema : async ( ) => { } ,
915+ } ;
916+
917+ await kernel . use ( {
918+ name : 'mock-fail-driver' ,
919+ type : 'driver' ,
920+ version : '1.0.0' ,
921+ init : async ( ctx ) => {
922+ ctx . registerService ( 'driver.faildb' , mockDriver ) ;
923+ } ,
924+ } ) ;
925+
926+ const plugin = new ObjectQLPlugin ( ) ;
927+ await kernel . use ( plugin ) ;
928+
929+ // Act & Assert — should not throw
930+ await expect ( kernel . bootstrap ( ) ) . resolves . not . toThrow ( ) ;
931+ } ) ;
932+
933+ it ( 'should restore metadata before syncRegisteredSchemas so restored objects get table sync' , async ( ) => {
934+ // Arrange — track the order of operations
935+ const operations : string [ ] = [ ] ;
936+ const mockDriver = {
937+ name : 'order-driver' ,
938+ version : '1.0.0' ,
939+ connect : async ( ) => { } ,
940+ disconnect : async ( ) => { } ,
941+ find : async ( object : string ) => {
942+ if ( object === 'sys_metadata' ) {
943+ operations . push ( 'loadMetaFromDb' ) ;
944+ return [
945+ {
946+ id : '1' ,
947+ type : 'object' ,
948+ name : 'restored_obj' ,
949+ state : 'active' ,
950+ metadata : JSON . stringify ( {
951+ name : 'restored_obj' ,
952+ label : 'Restored Object' ,
953+ fields : { title : { name : 'title' , label : 'Title' , type : 'text' } } ,
954+ } ) ,
955+ packageId : 'user_pkg' ,
956+ } ,
957+ ] ;
958+ }
959+ return [ ] ;
960+ } ,
961+ findOne : async ( ) => null ,
962+ create : async ( _o : string , d : any ) => d ,
963+ update : async ( _o : string , _i : any , d : any ) => d ,
964+ delete : async ( ) => true ,
965+ syncSchema : async ( object : string ) => {
966+ operations . push ( `syncSchema:${ object } ` ) ;
967+ } ,
968+ } ;
969+
970+ await kernel . use ( {
971+ name : 'mock-order-driver' ,
972+ type : 'driver' ,
973+ version : '1.0.0' ,
974+ init : async ( ctx ) => {
975+ ctx . registerService ( 'driver.order' , mockDriver ) ;
976+ } ,
977+ } ) ;
978+
979+ const plugin = new ObjectQLPlugin ( ) ;
980+ await kernel . use ( plugin ) ;
981+
982+ // Act
983+ await kernel . bootstrap ( ) ;
984+
985+ // Assert — loadMetaFromDb must appear before any syncSchema call
986+ const loadIdx = operations . indexOf ( 'loadMetaFromDb' ) ;
987+ expect ( loadIdx ) . toBeGreaterThanOrEqual ( 0 ) ;
988+
989+ const firstSync = operations . findIndex ( ( op ) => op . startsWith ( 'syncSchema:' ) ) ;
990+ if ( firstSync >= 0 ) {
991+ expect ( loadIdx ) . toBeLessThan ( firstSync ) ;
992+ }
993+ } ) ;
994+ } ) ;
825995} ) ;
0 commit comments