Files
openmaic/OpenMAIC/e2e/tests/model-invariant-580.spec.ts
2026-08-16 14:58:47 +08:00

130 lines
4.7 KiB
TypeScript

import { test, expect } from '../fixtures/base';
import { HomePage } from '../pages/home.page';
import { createSettingsStorage } from '../fixtures/test-data/settings';
/**
* #580 — "usable provider ⇒ a concrete model is always selected".
*
* State A: no usable provider (keyless ollama/lemonade no longer count until
* the user sets an explicit baseUrl) → generate button disabled. The
* learner surface has no model picker or settings CTA.
* State B: a server-configured provider → a concrete model is auto-resolved,
* generation is enabled while the learner surface still hides the
* provider / model picker.
*
* The Playwright Chromium locale is en-US, so UI strings are English
* ("Enter Classroom").
*/
const SCREENSHOT_DIR = 'e2e/screenshots';
// fetchServerProviders reads data.tts/asr/pdf/image/video/webSearch via
// Object.keys(); omitting them throws and is silently swallowed by its
// try/catch, so the mock must return the full shape (like the unit helper).
function serverProvidersBody(providers: Record<string, { models?: string[] }>) {
return JSON.stringify({
providers,
tts: {},
asr: {},
pdf: {},
image: {},
video: {},
webSearch: {},
});
}
// Run serially with one worker: a single shared dev server + async
// server-provider reconcile makes parallel runs flaky.
test.describe.configure({ mode: 'serial' });
test.describe('#580 model-selection invariant', () => {
test('State A: no usable provider → disabled generate + no model/settings affordance', async ({
page,
}) => {
await page.route('**/api/server-providers', (route) =>
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: serverProvidersBody({}),
}),
);
await page.addInitScript(
(settings) => {
localStorage.setItem('maic:account:settings-storage', settings);
},
createSettingsStorage({
modelId: '',
providerId: 'openai',
providersConfig: { openai: { apiKey: '' } },
autoConfigApplied: true,
}),
);
const home = new HomePage(page);
await Promise.all([page.waitForResponse('**/api/server-providers'), home.goto()]);
await expect(home.textarea).toBeVisible();
// Learners never choose or configure the model from the generation form.
await expect(page.locator('button:has(svg.lucide-settings)')).toHaveCount(0);
await expect(page.getByText('Set up model', { exact: true })).toHaveCount(0);
await expect(page.locator('button[aria-label*=" / "]')).toHaveCount(0);
// Even with a requirement typed, generation stays disabled (gate is
// hasUsableProvider, not modelId) — and crucially NO toast / forced dialog.
await home.fillRequirement('Explain how photosynthesis works');
await expect(home.enterButton).toBeDisabled();
await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
await expect(page.getByRole('dialog')).toHaveCount(0);
await page.screenshot({
path: `${SCREENSHOT_DIR}/580-state-a-no-provider.png`,
fullPage: true,
animations: 'disabled',
caret: 'hide',
});
});
test('State B: server-configured provider → concrete model auto-selected, generation enabled', async ({
page,
}) => {
await page.route('**/api/server-providers', (route) =>
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: serverProvidersBody({ openai: { models: ['gpt-4o', 'gpt-4o-mini'] } }),
}),
);
await page.addInitScript(
(settings) => {
localStorage.setItem('maic:account:settings-storage', settings);
},
createSettingsStorage({
modelId: '',
providerId: 'openai',
providersConfig: { openai: { apiKey: '' } },
autoConfigApplied: true,
}),
);
const home = new HomePage(page);
await Promise.all([page.waitForResponse('**/api/server-providers'), home.goto()]);
await expect(home.textarea).toBeVisible();
// Reconcile resolves (openai, '' → first server model 'gpt-4o') for the
// request path, but the learner toolbar never exposes that choice.
await expect(page.locator('button:has(svg.lucide-settings)')).toHaveCount(0);
await expect(page.getByText('Set up model', { exact: true })).toHaveCount(0);
await expect(page.locator('button[aria-label*=" / "]')).toHaveCount(0);
await home.fillRequirement('Explain how photosynthesis works');
await expect(home.enterButton).toBeEnabled();
await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
await page.screenshot({
path: `${SCREENSHOT_DIR}/580-state-b-usable-provider.png`,
fullPage: true,
animations: 'disabled',
caret: 'hide',
});
});
});