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
18 changes: 9 additions & 9 deletions e2e/E2E_COVERAGE_REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
| Dashboard | `/dashboard` | 9 | 7 | 🔶 78% |
| Session List | `/session` | 22 | 14 | 🔶 64% |
| Session Launcher | `/session/start` | 14 | 3 | 🔶 21% |
| Serving | `/serving` | 7 | 0 | ❌ 0% |
| Serving | `/serving` | 7 | 2 | 🔶 29% |
| Endpoint Detail | `/serving/:serviceId` | 20 | 9 | 🔶 45% |
| Service Launcher | `/service/start` | 5 | 0 | ❌ 0% |
| Service Launcher | `/service/start` | 5 | 1 | 🔶 20% |
| VFolder / Data | `/data` | 45 | 32 | 🔶 71% |
Comment thread
ironAiken2 marked this conversation as resolved.
| Model Store | `/model-store` | 6 | 6 | ✅ 100% |
| Admin Model Store | `/admin-model-store` | 22 | 22 | ✅ 100% |
Expand Down Expand Up @@ -238,7 +238,7 @@

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

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

**Filter:** Active | Destroyed (radio)
**Primary action:** "Start Service" → navigates to `/service/start`
Expand All @@ -247,15 +247,15 @@

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

**Coverage: ❌ 0/7 features**
**Coverage: 🔶 2/7 features**

---

Expand Down Expand Up @@ -296,17 +296,17 @@

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

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

| Feature | Status | Test |
| ----------------------- | ------ | ---- |
| Create model service | | - |
| Create model service | | `Admin can deploy a model service via ServiceLauncher UI` |
| Update existing service | ❌ | - |
| Resource configuration | ❌ | - |
| Model folder selection | ❌ | - |
| Form validation | ❌ | - |

**Coverage: ❌ 0/5 features**
**Coverage: 🔶 1/5 features**

---

Expand Down
118 changes: 83 additions & 35 deletions e2e/serving/endpoint-route-table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test.describe(
vars: Record<string, any>,
) => Record<string, any> = endpointDetailRunningMockResponse,
enableRouteNode: boolean = true,
enableRouteHealthStatus: boolean = true,
) {
await loginAsAdmin(page, request);
await setupGraphQLMocks(page, {
Expand All @@ -50,18 +51,26 @@ test.describe(
).toBeVisible({
timeout: 10000,
});
// Inject the route-node feature flag into the already-initialized client.
// Because the next navigation (clicking the link below) is a client-side
// React Router navigation, no page reload occurs, so the flag persists.
await page.evaluate((flag) => {
const client = (globalThis as any).backendaiclient;
if (client) {
// Ensure _updateSupportList has already run by calling supports() once,
// then override the route-node flag.
client.supports('route-node');
client._features['route-node'] = flag;
}
}, enableRouteNode);
// Inject the route-node and route-health-status feature flags into the
// already-initialized client. Because the next navigation (clicking the
// link below) is a client-side React Router navigation, no page reload
// occurs, so the flags persist.
await page.evaluate(
({ routeNode, routeHealthStatus }) => {
const client = (globalThis as any).backendaiclient;
if (client) {
// Ensure _updateSupportList has already run by calling supports() once,
// then override the feature flags.
client.supports('route-node');
client._features['route-node'] = routeNode;
client._features['route-health-status'] = routeHealthStatus;
}
},
{
routeNode: enableRouteNode,
routeHealthStatus: enableRouteHealthStatus,
},
);
// Click the mock endpoint link to navigate to the detail page via React Router.
await page
.getByRole('link', { name: 'mock-endpoint', exact: true })
Expand Down Expand Up @@ -137,11 +146,11 @@ test.describe(
await expect(
card.getByRole('columnheader', { name: 'Status', exact: true }),
).toBeVisible();
// TODO(needs-backend): Re-enable when BAIRouteNodes exposes the Traffic
// Status column. It is currently commented out in BAIRouteNodes.tsx
// pending backend support for per-route traffic status (FR-2591).
await expect(
card.getByRole('columnheader', { name: 'Traffic Status' }),
).toBeVisible();
await expect(
card.getByRole('columnheader', { name: 'Traffic Ratio' }),
card.getByRole('columnheader', { name: 'Created At' }),
).toBeVisible();
});

Expand Down Expand Up @@ -305,7 +314,11 @@ test.describe(
// 3. Property Filter
// ─────────────────────────────────────────────────────────────────────────

test('3.1 Admin can see the Traffic Status filter property in the property filter selector', async ({
// TODO(needs-backend): Re-enable when the EndpointDetailPage route property
// filter exposes a "Traffic Status" option. The filter is currently only
// populated with Health Status, pending backend support for per-route
// traffic status (FR-2591).
test.fixme('3.1 Admin can see the Traffic Status filter property in the property filter selector', async ({
page,
request,
}) => {
Expand All @@ -325,7 +338,9 @@ test.describe(
await page.keyboard.press('Escape');
});

test('3.2 Admin can filter routes by trafficStatus ACTIVE using the property filter', async ({
// TODO(needs-backend): Re-enable when the route property filter exposes a
// "Traffic Status" option (FR-2591).
test.fixme('3.2 Admin can filter routes by trafficStatus ACTIVE using the property filter', async ({
page,
request,
}) => {
Expand Down Expand Up @@ -358,7 +373,9 @@ test.describe(
await expect(filterTag.first()).toBeVisible();
});

test('3.3 Admin can filter routes by trafficStatus INACTIVE using the property filter', async ({
// TODO(needs-backend): Re-enable when the route property filter exposes a
// "Traffic Status" option (FR-2591).
test.fixme('3.3 Admin can filter routes by trafficStatus INACTIVE using the property filter', async ({
page,
request,
}) => {
Expand Down Expand Up @@ -391,7 +408,11 @@ test.describe(
await expect(filterTag.first()).toBeVisible();
});

test('3.4 Admin can remove an applied filter to restore the full route list', async ({
// TODO(needs-backend): Re-enable when the route property filter exposes a
// "Traffic Status" option (FR-2591). The underlying remove-filter behavior
// is covered indirectly via the Health Status filter once BAIRouteNodes
// supports a secondary filter.
test.fixme('3.4 Admin can remove an applied filter to restore the full route list', async ({
page,
request,
}) => {
Expand Down Expand Up @@ -451,11 +472,10 @@ test.describe(
await expect(
card.getByRole('columnheader', { name: 'Status', exact: true }),
).toBeVisible();
// TODO(needs-backend): Re-enable when BAIRouteNodes exposes the Traffic
// Status column (FR-2591).
await expect(
card.getByRole('columnheader', { name: 'Traffic Status' }),
).toBeVisible();
await expect(
card.getByRole('columnheader', { name: 'Traffic Ratio' }),
card.getByRole('columnheader', { name: 'Created At' }),
).toBeVisible();
});

Expand All @@ -474,9 +494,8 @@ test.describe(
.first();
await expect(healthyTag).toBeVisible();

// Verify ACTIVE traffic status tag
const activeTag = card.locator('.ant-tag').filter({ hasText: 'ACTIVE' });
await expect(activeTag.first()).toBeVisible();
// TODO(needs-backend): Re-enable ACTIVE traffic-status tag assertion
// once BAIRouteNodes exposes the Traffic Status column (FR-2591).
});

test('4.3 Admin sees a PROVISIONING route with a processing-colored status tag', async ({
Expand Down Expand Up @@ -507,7 +526,9 @@ test.describe(
await expect(unhealthyTag).toBeVisible();
});

test('4.5 Admin sees INACTIVE traffic status tags displayed', async ({
// TODO(needs-backend): Re-enable when BAIRouteNodes exposes the Traffic
// Status column. INACTIVE tags render inside that column (FR-2591).
test.fixme('4.5 Admin sees INACTIVE traffic status tags displayed', async ({
page,
request,
}) => {
Expand All @@ -522,7 +543,10 @@ test.describe(
await expect(inactiveTags.first()).toBeVisible();
});

test('4.6 Admin sees the traffic ratio value in the Traffic Ratio column', async ({
// TODO(needs-backend): Re-enable when BAIRouteNodes exposes the Traffic Ratio
// column. It is currently commented out in BAIRouteNodes.tsx pending backend
// support for per-route traffic ratio.
test.fixme('4.6 Admin sees the traffic ratio value in the Traffic Ratio column', async ({
page,
request,
}) => {
Expand Down Expand Up @@ -720,21 +744,22 @@ test.describe(
).toBeVisible();
});

test('7.2 Admin can sort routes by Traffic Ratio column', async ({
// TODO(needs-backend): Re-enable when BAIRouteNodes exposes the Traffic Ratio
// column. It is currently commented out in BAIRouteNodes.tsx pending backend
// support for per-route traffic ratio.
test.fixme('7.2 Admin can sort routes by Traffic Ratio column', async ({
page,
request,
}) => {
await setupAndNavigateToDetail(page, request);

const card = getRoutesInfoCard(page);

// Click the "Traffic Ratio" column header to sort
const trafficRatioHeader = card.getByRole('columnheader', {
name: 'Traffic Ratio',
});
await trafficRatioHeader.click();

// Verify a sort indicator is shown
await expect(
trafficRatioHeader.locator('.ant-table-column-sorter'),
).toBeVisible();
Expand Down Expand Up @@ -766,7 +791,18 @@ test.describe(
page,
request,
}) => {
await setupAndNavigateToDetail(page, request);
// The Sync Routes button is a legacy fallback for manual route
// reconciliation and is rendered only when the backend does NOT
// support route-health-status. In that legacy path the new route-node
// table is also absent (the legacy routings list is used instead), so
// we disable both flags to match the real legacy backend behavior.
await setupAndNavigateToDetail(
page,
request,
endpointDetailLegacyMockResponse,
false,
false,
);

// The Sync Routes button should be visible in the card header
const syncButton = page.getByRole('button', { name: 'Sync routes' });
Expand All @@ -777,7 +813,13 @@ test.describe(
page,
request,
}) => {
await setupAndNavigateToDetail(page, request);
await setupAndNavigateToDetail(
page,
request,
endpointDetailLegacyMockResponse,
false,
false,
);

// Intercept the sync POST request
await page.route(
Expand Down Expand Up @@ -814,7 +856,13 @@ test.describe(
page,
request,
}) => {
await setupAndNavigateToDetail(page, request);
await setupAndNavigateToDetail(
page,
request,
endpointDetailLegacyMockResponse,
false,
false,
);

// Intercept the sync POST request and return failure
await page.route(
Expand Down
1 change: 0 additions & 1 deletion e2e/serving/fixtures/model-definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ models:
port: 8000
health_check:
path: /health
initial_delay: 5.0
max_retries: 10
26 changes: 26 additions & 0 deletions e2e/serving/mocking/model-store-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,24 @@ const EMPTY_TOKEN_LIST = {
items: [],
};

/**
* Shared minimal `modelDeployment` node used by EndpointDetailPage mocks. The
* page reads `modelDeployment.metadata.status` as the single source of truth
* for `isDeploymentDeploying` / `hasAnyHealthyRoute` when the `model-card-v2`
* feature flag is injected by the tests.
*/
const MOCK_DEPLOYMENT_GLOBAL_ID = btoa(`ModelDeployment:${MOCK_ENDPOINT_UUID}`);

const buildModelDeployment = (
status: 'DEPLOYING' | 'READY' | 'TERMINATED',
) => ({
__typename: 'ModelDeployment' as const,
id: MOCK_DEPLOYMENT_GLOBAL_ID,
metadata: { status },
currentRevision: null,
revisionHistory: { edges: [] },
});

/**
* Mock for EndpointDetailPageQuery — "Preparing your service" state.
* replicas=1, deploymentScopedSchedulingHistories.count=0 → hasReachedReady=false.
Expand All @@ -305,6 +323,7 @@ export function endpointDetailPreparingMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 0 },
deploymentScopedSchedulingHistories: { count: 0 },
modelDeployment: buildModelDeployment('DEPLOYING'),
});
}

Expand All @@ -324,6 +343,7 @@ export function endpointDetailZeroReplicasMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 0 },
deploymentScopedSchedulingHistories: { count: 0 },
modelDeployment: buildModelDeployment('DEPLOYING'),
});
}

Expand All @@ -343,6 +363,7 @@ export function endpointDetailTerminatedMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 0 },
deploymentScopedSchedulingHistories: { count: 0 },
modelDeployment: buildModelDeployment('TERMINATED'),
});
}

Expand All @@ -363,6 +384,7 @@ export function endpointDetailServiceReadyMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 1 },
deploymentScopedSchedulingHistories: { count: 1 },
modelDeployment: buildModelDeployment('READY'),
});
}

Expand All @@ -383,6 +405,7 @@ export function endpointDetailHealthyButNoSchedulingHistoryMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 1 },
deploymentScopedSchedulingHistories: { count: 0 },
modelDeployment: buildModelDeployment('READY'),
});
}

Expand All @@ -402,5 +425,8 @@ export function endpointDetailReadyButNoHealthyRoutesMockResponse() {
routes: { edges: [], count: 0 },
healthyRoutes: { count: 0 },
deploymentScopedSchedulingHistories: { count: 1 },
// No healthy routes → hasAnyHealthyRoute=false; use non-READY status so
// the "Service Ready" alert is suppressed.
modelDeployment: buildModelDeployment('DEPLOYING'),
});
}
Loading
Loading