@@ -693,4 +693,201 @@ describe('MetadataManager — IMetadataService Contract', () => {
693693 return deps . then ( result => expect ( result ) . toHaveLength ( 1 ) ) ;
694694 } ) ;
695695 } ) ;
696+
697+ // ==========================================
698+ // Package Publish / Revert / getPublished
699+ // ==========================================
700+
701+ describe ( 'publishPackage' , ( ) => {
702+ it ( 'should publish all items in a package' , async ( ) => {
703+ await manager . register ( 'object' , 'opportunity' , {
704+ name : 'opportunity' , label : 'Opportunity' , packageId : 'com.acme.crm' , state : 'draft' ,
705+ metadata : { fields : [ 'name' , 'amount' ] } ,
706+ } ) ;
707+ await manager . register ( 'view' , 'opp_list' , {
708+ name : 'opp_list' , label : 'Opp List' , packageId : 'com.acme.crm' , state : 'draft' ,
709+ metadata : { columns : [ 'name' , 'amount' ] } ,
710+ } ) ;
711+
712+ const result = await manager . publishPackage ( 'com.acme.crm' , { publishedBy : 'admin' } ) ;
713+
714+ expect ( result . success ) . toBe ( true ) ;
715+ expect ( result . packageId ) . toBe ( 'com.acme.crm' ) ;
716+ expect ( result . version ) . toBe ( 1 ) ;
717+ expect ( result . itemsPublished ) . toBe ( 2 ) ;
718+ expect ( result . publishedAt ) . toBeDefined ( ) ;
719+
720+ // Verify items are now active with published snapshots
721+ const obj = await manager . get ( 'object' , 'opportunity' ) as any ;
722+ expect ( obj . state ) . toBe ( 'active' ) ;
723+ expect ( obj . publishedDefinition ) . toBeDefined ( ) ;
724+ expect ( obj . publishedBy ) . toBe ( 'admin' ) ;
725+ expect ( obj . publishedAt ) . toBeDefined ( ) ;
726+
727+ const view = await manager . get ( 'view' , 'opp_list' ) as any ;
728+ expect ( view . state ) . toBe ( 'active' ) ;
729+ expect ( view . publishedDefinition ) . toBeDefined ( ) ;
730+ } ) ;
731+
732+ it ( 'should increment version on each publish' , async ( ) => {
733+ await manager . register ( 'object' , 'account' , {
734+ name : 'account' , packageId : 'crm' , state : 'draft' , version : 0 ,
735+ metadata : { fields : [ 'name' ] } ,
736+ } ) ;
737+
738+ const first = await manager . publishPackage ( 'crm' ) ;
739+ expect ( first . version ) . toBe ( 1 ) ;
740+
741+ const second = await manager . publishPackage ( 'crm' ) ;
742+ expect ( second . version ) . toBe ( 2 ) ;
743+ } ) ;
744+
745+ it ( 'should fail for empty package' , async ( ) => {
746+ const result = await manager . publishPackage ( 'nonexistent' ) ;
747+ expect ( result . success ) . toBe ( false ) ;
748+ expect ( result . itemsPublished ) . toBe ( 0 ) ;
749+ expect ( result . validationErrors ) . toBeDefined ( ) ;
750+ } ) ;
751+
752+ it ( 'should fail validation when items are invalid' , async ( ) => {
753+ // Register an item without a name (will fail validate)
754+ await manager . register ( 'object' , 'bad_item' , {
755+ packageId : 'com.acme.bad' , state : 'draft' ,
756+ metadata : { } ,
757+ } ) ;
758+
759+ const result = await manager . publishPackage ( 'com.acme.bad' , { validate : true } ) ;
760+ expect ( result . success ) . toBe ( false ) ;
761+ expect ( result . validationErrors ) . toBeDefined ( ) ;
762+ expect ( result . validationErrors ! . length ) . toBeGreaterThan ( 0 ) ;
763+ } ) ;
764+
765+ it ( 'should skip validation when validate=false' , async ( ) => {
766+ await manager . register ( 'object' , 'skip_val' , {
767+ packageId : 'com.acme.skip' , state : 'draft' ,
768+ metadata : { } ,
769+ } ) ;
770+
771+ const result = await manager . publishPackage ( 'com.acme.skip' , { validate : false } ) ;
772+ expect ( result . success ) . toBe ( true ) ;
773+ expect ( result . itemsPublished ) . toBe ( 1 ) ;
774+ } ) ;
775+ } ) ;
776+
777+ describe ( 'revertPackage' , ( ) => {
778+ it ( 'should revert to last published state' , async ( ) => {
779+ // Register and publish
780+ await manager . register ( 'object' , 'account' , {
781+ name : 'account' , label : 'Account' , packageId : 'crm' ,
782+ metadata : { fields : [ 'name' , 'email' ] } ,
783+ } ) ;
784+ await manager . publishPackage ( 'crm' ) ;
785+
786+ // Make edits after publish
787+ const item = await manager . get ( 'object' , 'account' ) as any ;
788+ await manager . register ( 'object' , 'account' , {
789+ ...item ,
790+ metadata : { fields : [ 'name' , 'email' , 'phone' ] } ,
791+ state : 'draft' ,
792+ } ) ;
793+
794+ // Verify edit was saved
795+ const edited = await manager . get ( 'object' , 'account' ) as any ;
796+ expect ( edited . metadata . fields ) . toContain ( 'phone' ) ;
797+
798+ // Revert
799+ await manager . revertPackage ( 'crm' ) ;
800+
801+ // Verify reverted to published state
802+ const reverted = await manager . get ( 'object' , 'account' ) as any ;
803+ expect ( reverted . state ) . toBe ( 'active' ) ;
804+ expect ( reverted . metadata ) . toEqual ( reverted . publishedDefinition ) ;
805+ } ) ;
806+
807+ it ( 'should throw for non-existent package' , async ( ) => {
808+ await expect ( manager . revertPackage ( 'nonexistent' ) ) . rejects . toThrow ( 'No metadata items found' ) ;
809+ } ) ;
810+
811+ it ( 'should throw for never-published package' , async ( ) => {
812+ await manager . register ( 'object' , 'new_item' , {
813+ name : 'new_item' , packageId : 'com.acme.new' ,
814+ } ) ;
815+
816+ await expect ( manager . revertPackage ( 'com.acme.new' ) ) . rejects . toThrow ( 'has never been published' ) ;
817+ } ) ;
818+ } ) ;
819+
820+ describe ( 'getPublished' , ( ) => {
821+ it ( 'should return published definition when available' , async ( ) => {
822+ await manager . register ( 'object' , 'account' , {
823+ name : 'account' , label : 'Account' , packageId : 'crm' ,
824+ metadata : { fields : [ 'name' ] } ,
825+ } ) ;
826+ await manager . publishPackage ( 'crm' ) ;
827+
828+ // Edit after publish
829+ const item = await manager . get ( 'object' , 'account' ) as any ;
830+ await manager . register ( 'object' , 'account' , {
831+ ...item ,
832+ metadata : { fields : [ 'name' , 'email' , 'phone' ] } ,
833+ } ) ;
834+
835+ // getPublished should return the published snapshot, not the edited version
836+ const published = await manager . getPublished ( 'object' , 'account' ) ;
837+ expect ( published ) . toBeDefined ( ) ;
838+ // The published snapshot was taken from the original metadata
839+ const pubAny = published as any ;
840+ expect ( pubAny . fields ) . toBeDefined ( ) ;
841+ } ) ;
842+
843+ it ( 'should return current definition when never published' , async ( ) => {
844+ await manager . register ( 'object' , 'contact' , {
845+ name : 'contact' , label : 'Contact' ,
846+ metadata : { fields : [ 'first_name' ] } ,
847+ } ) ;
848+
849+ const published = await manager . getPublished ( 'object' , 'contact' ) ;
850+ expect ( published ) . toBeDefined ( ) ;
851+ // Falls back to metadata field
852+ expect ( ( published as any ) . fields ) . toEqual ( [ 'first_name' ] ) ;
853+ } ) ;
854+
855+ it ( 'should return undefined for non-existent item' , async ( ) => {
856+ const result = await manager . getPublished ( 'object' , 'nonexistent' ) ;
857+ expect ( result ) . toBeUndefined ( ) ;
858+ } ) ;
859+ } ) ;
860+
861+ describe ( 'integration: edit → publish → edit → revert' , ( ) => {
862+ it ( 'should preserve published version through edit-revert cycle' , async ( ) => {
863+ // Step 1: Initial setup
864+ await manager . register ( 'object' , 'project' , {
865+ name : 'project' , label : 'Project' , packageId : 'pm' ,
866+ metadata : { fields : [ 'name' , 'status' ] } ,
867+ } ) ;
868+
869+ // Step 2: Publish v1
870+ const v1 = await manager . publishPackage ( 'pm' , { publishedBy : 'admin' } ) ;
871+ expect ( v1 . success ) . toBe ( true ) ;
872+ expect ( v1 . version ) . toBe ( 1 ) ;
873+
874+ // Step 3: Edit after publish
875+ const item = await manager . get ( 'object' , 'project' ) as any ;
876+ await manager . register ( 'object' , 'project' , {
877+ ...item ,
878+ metadata : { fields : [ 'name' , 'status' , 'priority' ] } ,
879+ state : 'draft' ,
880+ } ) ;
881+
882+ // Step 4: End user sees published version
883+ const endUserView = await manager . getPublished ( 'object' , 'project' ) as any ;
884+ expect ( endUserView . fields ) . toEqual ( [ 'name' , 'status' ] ) ;
885+
886+ // Step 5: Revert discards draft changes
887+ await manager . revertPackage ( 'pm' ) ;
888+ const reverted = await manager . get ( 'object' , 'project' ) as any ;
889+ expect ( reverted . state ) . toBe ( 'active' ) ;
890+ expect ( reverted . metadata . fields ) . toEqual ( [ 'name' , 'status' ] ) ;
891+ } ) ;
892+ } ) ;
696893} ) ;
0 commit comments