Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions tests/playwright/src/ai-lab-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,15 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
});
});

['Audio to Text', 'ChatBot', 'Summarizer', 'Code Generation', 'RAG Chatbot'].forEach(appName => {
['Audio to Text', 'ChatBot', 'Summarizer', 'Code Generation', 'RAG Chatbot', 'Function calling'].forEach(appName => {
test.describe.serial(`AI Recipe installation`, () => {
test.skip(
!process.env.EXT_TEST_RAG_CHATBOT && appName === 'RAG Chatbot',
'EXT_TEST_RAG_CHATBOT variable not set, skipping test',
);
let recipesCatalogPage: AILabRecipesCatalogPage;

test.beforeEach(`Open Recipes Catalog`, async ({ runner, page, navigationBar }) => {
test.beforeAll(`Open Recipes Catalog`, async ({ runner, page, navigationBar }) => {
aiLabPage = await reopenAILabDashboard(runner, page, navigationBar);
await aiLabPage.navigationBar.waitForLoad();

Expand All @@ -441,10 +441,55 @@ test.describe.serial(`AI Lab extension installation and verification`, () => {
await demoApp.startNewDeployment();
});

test.afterEach(`Stop ${appName} app`, async ({ navigationBar }) => {
test(`Verify that model service for the ${appName} is working`, async ({ request }) => {
test.skip(appName !== 'Function calling');
test.setTimeout(600_000);

const modelServicePage = await aiLabPage.navigationBar.openServices();
const serviceDetailsPage = await modelServicePage.openServiceDetails(
'ibm-granite/granite-3.3-8b-instruct-GGUF',
);

await playExpect
// eslint-disable-next-line sonarjs/no-nested-functions
.poll(async () => await serviceDetailsPage.getServiceState(), { timeout: 60_000 })
.toBe('RUNNING');
const port = await serviceDetailsPage.getInferenceServerPort();
const url = `http://localhost:${port}/v1/chat/completions`;

const response = await request.post(url, {
data: {
messages: [
{
content: 'You are a helpful assistant.',
role: 'system',
},
{
content: 'What is the capital of Czech Republic?',
role: 'user',
},
],
},
timeout: 600_000,
});

playExpect(response.ok()).toBeTruthy();
const body = await response.body();
const text = body.toString();
playExpect(text).toContain('Prague');
});

test(`Stop ${appName} app`, async () => {
test.setTimeout(150_000);
await stopAndDeleteApp(appName);
await cleanupServiceModels();
});

test.afterAll(`Ensure cleanup of "${appName}" app, related service, and images`, async ({ navigationBar }) => {
test.setTimeout(150_000);

await stopAndDeleteApp(appName);
await cleanupServiceModels();
await deleteUnusedImages(navigationBar);
});
});
Expand All @@ -465,6 +510,10 @@ async function cleanupServiceModels(): Promise<void> {
async function stopAndDeleteApp(appName: string): Promise<void> {
const aiRunningAppsPage = await aiLabPage.navigationBar.openRunningApps();
await aiRunningAppsPage.waitForLoad();
if (!(await aiRunningAppsPage.appExists(appName))) {
console.log(`"${appName}" is not present in the running apps list. Skipping stop and delete operations.`);
return;
}
await playExpect.poll(async () => await aiRunningAppsPage.appExists(appName), { timeout: 10_000 }).toBeTruthy();
await playExpect
.poll(async () => await aiRunningAppsPage.getCurrentStatusForApp(appName), { timeout: 60_000 })
Expand Down
23 changes: 23 additions & 0 deletions tests/playwright/src/model/ai-lab-model-service-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Locator, Page } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { handleConfirmationDialog, podmanAILabExtension } from '@podman-desktop/tests-playwright';
import { AILabCreatingModelServicePage } from './ai-lab-creating-model-service-page';
import { AILabServiceDetailsPage } from './ai-lab-service-details-page';

export class AiModelServicePage extends AILabBasePage {
readonly additionalActions: Locator;
Expand Down Expand Up @@ -66,6 +67,28 @@ export class AiModelServicePage extends AILabBasePage {
return (await this.getAllTableRows()).length;
}

async openServiceDetails(modelName: string): Promise<AILabServiceDetailsPage> {
const serviceRow = await this.getServiceByModel(modelName);
if (serviceRow === undefined) {
throw new Error(`Model [${modelName}] service doesn't exist`);
}
const serviceRowName = serviceRow.getByRole('cell').nth(3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open follow up issue to address missing aria-labels in the row definition. We should not rely on order of the cells. This is out of scope of this PR.

await serviceRowName.click();
return new AILabServiceDetailsPage(this.page, this.webview);
}

async getServiceByModel(modelName: string): Promise<Locator | undefined> {
const rows = await this.getAllTableRows();
for (let rowNum = 1; rowNum < rows.length; rowNum++) {
//skip header
const serviceModel = rows[rowNum].getByRole('cell').nth(4);
if ((await serviceModel.textContent()) === modelName) {
return rows[rowNum];
}
}
return undefined;
}

private async getAllTableRows(): Promise<Locator[]> {
return await this.webview.getByRole('row').all();
}
Expand Down
5 changes: 5 additions & 0 deletions tests/playwright/src/model/ai-lab-service-details-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ export class AILabServiceDetailsPage extends AILabBasePage {
const port = split ? split[split.length - 1].split('/')[0] : '';
return port;
}

async getServiceState(): Promise<string> {
const serviceState = await this.webview.getByRole('status').getAttribute('title');
return serviceState ?? 'UNKNOWN';
}
}
Loading