@@ -56,32 +56,7 @@ import * as fs from 'node:fs';
56
56
import * as path from 'node:path' ;
57
57
import { fileURLToPath } from 'node:url' ;
58
58
import type { AILabTryInstructLabPage } from './model/ai-lab-try-instructlab-page' ;
59
-
60
- const AI_LAB_EXTENSION_OCI_IMAGE =
61
- process . env . EXTENSION_OCI_IMAGE ?? 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly' ;
62
- const AI_LAB_EXTENSION_PREINSTALLED : boolean = process . env . EXTENSION_PREINSTALLED === 'true' ;
63
- const AI_LAB_CATALOG_STATUS_ACTIVE : string = 'ACTIVE' ;
64
-
65
- let aiLabPage : AILabDashboardPage ;
66
- const runnerOptions = {
67
- customFolder : 'ai-lab-tests-pd' ,
68
- aiLabModelUploadDisabled : isWindows ? true : false ,
69
- } ;
70
-
71
- interface AiApp {
72
- appName : string ;
73
- appModel : string ;
74
- }
75
-
76
- const AI_APPS : AiApp [ ] = [
77
- { appName : 'Audio to Text' , appModel : 'ggerganov/whisper.cpp' } ,
78
- { appName : 'ChatBot' , appModel : 'ibm-granite/granite-3.3-8b-instruct-GGUF' } ,
79
- { appName : 'Summarizer' , appModel : 'ibm-granite/granite-3.3-8b-instruct-GGUF' } ,
80
- { appName : 'Code Generation' , appModel : 'ibm-granite/granite-3.3-8b-instruct-GGUF' } ,
81
- { appName : 'RAG Chatbot' , appModel : 'ibm-granite/granite-3.3-8b-instruct-GGUF' } ,
82
- { appName : 'Function calling' , appModel : 'ibm-granite/granite-3.3-8b-instruct-GGUF' } ,
83
- { appName : 'Object Detection' , appModel : 'facebook/detr-resnet-101' } ,
84
- ] ;
59
+ import type { ApplicationCatalog } from '../../../packages/shared/src/models/IApplicationCatalog' ;
85
60
86
61
const __filename = fileURLToPath ( import . meta. url ) ;
87
62
const __dirname = path . dirname ( __filename ) ;
@@ -93,6 +68,57 @@ const TEST_AUDIO_FILE_PATH: string = path.resolve(
93
68
'resources' ,
94
69
`test-audio-to-text.wav` ,
95
70
) ;
71
+ const AI_JSON_FILE_PATH : string = path . resolve (
72
+ __dirname ,
73
+ '..' ,
74
+ '..' ,
75
+ '..' ,
76
+ 'packages' ,
77
+ 'backend' ,
78
+ 'src' ,
79
+ 'assets' ,
80
+ 'ai.json' ,
81
+ ) ;
82
+
83
+ const aiJSONFile = fs . readFileSync ( AI_JSON_FILE_PATH , 'utf8' ) ;
84
+ const AI_JSON : ApplicationCatalog = JSON . parse ( aiJSONFile ) as ApplicationCatalog ;
85
+ const AI_APP_MODELS : Set < string > = new Set ( ) ;
86
+ AI_JSON . recipes . forEach ( recipe => {
87
+ recipe . recommended ?. forEach ( model => {
88
+ AI_APP_MODELS . add ( model ) ;
89
+ } ) ;
90
+ } ) ;
91
+ // Create a set of AI models that are not the first recommended model for any app
92
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
93
+ const _AI_APP_UNUSED_MODELS : string [ ] = [
94
+ ...AI_APP_MODELS . values ( ) . filter ( model => {
95
+ // Check if the model is not the first recommended model for any app
96
+ return ! Array . from ( AI_JSON . recipes ) . some ( recipe => {
97
+ return recipe . recommended ?. at ( 0 ) === model ;
98
+ } ) ;
99
+ } ) ,
100
+ ] ;
101
+ const AI_APP_MODEL_AND_NAMES : Map < string , string [ ] > = new Map ( ) ;
102
+ AI_JSON . recipes . forEach ( recipe => {
103
+ const recommendedModel = recipe . recommended ?. at ( 0 ) ;
104
+ if ( recommendedModel ) {
105
+ if ( ! AI_APP_MODEL_AND_NAMES . has ( recommendedModel ) ) {
106
+ AI_APP_MODEL_AND_NAMES . set ( recommendedModel , [ ] ) ;
107
+ }
108
+ AI_APP_MODEL_AND_NAMES . get ( recommendedModel ) ?. push ( recipe . name ) ;
109
+ }
110
+ } ) ;
111
+
112
+ const AI_LAB_EXTENSION_OCI_IMAGE =
113
+ process . env . EXTENSION_OCI_IMAGE ?? 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly' ;
114
+ const AI_LAB_EXTENSION_PREINSTALLED : boolean = process . env . EXTENSION_PREINSTALLED === 'true' ;
115
+ const AI_LAB_CATALOG_STATUS_ACTIVE : string = 'ACTIVE' ;
116
+
117
+ let aiLabPage : AILabDashboardPage ;
118
+ const runnerOptions = {
119
+ customFolder : 'ai-lab-tests-pd' ,
120
+ aiLabModelUploadDisabled : isWindows ? true : false ,
121
+ } ;
96
122
97
123
test . use ( {
98
124
runnerOptions : new RunnerOptions ( runnerOptions ) ,
@@ -518,136 +544,141 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
518
544
} ) ;
519
545
} ) ;
520
546
521
- AI_APPS . forEach ( ( { appName, appModel } ) => {
522
- test . describe . serial ( `AI Recipe installation` , ( ) => {
523
- test . skip (
524
- ! process . env . EXT_TEST_RAG_CHATBOT && appName === 'RAG Chatbot' ,
525
- 'EXT_TEST_RAG_CHATBOT variable not set, skipping test' ,
526
- ) ;
527
- let recipesCatalogPage : AILabRecipesCatalogPage ;
528
-
529
- test . beforeAll ( `Open Recipes Catalog` , async ( { runner, page, navigationBar } ) => {
530
- aiLabPage = await reopenAILabDashboard ( runner , page , navigationBar ) ;
531
- await aiLabPage . navigationBar . waitForLoad ( ) ;
532
-
533
- recipesCatalogPage = await aiLabPage . navigationBar . openRecipesCatalog ( ) ;
534
- await recipesCatalogPage . waitForLoad ( ) ;
535
- } ) ;
536
-
537
- test ( `Install ${ appName } example app` , async ( ) => {
538
- test . skip (
539
- appName === 'Object Detection' && isCI && ! isMac ,
540
- 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
541
- ) ;
542
- test . setTimeout ( 1_500_000 ) ;
543
- const demoApp = await recipesCatalogPage . openRecipesCatalogApp ( appName ) ;
544
- await demoApp . waitForLoad ( ) ;
545
- await demoApp . startNewDeployment ( ) ;
546
- } ) ;
547
-
548
- test ( `Verify ${ appName } app HTTP page is reachable` , async ( { request } ) => {
549
- test . setTimeout ( 60_000 ) ;
550
- /// In the future, we could use this test for other AI applications
551
- test . skip (
552
- appName !== 'Object Detection' || ( isCI && ! isMac ) ,
553
- 'Runs only for Object Detection app on macOS CI or any local platform' ,
554
- ) ;
555
- const aiRunningAppsPage = await aiLabPage . navigationBar . openRunningApps ( ) ;
556
- const appPort = await aiRunningAppsPage . getAppPort ( appName ) ;
557
- const response = await request . get ( `http://localhost:${ appPort } ` , { timeout : 60_000 } ) ;
558
-
559
- playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
560
- const body = await response . text ( ) ;
561
- playExpect ( body ) . toContain ( '<title>Streamlit</title>' ) ;
562
- } ) ;
563
-
564
- test ( `Verify that model service for the ${ appName } is working` , async ( { request } ) => {
565
- test . skip ( appName !== 'Function calling' && appName !== 'Audio to Text' ) ;
566
- test . fail (
567
- appName === 'Audio to Text' ,
568
- 'Expected failure due to issue #3111: https://github.com/containers/podman-desktop-extension-ai-lab/issues/3111' ,
569
- ) ;
570
- test . setTimeout ( 600_000 ) ;
571
-
572
- const modelServicePage = await aiLabPage . navigationBar . openServices ( ) ;
573
- const serviceDetailsPage = await modelServicePage . openServiceDetails ( appModel ) ;
547
+ AI_APP_MODEL_AND_NAMES . forEach ( ( appNames , appModel ) => {
548
+ /* eslint-disable sonarjs/no-nested-functions */
549
+ test . describe . serial ( `AI Recipe installation for ${ appModel } ` , { tag : '@smoke' } , ( ) => {
550
+ appNames . forEach ( appName => {
551
+ test . describe . serial ( `AI Recipe installation ${ appName } ` , ( ) => {
552
+ test . skip (
553
+ ! process . env . EXT_TEST_RAG_CHATBOT && appName === 'RAG Chatbot' ,
554
+ 'EXT_TEST_RAG_CHATBOT variable not set, skipping test' ,
555
+ ) ;
556
+ let recipesCatalogPage : AILabRecipesCatalogPage ;
557
+
558
+ test . beforeAll ( `Open Recipes Catalog` , async ( { runner, page, navigationBar } ) => {
559
+ aiLabPage = await reopenAILabDashboard ( runner , page , navigationBar ) ;
560
+ await aiLabPage . navigationBar . waitForLoad ( ) ;
561
+
562
+ recipesCatalogPage = await aiLabPage . navigationBar . openRecipesCatalog ( ) ;
563
+ await recipesCatalogPage . waitForLoad ( ) ;
564
+ } ) ;
574
565
575
- await playExpect
576
- // eslint-disable-next-line sonarjs/no-nested-functions
577
- . poll ( async ( ) => await serviceDetailsPage . getServiceState ( ) , { timeout : 60_000 } )
578
- . toBe ( 'RUNNING' ) ;
566
+ test ( `Install ${ appName } example app` , async ( ) => {
567
+ test . skip (
568
+ appName === 'Object Detection' && isCI && ! isMac ,
569
+ 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
570
+ ) ;
571
+ test . setTimeout ( 1_500_000 ) ;
572
+ const demoApp = await recipesCatalogPage . openRecipesCatalogApp ( appName ) ;
573
+ await demoApp . waitForLoad ( ) ;
574
+ await demoApp . startNewDeployment ( ) ;
575
+ } ) ;
579
576
580
- const port = await serviceDetailsPage . getInferenceServerPort ( ) ;
581
- const baseUrl = `http://localhost:${ port } ` ;
582
-
583
- let response : APIResponse ;
584
- let expectedResponse : string ;
585
-
586
- switch ( appModel ) {
587
- case 'ggerganov/whisper.cpp' : {
588
- expectedResponse =
589
- 'And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country' ;
590
- const audioFileContent = fs . readFileSync ( TEST_AUDIO_FILE_PATH ) ;
591
-
592
- response = await request . post ( `${ baseUrl } /inference` , {
593
- headers : {
594
- Accept : 'application/json' ,
595
- } ,
596
- multipart : {
597
- file : {
598
- name : 'test.wav' ,
599
- mimeType : 'audio/wav' ,
600
- buffer : audioFileContent ,
601
- } ,
602
- } ,
603
- timeout : 600_000 ,
604
- } ) ;
605
- break ;
606
- }
607
-
608
- case 'ibm-granite/granite-3.3-8b-instruct-GGUF' : {
609
- expectedResponse = 'Prague' ;
610
- response = await request . post ( `${ baseUrl } /v1/chat/completions` , {
611
- data : {
612
- messages : [
613
- { role : 'system' , content : 'You are a helpful assistant.' } ,
614
- { role : 'user' , content : 'What is the capital of Czech Republic?' } ,
615
- ] ,
616
- } ,
617
- timeout : 600_000 ,
618
- } ) ;
619
- break ;
620
- }
621
-
622
- default :
623
- throw new Error ( `Unhandled model type: ${ appModel } ` ) ;
624
- }
577
+ test ( `Verify ${ appName } app HTTP page is reachable` , async ( { request } ) => {
578
+ test . setTimeout ( 60_000 ) ;
579
+ /// In the future, we could use this test for other AI applications
580
+ test . skip (
581
+ appName !== 'Object Detection' || ( isCI && ! isMac ) ,
582
+ 'Runs only for Object Detection app on macOS CI or any local platform' ,
583
+ ) ;
584
+ const aiRunningAppsPage = await aiLabPage . navigationBar . openRunningApps ( ) ;
585
+ const appPort = await aiRunningAppsPage . getAppPort ( appName ) ;
586
+ const response = await request . get ( `http://localhost:${ appPort } ` , { timeout : 60_000 } ) ;
587
+
588
+ playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
589
+ const body = await response . text ( ) ;
590
+ playExpect ( body ) . toContain ( '<title>Streamlit</title>' ) ;
591
+ } ) ;
625
592
626
- playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
627
- const body = await response . body ( ) ;
628
- const text = body . toString ( ) ;
629
- playExpect ( text ) . toContain ( expectedResponse ) ;
630
- } ) ;
593
+ test ( `Verify that model service for the ${ appName } is working` , async ( { request } ) => {
594
+ test . skip ( appName !== 'Function calling' && appName !== 'Audio to Text' ) ;
595
+ test . fail (
596
+ appName === 'Audio to Text' ,
597
+ 'Expected failure due to issue #3111: https://github.com/containers/podman-desktop-extension-ai-lab/issues/3111' ,
598
+ ) ;
599
+ test . setTimeout ( 600_000 ) ;
600
+
601
+ const modelServicePage = await aiLabPage . navigationBar . openServices ( ) ;
602
+ const serviceDetailsPage = await modelServicePage . openServiceDetails ( appModel ) ;
603
+
604
+ await playExpect
605
+ // eslint-disable-next-line sonarjs/no-nested-functions
606
+ . poll ( async ( ) => await serviceDetailsPage . getServiceState ( ) , { timeout : 60_000 } )
607
+ . toBe ( 'RUNNING' ) ;
608
+
609
+ const port = await serviceDetailsPage . getInferenceServerPort ( ) ;
610
+ const baseUrl = `http://localhost:${ port } ` ;
611
+
612
+ let response : APIResponse ;
613
+ let expectedResponse : string ;
614
+
615
+ switch ( appModel ) {
616
+ case 'ggerganov/whisper.cpp' : {
617
+ expectedResponse =
618
+ 'And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country' ;
619
+ const audioFileContent = fs . readFileSync ( TEST_AUDIO_FILE_PATH ) ;
620
+
621
+ response = await request . post ( `${ baseUrl } /inference` , {
622
+ headers : {
623
+ Accept : 'application/json' ,
624
+ } ,
625
+ multipart : {
626
+ file : {
627
+ name : 'test.wav' ,
628
+ mimeType : 'audio/wav' ,
629
+ buffer : audioFileContent ,
630
+ } ,
631
+ } ,
632
+ timeout : 600_000 ,
633
+ } ) ;
634
+ break ;
635
+ }
636
+
637
+ case 'ibm-granite/granite-3.3-8b-instruct-GGUF' : {
638
+ expectedResponse = 'Prague' ;
639
+ response = await request . post ( `${ baseUrl } /v1/chat/completions` , {
640
+ data : {
641
+ messages : [
642
+ { role : 'system' , content : 'You are a helpful assistant.' } ,
643
+ { role : 'user' , content : 'What is the capital of Czech Republic?' } ,
644
+ ] ,
645
+ } ,
646
+ timeout : 600_000 ,
647
+ } ) ;
648
+ break ;
649
+ }
650
+
651
+ default :
652
+ throw new Error ( `Unhandled model type: ${ appModel } ` ) ;
653
+ }
654
+
655
+ playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
656
+ const body = await response . body ( ) ;
657
+ const text = body . toString ( ) ;
658
+ playExpect ( text ) . toContain ( expectedResponse ) ;
659
+ } ) ;
631
660
632
- test ( `${ appName } : Restart, Stop, Delete. Clean up model service ` , async ( ) => {
633
- test . skip (
634
- appName === 'Object Detection' && isCI && ! isMac ,
635
- 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
636
- ) ;
637
- test . setTimeout ( 150_000 ) ;
661
+ test ( `${ appName } : Restart, Stop, Delete.` , async ( ) => {
662
+ test . skip (
663
+ appName === 'Object Detection' && isCI && ! isMac ,
664
+ 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
665
+ ) ;
666
+ test . setTimeout ( 150_000 ) ;
638
667
639
- await restartApp ( appName ) ;
640
- await stopAndDeleteApp ( appName ) ;
641
- await cleanupServices ( ) ;
642
- } ) ;
668
+ await restartApp ( appName ) ;
669
+ await stopAndDeleteApp ( appName ) ;
670
+ await cleanupServices ( ) ;
671
+ } ) ;
672
+ } ) ;
643
673
644
- test . afterAll ( `Ensure cleanup of "${ appName } " app, related service, and images` , async ( { navigationBar } ) => {
645
- test . setTimeout ( 150_000 ) ;
674
+ test . afterAll ( `Ensure cleanup of "${ appName } " app, related service, and images` , async ( { navigationBar } ) => {
675
+ test . setTimeout ( 150_000 ) ;
646
676
647
- await stopAndDeleteApp ( appName ) ;
648
- await cleanupServices ( ) ;
649
- await deleteAllModels ( ) ;
650
- await deleteUnusedImages ( navigationBar ) ;
677
+ await stopAndDeleteApp ( appName ) ;
678
+ await cleanupServices ( ) ;
679
+ await deleteAllModels ( ) ;
680
+ await deleteUnusedImages ( navigationBar ) ;
681
+ } ) ;
651
682
} ) ;
652
683
} ) ;
653
684
} ) ;
0 commit comments