Skip to content

Commit 7b50dec

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

File tree

2 files changed

+181
-149
lines changed

2 files changed

+181
-149
lines changed

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

Lines changed: 179 additions & 148 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),
@@ -520,135 +546,140 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
520546
});
521547
});
522548

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

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

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

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

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

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

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

649-
await stopAndDeleteApp(appName);
650-
await cleanupServiceModels();
651-
await deleteUnusedImages(navigationBar);
679+
await stopAndDeleteApp(appName);
680+
await cleanupServiceModels();
681+
await deleteUnusedImages(navigationBar);
682+
});
652683
});
653684
});
654685
});

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)