@@ -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 ) ,
@@ -520,135 +546,140 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
520
546
} ) ;
521
547
} ) ;
522
548
523
- AI_APPS . forEach ( ( { appName, appModel } ) => {
524
- test . describe . serial ( `AI Recipe installation` , ( ) => {
525
- test . skip (
526
- ! process . env . EXT_TEST_RAG_CHATBOT && appName === 'RAG Chatbot' ,
527
- 'EXT_TEST_RAG_CHATBOT variable not set, skipping test' ,
528
- ) ;
529
- let recipesCatalogPage : AILabRecipesCatalogPage ;
530
-
531
- test . beforeAll ( `Open Recipes Catalog` , async ( { runner, page, navigationBar } ) => {
532
- aiLabPage = await reopenAILabDashboard ( runner , page , navigationBar ) ;
533
- await aiLabPage . navigationBar . waitForLoad ( ) ;
534
-
535
- recipesCatalogPage = await aiLabPage . navigationBar . openRecipesCatalog ( ) ;
536
- await recipesCatalogPage . waitForLoad ( ) ;
537
- } ) ;
538
-
539
- test ( `Install ${ appName } example app` , async ( ) => {
540
- test . skip (
541
- appName === 'Object Detection' && isCI && ! isMac ,
542
- 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
543
- ) ;
544
- test . setTimeout ( 1_500_000 ) ;
545
- const demoApp = await recipesCatalogPage . openRecipesCatalogApp ( appName ) ;
546
- await demoApp . waitForLoad ( ) ;
547
- await demoApp . startNewDeployment ( ) ;
548
- } ) ;
549
-
550
- test ( `Verify ${ appName } app HTTP page is reachable` , async ( { request } ) => {
551
- test . setTimeout ( 60_000 ) ;
552
- /// In the future, we could use this test for other AI applications
553
- test . skip (
554
- appName !== 'Object Detection' || ( isCI && ! isMac ) ,
555
- 'Runs only for Object Detection app on macOS CI or any local platform' ,
556
- ) ;
557
- const aiRunningAppsPage = await aiLabPage . navigationBar . openRunningApps ( ) ;
558
- const appPort = await aiRunningAppsPage . getAppPort ( appName ) ;
559
- const response = await request . get ( `http://localhost:${ appPort } ` , { timeout : 60_000 } ) ;
560
-
561
- playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
562
- const body = await response . text ( ) ;
563
- playExpect ( body ) . toContain ( '<title>Streamlit</title>' ) ;
564
- } ) ;
565
-
566
- test ( `Verify that model service for the ${ appName } is working` , async ( { request } ) => {
567
- test . skip ( appName !== 'Function calling' && appName !== 'Audio to Text' ) ;
568
- test . fail (
569
- appName === 'Audio to Text' ,
570
- 'Expected failure due to issue #3111: https://github.com/containers/podman-desktop-extension-ai-lab/issues/3111' ,
571
- ) ;
572
- test . setTimeout ( 600_000 ) ;
573
-
574
- const modelServicePage = await aiLabPage . navigationBar . openServices ( ) ;
575
- const serviceDetailsPage = await modelServicePage . openServiceDetails ( appModel ) ;
549
+ AI_APP_MODEL_AND_NAMES . forEach ( ( appNames , appModel ) => {
550
+ /* eslint-disable sonarjs/no-nested-functions */
551
+ test . describe . serial ( `AI Recipe installation for ${ appModel } ` , { tag : '@smoke' } , ( ) => {
552
+ appNames . forEach ( appName => {
553
+ test . describe . serial ( `AI Recipe installation ${ appName } ` , ( ) => {
554
+ test . skip (
555
+ ! process . env . EXT_TEST_RAG_CHATBOT && appName === 'RAG Chatbot' ,
556
+ 'EXT_TEST_RAG_CHATBOT variable not set, skipping test' ,
557
+ ) ;
558
+ let recipesCatalogPage : AILabRecipesCatalogPage ;
559
+
560
+ test . beforeAll ( `Open Recipes Catalog` , async ( { runner, page, navigationBar } ) => {
561
+ aiLabPage = await reopenAILabDashboard ( runner , page , navigationBar ) ;
562
+ await aiLabPage . navigationBar . waitForLoad ( ) ;
563
+
564
+ recipesCatalogPage = await aiLabPage . navigationBar . openRecipesCatalog ( ) ;
565
+ await recipesCatalogPage . waitForLoad ( ) ;
566
+ } ) ;
576
567
577
- await playExpect
578
- // eslint-disable-next-line sonarjs/no-nested-functions
579
- . poll ( async ( ) => await serviceDetailsPage . getServiceState ( ) , { timeout : 60_000 } )
580
- . toBe ( 'RUNNING' ) ;
568
+ test ( `Install ${ appName } example app` , async ( ) => {
569
+ test . skip (
570
+ appName === 'Object Detection' && isCI && ! isMac ,
571
+ 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
572
+ ) ;
573
+ test . setTimeout ( 1_500_000 ) ;
574
+ const demoApp = await recipesCatalogPage . openRecipesCatalogApp ( appName ) ;
575
+ await demoApp . waitForLoad ( ) ;
576
+ await demoApp . startNewDeployment ( ) ;
577
+ } ) ;
581
578
582
- const port = await serviceDetailsPage . getInferenceServerPort ( ) ;
583
- const baseUrl = `http://localhost:${ port } ` ;
584
-
585
- let response : APIResponse ;
586
- let expectedResponse : string ;
587
-
588
- switch ( appModel ) {
589
- case 'ggerganov/whisper.cpp' : {
590
- expectedResponse =
591
- 'And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country' ;
592
- const audioFileContent = fs . readFileSync ( TEST_AUDIO_FILE_PATH ) ;
593
-
594
- response = await request . post ( `${ baseUrl } /inference` , {
595
- headers : {
596
- Accept : 'application/json' ,
597
- } ,
598
- multipart : {
599
- file : {
600
- name : 'test.wav' ,
601
- mimeType : 'audio/wav' ,
602
- buffer : audioFileContent ,
603
- } ,
604
- } ,
605
- timeout : 600_000 ,
606
- } ) ;
607
- break ;
608
- }
609
-
610
- case 'ibm-granite/granite-3.3-8b-instruct-GGUF' : {
611
- expectedResponse = 'Prague' ;
612
- response = await request . post ( `${ baseUrl } /v1/chat/completions` , {
613
- data : {
614
- messages : [
615
- { role : 'system' , content : 'You are a helpful assistant.' } ,
616
- { role : 'user' , content : 'What is the capital of Czech Republic?' } ,
617
- ] ,
618
- } ,
619
- timeout : 600_000 ,
620
- } ) ;
621
- break ;
622
- }
623
-
624
- default :
625
- throw new Error ( `Unhandled model type: ${ appModel } ` ) ;
626
- }
579
+ test ( `Verify ${ appName } app HTTP page is reachable` , async ( { request } ) => {
580
+ test . setTimeout ( 60_000 ) ;
581
+ /// In the future, we could use this test for other AI applications
582
+ test . skip (
583
+ appName !== 'Object Detection' || ( isCI && ! isMac ) ,
584
+ 'Runs only for Object Detection app on macOS CI or any local platform' ,
585
+ ) ;
586
+ const aiRunningAppsPage = await aiLabPage . navigationBar . openRunningApps ( ) ;
587
+ const appPort = await aiRunningAppsPage . getAppPort ( appName ) ;
588
+ const response = await request . get ( `http://localhost:${ appPort } ` , { timeout : 60_000 } ) ;
589
+
590
+ playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
591
+ const body = await response . text ( ) ;
592
+ playExpect ( body ) . toContain ( '<title>Streamlit</title>' ) ;
593
+ } ) ;
627
594
628
- playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
629
- const body = await response . body ( ) ;
630
- const text = body . toString ( ) ;
631
- playExpect ( text ) . toContain ( expectedResponse ) ;
632
- } ) ;
595
+ test ( `Verify that model service for the ${ appName } is working` , async ( { request } ) => {
596
+ test . skip ( appName !== 'Function calling' && appName !== 'Audio to Text' ) ;
597
+ test . fail (
598
+ appName === 'Audio to Text' ,
599
+ 'Expected failure due to issue #3111: https://github.com/containers/podman-desktop-extension-ai-lab/issues/3111' ,
600
+ ) ;
601
+ test . setTimeout ( 600_000 ) ;
602
+
603
+ const modelServicePage = await aiLabPage . navigationBar . openServices ( ) ;
604
+ const serviceDetailsPage = await modelServicePage . openServiceDetails ( appModel ) ;
605
+
606
+ await playExpect
607
+ // eslint-disable-next-line sonarjs/no-nested-functions
608
+ . poll ( async ( ) => await serviceDetailsPage . getServiceState ( ) , { timeout : 60_000 } )
609
+ . toBe ( 'RUNNING' ) ;
610
+
611
+ const port = await serviceDetailsPage . getInferenceServerPort ( ) ;
612
+ const baseUrl = `http://localhost:${ port } ` ;
613
+
614
+ let response : APIResponse ;
615
+ let expectedResponse : string ;
616
+
617
+ switch ( appModel ) {
618
+ case 'ggerganov/whisper.cpp' : {
619
+ expectedResponse =
620
+ 'And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country' ;
621
+ const audioFileContent = fs . readFileSync ( TEST_AUDIO_FILE_PATH ) ;
622
+
623
+ response = await request . post ( `${ baseUrl } /inference` , {
624
+ headers : {
625
+ Accept : 'application/json' ,
626
+ } ,
627
+ multipart : {
628
+ file : {
629
+ name : 'test.wav' ,
630
+ mimeType : 'audio/wav' ,
631
+ buffer : audioFileContent ,
632
+ } ,
633
+ } ,
634
+ timeout : 600_000 ,
635
+ } ) ;
636
+ break ;
637
+ }
638
+
639
+ case 'ibm-granite/granite-3.3-8b-instruct-GGUF' : {
640
+ expectedResponse = 'Prague' ;
641
+ response = await request . post ( `${ baseUrl } /v1/chat/completions` , {
642
+ data : {
643
+ messages : [
644
+ { role : 'system' , content : 'You are a helpful assistant.' } ,
645
+ { role : 'user' , content : 'What is the capital of Czech Republic?' } ,
646
+ ] ,
647
+ } ,
648
+ timeout : 600_000 ,
649
+ } ) ;
650
+ break ;
651
+ }
652
+
653
+ default :
654
+ throw new Error ( `Unhandled model type: ${ appModel } ` ) ;
655
+ }
656
+
657
+ playExpect ( response . ok ( ) ) . toBeTruthy ( ) ;
658
+ const body = await response . body ( ) ;
659
+ const text = body . toString ( ) ;
660
+ playExpect ( text ) . toContain ( expectedResponse ) ;
661
+ } ) ;
633
662
634
- test ( `${ appName } : Restart, Stop, Delete. Clean up model service ` , async ( ) => {
635
- test . skip (
636
- appName === 'Object Detection' && isCI && ! isMac ,
637
- 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
638
- ) ;
639
- test . setTimeout ( 150_000 ) ;
663
+ test ( `${ appName } : Restart, Stop, Delete.` , async ( ) => {
664
+ test . skip (
665
+ appName === 'Object Detection' && isCI && ! isMac ,
666
+ 'Currently we are facing issues with the Object Detection app installation on Windows and Linux CI.' ,
667
+ ) ;
668
+ test . setTimeout ( 150_000 ) ;
640
669
641
- await restartApp ( appName ) ;
642
- await stopAndDeleteApp ( appName ) ;
643
- await cleanupServiceModels ( ) ;
644
- } ) ;
670
+ await restartApp ( appName ) ;
671
+ await stopAndDeleteApp ( appName ) ;
672
+ await cleanupServiceModels ( ) ;
673
+ } ) ;
674
+ } ) ;
645
675
646
- test . afterAll ( `Ensure cleanup of "${ appName } " app, related service, and images` , async ( { navigationBar } ) => {
647
- test . setTimeout ( 150_000 ) ;
676
+ test . afterAll ( `Ensure cleanup of "${ appName } " app, related service, and images` , async ( { navigationBar } ) => {
677
+ test . setTimeout ( 150_000 ) ;
648
678
649
- await stopAndDeleteApp ( appName ) ;
650
- await cleanupServiceModels ( ) ;
651
- await deleteUnusedImages ( navigationBar ) ;
679
+ await stopAndDeleteApp ( appName ) ;
680
+ await cleanupServiceModels ( ) ;
681
+ await deleteUnusedImages ( navigationBar ) ;
682
+ } ) ;
652
683
} ) ;
653
684
} ) ;
654
685
} ) ;
0 commit comments