@@ -1119,6 +1119,103 @@ describe('HttpDispatcher', () => {
11191119 expect ( updatePackage ) . toHaveBeenCalledWith ( { packageId : 'a.b' , patch : { description : 'hi' } } ) ;
11201120 } ) ;
11211121
1122+ it ( 'POST /packages creates a new package (201) after checking the id is free' , async ( ) => {
1123+ const installPackage = vi
1124+ . fn ( )
1125+ . mockReturnValue ( { manifest : { id : 'com.acme.new' , name : 'New' , version : '0.1.0' } } ) ;
1126+ const mockRegistry = {
1127+ getPackage : vi . fn ( ) . mockReturnValue ( undefined ) ,
1128+ installPackage,
1129+ getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1130+ } ;
1131+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1132+ if ( name === 'objectql' ) return Promise . resolve ( { registry : mockRegistry } ) ;
1133+ return null ; // no protocol service → fall back to registry.installPackage
1134+ } ) ;
1135+
1136+ const result = await dispatcher . handlePackages (
1137+ '' ,
1138+ 'POST' ,
1139+ { manifest : { id : 'com.acme.new' , name : 'New' , version : '0.1.0' , type : 'app' } } ,
1140+ { } ,
1141+ { request : { } } ,
1142+ ) ;
1143+ expect ( result . response ?. status ) . toBe ( 201 ) ;
1144+ expect ( mockRegistry . getPackage ) . toHaveBeenCalledWith ( 'com.acme.new' ) ;
1145+ expect ( installPackage ) . toHaveBeenCalled ( ) ;
1146+ } ) ;
1147+
1148+ it ( 'POST /packages rejects a duplicate id with 409 instead of silently overwriting' , async ( ) => {
1149+ const installPackage = vi . fn ( ) ;
1150+ const mockRegistry = {
1151+ getPackage : vi . fn ( ) . mockReturnValue ( { manifest : { id : 'com.acme.crm' , name : 'Existing' } } ) ,
1152+ installPackage,
1153+ getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1154+ } ;
1155+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1156+ if ( name === 'objectql' ) return Promise . resolve ( { registry : mockRegistry } ) ;
1157+ return null ;
1158+ } ) ;
1159+
1160+ const result = await dispatcher . handlePackages (
1161+ '' ,
1162+ 'POST' ,
1163+ { manifest : { id : 'com.acme.crm' , name : 'Clobber' , version : '9.9.9' } } ,
1164+ { } ,
1165+ { request : { } } ,
1166+ ) ;
1167+ expect ( result . response ?. status ) . toBe ( 409 ) ;
1168+ // The existing manifest must NOT be overwritten.
1169+ expect ( installPackage ) . not . toHaveBeenCalled ( ) ;
1170+ } ) ;
1171+
1172+ it ( 'POST /packages?overwrite=true allows intentional overwrite of an existing id' , async ( ) => {
1173+ const installPackage = vi
1174+ . fn ( )
1175+ . mockReturnValue ( { manifest : { id : 'com.acme.crm' , name : 'Upgraded' , version : '2.0.0' } } ) ;
1176+ const mockRegistry = {
1177+ getPackage : vi . fn ( ) . mockReturnValue ( { manifest : { id : 'com.acme.crm' } } ) ,
1178+ installPackage,
1179+ getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1180+ } ;
1181+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1182+ if ( name === 'objectql' ) return Promise . resolve ( { registry : mockRegistry } ) ;
1183+ return null ;
1184+ } ) ;
1185+
1186+ const result = await dispatcher . handlePackages (
1187+ '' ,
1188+ 'POST' ,
1189+ { manifest : { id : 'com.acme.crm' , name : 'Upgraded' , version : '2.0.0' } } ,
1190+ { overwrite : 'true' } ,
1191+ { request : { } } ,
1192+ ) ;
1193+ expect ( result . response ?. status ) . toBe ( 201 ) ;
1194+ expect ( installPackage ) . toHaveBeenCalled ( ) ;
1195+ } ) ;
1196+
1197+ it ( 'POST /packages rejects a missing id with 400' , async ( ) => {
1198+ const mockRegistry = {
1199+ getPackage : vi . fn ( ) ,
1200+ installPackage : vi . fn ( ) ,
1201+ getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1202+ } ;
1203+ ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
1204+ if ( name === 'objectql' ) return Promise . resolve ( { registry : mockRegistry } ) ;
1205+ return null ;
1206+ } ) ;
1207+
1208+ const result = await dispatcher . handlePackages (
1209+ '' ,
1210+ 'POST' ,
1211+ { manifest : { name : 'No Id' } } ,
1212+ { } ,
1213+ { request : { } } ,
1214+ ) ;
1215+ expect ( result . response ?. status ) . toBe ( 400 ) ;
1216+ expect ( mockRegistry . installPackage ) . not . toHaveBeenCalled ( ) ;
1217+ } ) ;
1218+
11221219 it ( 'PATCH /packages/:id rejects an empty patch with 400' , async ( ) => {
11231220 ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
11241221 if ( name === 'objectql' ) return Promise . resolve ( { registry : { getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) } } ) ;
@@ -1409,7 +1506,11 @@ describe('HttpDispatcher', () => {
14091506 package : { manifest : { id : 'app.demo' } , status : 'installed' } ,
14101507 message : 'Installed package: app.demo' ,
14111508 } ) ;
1412- const mockRegistry = { installPackage : vi . fn ( ) , getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) } ;
1509+ const mockRegistry = {
1510+ installPackage : vi . fn ( ) ,
1511+ getPackage : vi . fn ( ) . mockReturnValue ( undefined ) ,
1512+ getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1513+ } ;
14131514 ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
14141515 if ( name === 'protocol' ) return Promise . resolve ( { installPackage } ) ;
14151516 if ( name === 'objectql' ) return Promise . resolve ( { registry : mockRegistry } ) ;
@@ -1429,6 +1530,7 @@ describe('HttpDispatcher', () => {
14291530 it ( 'falls back to registry.installPackage when the protocol lacks the method' , async ( ) => {
14301531 const mockRegistry = {
14311532 installPackage : vi . fn ( ) . mockReturnValue ( { manifest : { id : 'app.fb' } , status : 'installed' } ) ,
1533+ getPackage : vi . fn ( ) . mockReturnValue ( undefined ) ,
14321534 getAllPackages : vi . fn ( ) . mockReturnValue ( [ ] ) ,
14331535 } ;
14341536 ( kernel as any ) . getService = vi . fn ( ) . mockImplementation ( ( name : string ) => {
0 commit comments