@@ -11,6 +11,7 @@ import { ApimServiceContext, ResourceDescriptor } from '../../../src/models/type
1111import { PublishConfig } from '../../../src/models/config.js' ;
1212import { LogLevel } from '../../../src/lib/logger.js' ;
1313import { applyOverrides } from '../../../src/services/override-merger.js' ;
14+ import { EnvMapping } from '../../../src/services/env-mapper.js' ;
1415
1516// Mock resource-publisher so we can verify call sequence
1617const mockPublishResource = vi . fn ( ) ;
@@ -479,6 +480,31 @@ describe('api-publisher', () => {
479480 expect ( client . putResource ) . toHaveBeenCalledTimes ( 1 ) ;
480481 } ) ;
481482
483+ it ( 'skips the revision artifact whose number equals the current root revision' , async ( ) => {
484+ const client = createMockClient ( ) ;
485+ const store = createMockStore ( [
486+ { type : ResourceType . Api , nameParts : [ 'orders-api;rev=2' ] } ,
487+ { type : ResourceType . Api , nameParts : [ 'orders-api;rev=3' ] } ,
488+ ] ) ;
489+ // Root is the current revision 3; the ;rev=3 artifact duplicates it.
490+ store . readResource . mockImplementation ( async ( _dir : string , d : ResourceDescriptor ) => {
491+ if ( d . type === ResourceType . Api && ! ( d . nameParts [ 0 ] ?? '' ) . includes ( ';rev=' ) ) {
492+ return { name : 'orders-api' , properties : { apiRevision : '3' , isCurrent : true } } ;
493+ }
494+ return null ;
495+ } ) ;
496+
497+ const apiDescriptor : ResourceDescriptor = { type : ResourceType . Api , nameParts : [ 'orders-api' ] } ;
498+
499+ await publishApi ( client , store , testContext , apiDescriptor , testConfig ) ;
500+
501+ const publishedRevs = mockPublishResource . mock . calls . map (
502+ ( c : unknown [ ] ) => ( c [ 3 ] as ResourceDescriptor ) . nameParts [ 0 ]
503+ ) ;
504+ expect ( publishedRevs ) . toContain ( 'orders-api;rev=2' ) ;
505+ expect ( publishedRevs ) . not . toContain ( 'orders-api;rev=3' ) ;
506+ } ) ;
507+
482508 it ( 'should replay root API without re-importing specification after revisions' , async ( ) => {
483509 const client = createMockClient ( ) ;
484510 const revisions = [ { type : ResourceType . Api , nameParts : [ 'orders-api;rev=2' ] } ] ;
@@ -596,6 +622,72 @@ describe('api-publisher', () => {
596622 expect ( mockPublishResource . mock . calls [ 0 ] [ 3 ] . nameParts [ 0 ] ) . toBe ( 'orders-api;rev=2' ) ;
597623 } ) ;
598624
625+ it ( 'fails the API publish when a revision publish fails (no silent success)' , async ( ) => {
626+ const client = createMockClient ( ) ;
627+ const store = createMockStore ( [
628+ { type : ResourceType . Api , nameParts : [ 'orders-api;rev=2' ] } ,
629+ ] ) ;
630+ // Root API publishes fine; the revision publish returns failed.
631+ mockPublishResource . mockResolvedValue ( {
632+ descriptor : { type : ResourceType . Api , nameParts : [ 'orders-api;rev=2' ] } ,
633+ status : 'failed' ,
634+ action : 'noop' ,
635+ error : new Error ( 'revision boom' ) ,
636+ } ) ;
637+
638+ const apiDescriptor : ResourceDescriptor = {
639+ type : ResourceType . Api ,
640+ nameParts : [ 'orders-api' ] ,
641+ } ;
642+
643+ const result = await publishApi ( client , store , testContext , apiDescriptor , testConfig ) ;
644+
645+ expect ( result . status ) . toBe ( 'failed' ) ;
646+ expect ( result . error ?. message ) . toContain ( 'orders-api;rev=2' ) ;
647+ } ) ;
648+
649+ it ( 'reconciles operations against the env-mapped API name after spec import' , async ( ) => {
650+ const client = createMockClient ( ) ;
651+ const store = createMockStore ( [
652+ { type : ResourceType . ApiOperation , nameParts : [ 'orders-api' , 'get-orders' ] } ,
653+ ] ) ;
654+ store . readResource . mockImplementation ( async ( _dir : string , d : ResourceDescriptor ) => {
655+ if ( d . type === ResourceType . Api && ! ( d . nameParts [ 0 ] ?? '' ) . includes ( ';rev=' ) ) {
656+ return { name : 'orders-api' , properties : { } } ;
657+ }
658+ if ( d . type === ResourceType . ApiOperation ) {
659+ return { name : 'get-orders' , properties : { displayName : 'Get Orders' , method : 'GET' , urlTemplate : '/orders' } } ;
660+ }
661+ return null ;
662+ } ) ;
663+ store . readContent . mockImplementation ( async ( _dir : string , _d : ResourceDescriptor , kind : string ) => {
664+ return kind === 'specification'
665+ ? { content : 'openapi: 3.0.0\ninfo:\n title: t\n version: "1"\npaths: {}\n' , format : 'yaml' }
666+ : undefined ;
667+ } ) ;
668+ // Execute the reconcile tasks so the PATCH target can be asserted.
669+ mockRunParallel . mockImplementation ( async ( tasks : Array < ( ) => Promise < unknown > > ) => {
670+ for ( const t of tasks ) await t ( ) ;
671+ } ) ;
672+
673+ const envMapping : EnvMapping = { prefix : 'dev-' , suffix : '-eu' , appliesTo : new Set ( [ ResourceType . Api ] ) } ;
674+ const config : PublishConfig = { ...testConfig , envMapping } ;
675+
676+ await publishApi (
677+ client , store , testContext ,
678+ { type : ResourceType . Api , nameParts : [ 'orders-api' ] } ,
679+ config
680+ ) ;
681+
682+ const opPatch = client . patchResource . mock . calls . find (
683+ ( c : unknown [ ] ) => ( c [ 1 ] as ResourceDescriptor ) . type === ResourceType . ApiOperation
684+ ) ;
685+ expect ( opPatch ) . toBeDefined ( ) ;
686+ // PATCH must target the affixed API name, not the canonical one.
687+ expect ( ( opPatch ! [ 1 ] as ResourceDescriptor ) . nameParts [ 0 ] ) . toBe ( 'dev-orders-api-eu' ) ;
688+ expect ( ( opPatch ! [ 1 ] as ResourceDescriptor ) . nameParts [ 1 ] ) . toBe ( 'get-orders' ) ;
689+ } ) ;
690+
599691 it ( 'should publish API child resources in parallel' , async ( ) => {
600692 const client = createMockClient ( ) ;
601693 const children = [
0 commit comments