Skip to content

Commit 6ed26ef

Browse files
committed
chore(test): implement POC for ai.json-based tests
Signed-off-by: Tibor Dancs <[email protected]>
1 parent e67ebb7 commit 6ed26ef

File tree

2 files changed

+182
-150
lines changed

2 files changed

+182
-150
lines changed

tests/playwright/src/ai-lab-extension.spec.ts

Lines changed: 180 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -56,32 +56,7 @@ import * as fs from 'node:fs';
5656
import * as path from 'node:path';
5757
import { fileURLToPath } from 'node:url';
5858
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';
8560

8661
const __filename = fileURLToPath(import.meta.url);
8762
const __dirname = path.dirname(__filename);
@@ -93,6 +68,57 @@ const TEST_AUDIO_FILE_PATH: string = path.resolve(
9368
'resources',
9469
`test-audio-to-text.wav`,
9570
);
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+
};
96122

97123
test.use({
98124
runnerOptions: new RunnerOptions(runnerOptions),
@@ -518,136 +544,141 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
518544
});
519545
});
520546

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+
});
574565

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+
});
579576

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+
});
625592

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+
});
631660

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);
638667

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+
});
643673

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);
646676

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+
});
651682
});
652683
});
653684
});

tests/playwright/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
"compilerOptions": {
33
"target": "esnext",
44
"module": "esnext",
5-
"moduleResolution":"node",
5+
"moduleResolution": "node",
66
"strict": true,
77
"preserveValueImports": false,
88
"skipLibCheck": false,
99
"baseUrl": ".",
10+
"resolveJsonModule": true
1011
},
1112
"include": ["src/**/*.ts", "playwright.config.ts"],
1213
"exclude": ["node_modules/**"]

0 commit comments

Comments
 (0)