@@ -1022,6 +1022,59 @@ describe('HttpDispatcher', () => {
10221022 expect ( insert ) . toHaveBeenCalledTimes ( 2 ) ;
10231023 expect ( insert ) . toHaveBeenCalledWith ( 'project' , expect . objectContaining ( { name : 'Apollo' } ) , expect . anything ( ) ) ;
10241024 } ) ;
1025+
1026+ // ADR-0045: "Publish" = live AND visible. A materialized (additive)
1027+ // build leaves its app at hidden:true; publish-drafts must flip it so
1028+ // one publish verb serves both the draft and the materialize regimes.
1029+ it ( 'POST /packages/:id/publish-drafts unhides the package\'s hidden app' , async ( ) => {
1030+ const publishPackageDrafts = vi . fn ( ) . mockResolvedValue ( {
1031+ success : true , publishedCount : 0 , failedCount : 0 , published : [ ] , failed : [ ] , seedApplied : { success : true } ,
1032+ } ) ;
1033+ const getMetaItems = vi . fn ( ) . mockResolvedValue ( [
1034+ { name : 'production_management' , label : '生产管理' , hidden : true , navigation : [ ] } ,
1035+ { name : 'already_visible' , hidden : false , navigation : [ ] } ,
1036+ ] ) ;
1037+ const saveMetaItem = vi . fn ( ) . mockResolvedValue ( { ok : true } ) ;
1038+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1039+ if ( name === 'protocol' ) return Promise . resolve ( { publishPackageDrafts, getMetaItems, saveMetaItem } ) ;
1040+ if ( name === 'objectql' ) return Promise . resolve ( { registry : { getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) } } ) ;
1041+ return null ;
1042+ } ) ;
1043+
1044+ const result = await dispatcher . handlePackages ( '/app.production_management/publish-drafts' , 'POST' , { } , { } , { request : { } } ) ;
1045+
1046+ expect ( result . response ?. status ) . toBe ( 200 ) ;
1047+ expect ( getMetaItems ) . toHaveBeenCalledWith ( expect . objectContaining ( { type : 'app' , packageId : 'app.production_management' } ) ) ;
1048+ // Only the hidden app is re-saved, with hidden:false and everything else intact.
1049+ expect ( saveMetaItem ) . toHaveBeenCalledTimes ( 1 ) ;
1050+ expect ( saveMetaItem ) . toHaveBeenCalledWith ( expect . objectContaining ( {
1051+ type : 'app' ,
1052+ name : 'production_management' ,
1053+ item : expect . objectContaining ( { hidden : false , label : '生产管理' } ) ,
1054+ packageId : 'app.production_management' ,
1055+ } ) ) ;
1056+ expect ( ( result . response as any ) ?. body ?. data ?. unhiddenApps ) . toEqual ( [ 'production_management' ] ) ;
1057+ } ) ;
1058+
1059+ it ( 'POST /packages/:id/publish-drafts reports (not throws) when the visibility flip fails' , async ( ) => {
1060+ const publishPackageDrafts = vi . fn ( ) . mockResolvedValue ( {
1061+ success : true , publishedCount : 1 , failedCount : 0 , published : [ ] , failed : [ ] , seedApplied : { success : true } ,
1062+ } ) ;
1063+ const getMetaItems = vi . fn ( ) . mockRejectedValue ( new Error ( 'meta backend down' ) ) ;
1064+ const saveMetaItem = vi . fn ( ) ;
1065+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1066+ if ( name === 'protocol' ) return Promise . resolve ( { publishPackageDrafts, getMetaItems, saveMetaItem } ) ;
1067+ if ( name === 'objectql' ) return Promise . resolve ( { registry : { getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) } } ) ;
1068+ return null ;
1069+ } ) ;
1070+
1071+ const result = await dispatcher . handlePackages ( '/app.edu/publish-drafts' , 'POST' , { } , { } , { request : { } } ) ;
1072+
1073+ // The draft publish itself succeeded — the flip failure is surfaced, not fatal.
1074+ expect ( result . response ?. status ) . toBe ( 200 ) ;
1075+ expect ( ( result . response as any ) ?. body ?. data ?. unhideError ) . toBe ( 'meta backend down' ) ;
1076+ expect ( saveMetaItem ) . not . toHaveBeenCalled ( ) ;
1077+ } ) ;
10251078 } ) ;
10261079
10271080 // ═══════════════════════════════════════════════════════════════
0 commit comments