Skip to content

Commit 9ccb03c

Browse files
committed
e2e(FR-2472): review and rewrite failing Serving E2E tests
- Add deduplicateTomlKeys() to test-util.ts to handle duplicate TOML keys in remote server config.toml (fixes config.toml parse failures) - Fix serving-deploy-lifecycle.spec.ts: set AI Accelerator to 0 to avoid GPU-based allocation preset (cuda01-small) causing service creation failures - Fix model-definition.yaml fixture: remove initial_delay from health_check section which is rejected by the backend trafaret validator - Update E2E_COVERAGE_REPORT.md with new Serving and Service Launcher coverage
1 parent 4e0fbf5 commit 9ccb03c

File tree

4 files changed

+97
-14
lines changed

4 files changed

+97
-14
lines changed

e2e/E2E_COVERAGE_REPORT.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# E2E Test Coverage Report
22

3-
> **Last Updated:** 2026-04-01
3+
> **Last Updated:** 2026-04-07
44
> **Router Source:** [`react/src/routes.tsx`](../react/src/routes.tsx)
55
> **E2E Root:** [`e2e/`](.)
66
>
@@ -22,9 +22,9 @@
2222
| Dashboard | `/dashboard` | 9 | 7 | 🔶 78% |
2323
| Session List | `/session` | 22 | 14 | 🔶 64% |
2424
| Session Launcher | `/session/start` | 14 | 3 | 🔶 21% |
25-
| Serving | `/serving` | 7 | 0 | ❌ 0% |
25+
| Serving | `/serving` | 7 | 2 | 🔶 29% |
2626
| Endpoint Detail | `/serving/:serviceId` | 20 | 9 | 🔶 45% |
27-
| Service Launcher | `/service/start` | 5 | 0 | ❌ 0% |
27+
| Service Launcher | `/service/start` | 5 | 1 | 🔶 20% |
2828
| VFolder / Data | `/data` | 45 | 32 | 🔶 71% |
2929
| Model Store | `/model-store` | 6 | 0 | ❌ 0% |
3030
| Storage Host | `/storage-settings/:hostname` | 3 | 0 | ❌ 0% |
@@ -222,7 +222,7 @@
222222

223223
### 6. Serving / Model Service (`/serving`)
224224

225-
**Test files:** None (visual regression only: [`e2e/visual_regression/serving/serving_page.test.ts`](visual_regression/serving/serving_page.test.ts))
225+
**Test files:** [`e2e/serving/serving-deploy-lifecycle.spec.ts`](serving/serving-deploy-lifecycle.spec.ts) (integration, `@integration @serving`)
226226

227227
**Filter:** Active | Destroyed (radio)
228228
**Primary action:** "Start Service" → navigates to `/service/start`
@@ -231,15 +231,15 @@
231231

232232
| Feature | Status | Test |
233233
| --------------------------------------------------------- | ------ | ---- |
234-
| Endpoint list rendering | | - |
234+
| Endpoint list rendering | | `Admin can deploy a model service via ServiceLauncher UI` (verifies row visible in serving list) |
235235
| "Start Service" → navigate to `/service/start` || - |
236236
| Endpoint name click → EndpointDetailPage || - |
237237
| Status filtering (Active/Destroyed) || - |
238238
| Property filtering || - |
239239
| Edit endpoint → navigate to `/service/update/:endpointId` || - |
240-
| Delete endpoint → confirm dialog | | - |
240+
| Delete endpoint → confirm dialog | | `Admin can terminate a deployed service` |
241241

242-
**Coverage: ❌ 0/7 features**
242+
**Coverage: 🔶 2/7 features**
243243

244244
---
245245

@@ -280,17 +280,17 @@
280280

281281
### 8. Service Launcher (`/service/start`, `/service/update/:endpointId`)
282282

283-
**Test files:** None
283+
**Test files:** [`e2e/serving/serving-deploy-lifecycle.spec.ts`](serving/serving-deploy-lifecycle.spec.ts) (integration, `@integration @serving`)
284284

285285
| Feature | Status | Test |
286286
| ----------------------- | ------ | ---- |
287-
| Create model service | | - |
287+
| Create model service | | `Admin can deploy a model service via ServiceLauncher UI` |
288288
| Update existing service || - |
289289
| Resource configuration || - |
290290
| Model folder selection || - |
291291
| Form validation || - |
292292

293-
**Coverage: ❌ 0/5 features**
293+
**Coverage: 🔶 1/5 features**
294294

295295
---
296296

e2e/serving/fixtures/model-definition.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ models:
88
port: 8000
99
health_check:
1010
path: /health
11-
initial_delay: 5.0
1211
max_retries: 10

e2e/serving/serving-deploy-lifecycle.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,33 @@ async function createServiceViaUI(
191191
.first()
192192
.click({ timeout: 10000 });
193193

194+
// Set AI Accelerator to 0 to avoid GPU-based allocation presets.
195+
// When the resource group has a GPU preset selected by default (e.g. cuda01-small),
196+
// service creation would fail if no GPU agents are available.
197+
// Setting the accelerator count to 0 ensures CPU-only resource allocation.
198+
//
199+
// Strategy: Find the AI Accelerator form item by its label text, then target
200+
// the spinbutton inside it. Ant Design Form.Item uses a `label` element that
201+
// may not have a `for` attribute in all versions, so we use a compound selector.
202+
const acceleratorFormItem = page
203+
.locator('.ant-form-item')
204+
.filter({ hasText: 'AI Accelerator' })
205+
.first();
206+
const acceleratorSpinbutton = acceleratorFormItem.getByRole('spinbutton');
207+
if (
208+
await acceleratorSpinbutton.isVisible({ timeout: 5000 }).catch(() => false)
209+
) {
210+
// Scroll into view first to ensure the element is interactable
211+
await acceleratorSpinbutton.scrollIntoViewIfNeeded();
212+
// Triple-click to select all content, then type the new value
213+
await acceleratorSpinbutton.click({ clickCount: 3 });
214+
await acceleratorSpinbutton.fill('0');
215+
// Trigger change event so form updates allocationPreset to 'custom'
216+
await acceleratorSpinbutton.press('Tab');
217+
// Wait briefly for form to react to the change
218+
await page.waitForTimeout(500);
219+
}
220+
194221
// Check "Open To Public"
195222
const openToPublicCheckbox = page.getByLabel('Open To Public');
196223
await openToPublicCheckbox.scrollIntoViewIfNeeded();
@@ -205,8 +232,10 @@ async function createServiceViaUI(
205232
await expect(createButton).toBeEnabled({ timeout: 5000 });
206233
await createButton.click();
207234

208-
// Wait for redirect to serving page and verify the service appears
209-
await page.waitForURL('**/serving', { timeout: 15000 });
235+
// Wait for the service creation to complete.
236+
// The form navigates to /serving on success or stays on /service/start on error.
237+
await page.waitForURL('**/serving', { timeout: 30000 });
238+
210239
await expect(
211240
page.getByRole('row').filter({ hasText: serviceName }).first(),
212241
).toBeVisible({ timeout: 15000 });

e2e/utils/test-util.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,57 @@ function tomlStringifyCompatible(config: any): string {
715715
return tomlStr;
716716
}
717717

718+
/**
719+
* Pre-process a TOML string to remove duplicate keys within each section.
720+
* Some server configurations may have duplicate keys (e.g., `debug = true`
721+
* duplicated keys (e.g., `debug = true` appearing twice under [general]).
722+
* @returns A deduplicated TOML string safe for strict parsers
723+
*/
724+
function deduplicateTomlKeys(tomlStr: string): string {
725+
const lines = tomlStr.split('\n');
726+
// Collect lines per section, tracking the last definition of each key
727+
type SectionEntry = { key: string; lineIndex: number };
728+
const sections: { header: string; entries: SectionEntry[] }[] = [
729+
{ header: '', entries: [] },
730+
];
731+
732+
let currentSection = sections[0];
733+
734+
for (let i = 0; i < lines.length; i++) {
735+
const line = lines[i];
736+
const trimmed = line.trim();
737+
738+
// New section header
739+
if (trimmed.startsWith('[') && !trimmed.startsWith('[[')) {
740+
currentSection = { header: trimmed, entries: [] };
741+
sections.push(currentSection);
742+
continue;
743+
}
744+
745+
// Skip comments and empty lines — they don't need deduplication
746+
if (!trimmed || trimmed.startsWith('#')) {
747+
continue;
748+
}
749+
750+
// Extract the key part (before the first '=')
751+
const eqIdx = trimmed.indexOf('=');
752+
if (eqIdx === -1) continue;
753+
const key = trimmed.substring(0, eqIdx).trim();
754+
755+
// Track the last line index seen for this key
756+
const existing = currentSection.entries.find((e) => e.key === key);
757+
if (existing) {
758+
// Mark previous occurrence for removal
759+
lines[existing.lineIndex] = null as unknown as string;
760+
existing.lineIndex = i;
761+
} else {
762+
currentSection.entries.push({ key, lineIndex: i });
763+
}
764+
}
765+
766+
return lines.filter((l) => l !== null).join('\n');
767+
}
768+
718769
// Store the accumulated config modifications in a WeakMap keyed by page
719770
const configCache = new WeakMap<Page, any>();
720771

@@ -751,7 +802,11 @@ export async function modifyConfigToml(
751802
if (!res.ok) throw new Error(`HTTP ${res.status}`);
752803
return res.text();
753804
});
754-
config = TOML.parse(configToml);
805+
// Pre-process TOML to remove duplicate keys before parsing.
806+
// Some server configurations may have duplicate keys (e.g., debug = true
807+
// appearing twice under [general]) which strict TOML parsers reject.
808+
const deduplicatedToml = deduplicateTomlKeys(configToml);
809+
config = TOML.parse(deduplicatedToml);
755810
break; // Success, exit retry loop
756811
} catch (error) {
757812
lastError = error;

0 commit comments

Comments
 (0)