@@ -516,4 +516,168 @@ describe('Model Catalog Generator', () => {
516516 // Should not have annotations if none were provided in API
517517 expect ( modelServerComponent . metadata . annotations ) . toBeUndefined ( ) ;
518518 } ) ;
519+
520+ it ( 'should copy modelServer annotations to component metadata when present' , ( ) => {
521+ const modelCatalog : ModelCatalog = {
522+ modelServer : {
523+ name : 'annotated-server' ,
524+ owner : 'example-user' ,
525+ description : 'Model server with annotations' ,
526+ lifecycle : 'production' ,
527+ annotations : {
528+ 'custom.io/annotation1' : 'server-value1' ,
529+ 'custom.io/annotation2' : 'server-value2' ,
530+ 'backstage.io/custom-tag' : 'server-custom' ,
531+ } ,
532+ } ,
533+ models : [
534+ {
535+ name : 'test-model' ,
536+ description : 'Test model' ,
537+ lifecycle : 'production' ,
538+ owner : 'example-user' ,
539+ } ,
540+ ] ,
541+ } ;
542+
543+ const modelCatalogEntities = GenerateCatalogEntities ( modelCatalog ) ;
544+
545+ const modelServerComponent = modelCatalogEntities . find (
546+ entity =>
547+ entity . kind === 'Component' &&
548+ entity . metadata . name === 'annotated-server' ,
549+ ) as ComponentEntity ;
550+
551+ expect ( modelServerComponent ) . toBeDefined ( ) ;
552+ expect ( modelServerComponent . metadata . annotations ) . toBeDefined ( ) ;
553+ expect ( modelServerComponent . metadata . annotations ) . toEqual ( {
554+ 'custom.io/annotation1' : 'server-value1' ,
555+ 'custom.io/annotation2' : 'server-value2' ,
556+ 'backstage.io/custom-tag' : 'server-custom' ,
557+ } ) ;
558+ } ) ;
559+
560+ it ( 'should merge modelServer annotations with API annotations when both are present' , ( ) => {
561+ const modelCatalog : ModelCatalog = {
562+ modelServer : {
563+ name : 'fully-annotated-service' ,
564+ owner : 'example-user' ,
565+ description : 'Model service with both server and API annotations' ,
566+ lifecycle : 'production' ,
567+ annotations : {
568+ 'server.io/annotation1' : 'from-server' ,
569+ 'server.io/annotation2' : 'also-from-server' ,
570+ } ,
571+ API : {
572+ url : 'https://api.example.com' ,
573+ type : Type . Openapi ,
574+ spec : 'https://example.com/openapi.json' ,
575+ annotations : {
576+ 'api.io/annotation1' : 'from-api' ,
577+ 'api.io/annotation2' : 'also-from-api' ,
578+ } ,
579+ } ,
580+ } ,
581+ models : [
582+ {
583+ name : 'test-model' ,
584+ description : 'Test model' ,
585+ lifecycle : 'production' ,
586+ owner : 'example-user' ,
587+ } ,
588+ ] ,
589+ } ;
590+
591+ const modelCatalogEntities = GenerateCatalogEntities ( modelCatalog ) ;
592+
593+ // Find the model server component entity
594+ const modelServerComponent = modelCatalogEntities . find (
595+ entity =>
596+ entity . kind === 'Component' &&
597+ entity . metadata . name === 'fully-annotated-service' ,
598+ ) as ComponentEntity ;
599+
600+ expect ( modelServerComponent ) . toBeDefined ( ) ;
601+ expect ( modelServerComponent . metadata . annotations ) . toBeDefined ( ) ;
602+ // Should contain annotations from both API and modelServer
603+ expect ( modelServerComponent . metadata . annotations ) . toEqual ( {
604+ 'api.io/annotation1' : 'from-api' ,
605+ 'api.io/annotation2' : 'also-from-api' ,
606+ 'server.io/annotation1' : 'from-server' ,
607+ 'server.io/annotation2' : 'also-from-server' ,
608+ } ) ;
609+ } ) ;
610+
611+ it ( 'should copy model annotations to resource metadata when present' , ( ) => {
612+ const modelCatalog : ModelCatalog = {
613+ models : [
614+ {
615+ name : 'annotated-model' ,
616+ description : 'Model with annotations' ,
617+ lifecycle : 'production' ,
618+ owner : 'example-user' ,
619+ annotations : {
620+ 'custom.io/model-annotation1' : 'model-value1' ,
621+ 'custom.io/model-annotation2' : 'model-value2' ,
622+ 'backstage.io/custom-model-tag' : 'model-custom' ,
623+ } ,
624+ } ,
625+ ] ,
626+ } ;
627+
628+ const modelCatalogEntities = GenerateCatalogEntities ( modelCatalog ) ;
629+
630+ // Find the model resource entity
631+ const modelResource = modelCatalogEntities . find (
632+ entity =>
633+ entity . kind === 'Resource' &&
634+ entity . metadata . name === 'annotated-model' ,
635+ ) ;
636+
637+ expect ( modelResource ) . toBeDefined ( ) ;
638+ expect ( modelResource ! . metadata . annotations ) . toBeDefined ( ) ;
639+ expect ( modelResource ! . metadata . annotations ) . toEqual ( {
640+ 'custom.io/model-annotation1' : 'model-value1' ,
641+ 'custom.io/model-annotation2' : 'model-value2' ,
642+ 'backstage.io/custom-model-tag' : 'model-custom' ,
643+ } ) ;
644+ } ) ;
645+
646+ it ( 'should copy model annotations and preserve TechDocs special case handling' , ( ) => {
647+ const modelCatalog : ModelCatalog = {
648+ models : [
649+ {
650+ name : 'fully-annotated-model' ,
651+ description : 'Model with multiple annotations including TechDocs' ,
652+ lifecycle : 'production' ,
653+ owner : 'example-user' ,
654+ annotations : {
655+ TechDocs :
656+ 'https://github.com/redhat-ai-dev/granite-3.1-8b-lab-docs/tree/main' ,
657+ 'custom.io/model-annotation1' : 'model-value1' ,
658+ 'custom.io/model-annotation2' : 'model-value2' ,
659+ } ,
660+ } ,
661+ ] ,
662+ } ;
663+
664+ const modelCatalogEntities = GenerateCatalogEntities ( modelCatalog ) ;
665+
666+ // Find the model resource entity
667+ const modelResource = modelCatalogEntities . find (
668+ entity =>
669+ entity . kind === 'Resource' &&
670+ entity . metadata . name === 'fully-annotated-model' ,
671+ ) ;
672+
673+ expect ( modelResource ) . toBeDefined ( ) ;
674+ expect ( modelResource ! . metadata . annotations ) . toBeDefined ( ) ;
675+ // Should have TechDocs converted to backstage.io/techdocs-ref and other annotations copied
676+ expect ( modelResource ! . metadata . annotations ) . toEqual ( {
677+ 'backstage.io/techdocs-ref' :
678+ 'url:https://github.com/redhat-ai-dev/granite-3.1-8b-lab-docs/tree/main' ,
679+ 'custom.io/model-annotation1' : 'model-value1' ,
680+ 'custom.io/model-annotation2' : 'model-value2' ,
681+ } ) ;
682+ } ) ;
519683} ) ;
0 commit comments