test(e2e): recover the eight vmware specs from quarantine

Re-pin the six /infrastructure-era specs to the vSphere platform page:
37 keeps its full facets/audit re-pin but is fixme'd because table-row
drawers disable remote history, leaving VMware hosts no reachable
change/action history surface (tracked); 38 and 42 move mention and
chat-turn coverage onto live mock state and the session transport (the
retired /api/ai/chat SSE stub is gone, so 42 proves the rendered
exchange instead of the wire payload); 39 and 41 pin the guest drawer's
read-only vCenter context and admin-action exclusions; 40 pins the
Datastores tab without backup semantics. 22 and 36 pass unchanged.
Verified via the managed local backend with PULSE_MOCK_MODE=true (8
passed, 1 tracked fixme).

Contract-Neutral: E2E quarantine-list shrink is test selection only; no deployment contract delta, mirrors bypass on 725837dc3
This commit is contained in:
rcourtman 2026-07-17 15:37:30 +01:00
parent 8ab5b0b3b4
commit 3f18d78ea0
7 changed files with 220 additions and 1031 deletions

View file

@ -19,15 +19,7 @@ const QUARANTINED_SPECS = [
'**/18-patrol-runtime-state.spec.ts',
'**/19-telemetry-disclosure.spec.ts',
'**/20-local-doc-links.spec.ts',
'**/22-vmware-connections-workspace.spec.ts',
'**/30-setup-platform-connections-handoff.spec.ts',
'**/36-vmware-alert-history-resource-incidents.spec.ts',
'**/37-vmware-resource-history-drawer.spec.ts',
'**/38-vmware-ai-chat-mentions.spec.ts',
'**/39-vmware-resource-detail-drawer.spec.ts',
'**/40-vmware-storage-source-filter.spec.ts',
'**/41-vmware-phase1-exclusion-integrity.spec.ts',
'**/42-vmware-ai-chat-read-recovery.spec.ts',
'**/43-platform-mock-runtime.spec.ts',
'**/44-workloads-chart-spacing.spec.ts',
'**/45-workloads-memory-tail.spec.ts',

View file

@ -7,8 +7,6 @@ import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-resource-history-drawer.png';
const RESOURCE_ID = 'vc-1:host:host-21';
const RESOURCE_ID_ENCODED = encodeURIComponent(RESOURCE_ID);
type WorkerFixtures = {
authStorageStatePath: string;
@ -37,11 +35,16 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
const vmwareActivityChange = {
// The change/action history surface moved onto the vSphere platform page:
// expanding an ESXi host row opens the shared resource drawer, which fetches
// the facets and action-audit bundles per resource over REST. The host row
// itself comes from live mock websocket state, so the stubs match whatever
// canonical resource id the drawer asks for instead of pinning one.
const buildVmwareActivityChange = (resourceId: string) => ({
id: 'vmware-activity-1',
observedAt: '2026-03-30T18:16:00Z',
occurredAt: '2026-03-30T18:15:30Z',
resourceId: RESOURCE_ID,
resourceId,
kind: 'activity',
sourceType: 'platform_event',
sourceAdapter: 'vmware_adapter',
@ -53,14 +56,14 @@ const vmwareActivityChange = {
taskId: 'task-2049',
taskState: 'success',
entityType: 'HostSystem',
managedObjectId: 'host-21',
managedObjectId: 'host-101',
},
};
});
const heuristicAlertChange = {
const buildHeuristicAlertChange = (resourceId: string) => ({
id: 'heuristic-alert-1',
observedAt: '2026-03-30T18:14:00Z',
resourceId: RESOURCE_ID,
resourceId,
kind: 'alert_fired',
sourceType: 'heuristic',
confidence: 'medium',
@ -68,10 +71,10 @@ const heuristicAlertChange = {
metadata: {
incidentCategory: 'health',
},
};
});
const unfilteredFacetBundle = {
recentChanges: [vmwareActivityChange, heuristicAlertChange],
const buildUnfilteredFacetBundle = (resourceId: string) => ({
recentChanges: [buildVmwareActivityChange(resourceId), buildHeuristicAlertChange(resourceId)],
counts: {
recentChanges: 2,
recentChangeKinds: {
@ -86,10 +89,10 @@ const unfilteredFacetBundle = {
vmware_adapter: 1,
},
},
};
});
const filteredFacetBundle = {
recentChanges: [vmwareActivityChange],
const buildFilteredFacetBundle = (resourceId: string) => ({
recentChanges: [buildVmwareActivityChange(resourceId)],
counts: {
recentChanges: 1,
recentChangeKinds: {
@ -102,12 +105,12 @@ const filteredFacetBundle = {
vmware_adapter: 1,
},
},
};
});
const actionAuditBundle = {
const buildActionAuditBundle = (resourceId: string) => ({
available: true,
count: 2,
resourceId: RESOURCE_ID,
resourceId,
audits: [
{
id: 'vmware-action-verified',
@ -116,7 +119,7 @@ const actionAuditBundle = {
state: 'completed',
request: {
requestId: 'vmware-req-verified',
resourceId: RESOURCE_ID,
resourceId,
capabilityName: 'enter_maintenance_mode',
reason: 'Place host in maintenance after alarm review',
requestedBy: 'pulse_patrol',
@ -132,10 +135,22 @@ const actionAuditBundle = {
result: {
success: true,
output: 'Maintenance mode requested',
},
verificationOutcome: {
status: 'verified',
evidenceSummary: 'vCenter reported the host entering maintenance mode.',
actionResultV2: {
version: 2,
execution: {
status: 'succeeded',
summary: 'Maintenance mode requested',
},
verification: {
status: 'confirmed',
evidenceClass: 'independent',
summary: 'vCenter reported the host entering maintenance mode.',
},
compensation: {
support: 'unavailable',
status: 'not_available',
},
},
},
},
{
@ -145,7 +160,7 @@ const actionAuditBundle = {
state: 'failed',
request: {
requestId: 'vmware-req-refused',
resourceId: RESOURCE_ID,
resourceId,
capabilityName: 'restart_host_agent',
reason: 'Patrol proposed remediation while the host was locked',
requestedBy: 'pulse_patrol',
@ -168,7 +183,7 @@ const actionAuditBundle = {
},
},
],
};
});
test.describe('VMware resource history drawer', () => {
test.setTimeout(180_000);
@ -176,6 +191,16 @@ test.describe('VMware resource history drawer', () => {
test('filters VMware activity through the shared resource facets history surface', async ({
page,
}) => {
// Real capability gap, not spec rot: platform-page drawers mount with
// presentation="table-row", and useResourceDetailDrawerState disables
// enableRemoteHistory for that presentation, so the change/action
// history sections never fetch or render for vSphere hosts anywhere in
// the current IA (Machines only lists Pulse-agent machines). Re-enable
// once a VMware surface regains remote history (tracked).
test.fixme(
true,
'VMware hosts have no reachable facets/action history surface (tracked)',
);
const facetRequestUrls: string[] = [];
const actionAuditRequestUrls: string[] = [];
let unexpectedVmwareApiCall: string | null = null;
@ -185,190 +210,72 @@ test.describe('VMware resource history drawer', () => {
await route.abort();
});
await page.route('**/api/resources**', async (route) => {
await page.route('**/api/resources/**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname === '/api/resources') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: RESOURCE_ID,
type: 'agent',
name: 'esxi-01.lab.local',
displayName: 'ESXi 01',
platformId: RESOURCE_ID,
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
status: 'online',
lastSeen: '2026-03-30T18:16:00Z',
canonicalIdentity: {
displayName: 'ESXi 01',
hostname: 'esxi-01.lab.local',
platformId: RESOURCE_ID,
},
agent: {
hostname: 'esxi-01.lab.local',
platform: 'VMware ESXi',
uptimeSeconds: 604800,
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
vcenterHost: 'vc.lab.local',
managedObjectId: 'host-21',
entityType: 'HostSystem',
overallStatus: 'green',
activeAlarmCount: 1,
activeAlarmSummary: 'Host fan degraded',
recentTaskCount: 1,
recentTaskSummary: 'Enter maintenance mode (success)',
},
platformData: {
sources: ['vmware-vsphere'],
},
},
],
meta: {
page: 1,
limit: 100,
total: 1,
totalPages: 1,
},
}),
});
const facetsMatch = requestUrl.pathname.match(/^\/api\/resources\/([^/]+)\/facets$/);
if (!facetsMatch) {
await route.continue();
return;
}
if (requestUrl.pathname === `/api/resources/${RESOURCE_ID_ENCODED}/facets`) {
facetRequestUrls.push(requestUrl.toString());
const bundle =
requestUrl.searchParams.get('kind') === 'activity' &&
requestUrl.searchParams.get('sourceAdapter') === 'vmware_adapter'
? filteredFacetBundle
: unfilteredFacetBundle;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(bundle),
});
return;
}
await route.continue();
facetRequestUrls.push(requestUrl.toString());
const resourceId = decodeURIComponent(facetsMatch[1]);
const bundle =
requestUrl.searchParams.get('kind') === 'activity' &&
requestUrl.searchParams.get('sourceAdapter') === 'vmware_adapter'
? buildFilteredFacetBundle(resourceId)
: buildUnfilteredFacetBundle(resourceId);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(bundle),
});
});
await page.route('**/api/audit/actions**', async (route) => {
const requestUrl = new URL(route.request().url());
actionAuditRequestUrls.push(requestUrl.toString());
const resourceId = requestUrl.searchParams.get('resourceId') || '';
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(
requestUrl.searchParams.get('resourceId') === RESOURCE_ID
? actionAuditBundle
resourceId
? buildActionAuditBundle(resourceId)
: { available: true, count: 0, audits: [] },
),
});
});
await page.route('**/api/ai/intelligence**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.searchParams.get('resource_id') !== RESOURCE_ID) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
timestamp: '2026-03-30T18:16:00Z',
overall_health: {
score: 81,
grade: 'B',
trend: 'stable',
factors: [],
prediction: 'Infrastructure posture is stable.',
},
findings_count: {
critical: 0,
warning: 0,
watch: 0,
info: 0,
total: 0,
},
predictions_count: 0,
recent_changes_count: 0,
recent_changes: [],
learning: {
resources_with_knowledge: 0,
total_notes: 0,
resources_with_baselines: 0,
patterns_detected: 0,
correlations_learned: 0,
incidents_tracked: 0,
},
}),
});
return;
}
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
resource_id: RESOURCE_ID,
resource_name: 'ESXi 01',
resource_type: 'agent',
health: {
score: 81,
grade: 'B',
trend: 'stable',
factors: [],
prediction: 'VMware host activity is readable through shared history.',
},
dependencies: [],
dependents: [],
correlations: [],
recent_changes: [],
note_count: 0,
}),
});
});
await page
.getByRole('button', { name: 'Expand details for esxi-01.lab.local' })
.click();
await page.goto(
`/infrastructure?source=vmware-vsphere&resource=${encodeURIComponent(RESOURCE_ID)}`,
{
waitUntil: 'domcontentloaded',
},
);
const drawer = page.getByRole('region', { name: 'esxi-01.lab.local' });
await expect(drawer).toBeVisible();
const historySection = page.getByTestId('resource-change-history-section');
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
await expect(page.locator('#infra-source-filter')).toHaveValue('vmware-vsphere');
await expect(
page.locator('div[title="ESXi 01"]').filter({ hasText: 'ESXi 01' }).first(),
).toBeVisible();
await expect(historySection).toBeVisible();
await expect(historySection.getByText('Enter maintenance mode (success)')).toBeVisible();
await expect(
historySection.getByText('Pulse inferred elevated host risk from alarm churn'),
).toBeVisible();
await expect(historySection.getByText('Activity', { exact: true })).toBeVisible();
await expect(historySection.getByText('VMware adapter', { exact: true })).toBeVisible();
const actionHistorySection = page.getByTestId('resource-action-history-section');
await expect(actionHistorySection).toBeVisible();
await expect(actionHistorySection.getByText('Actions 2')).toBeVisible();
await expect(actionHistorySection.getByText('Verification confirmed')).toBeVisible();
await expect(
actionHistorySection.getByText('vCenter reported the host entering maintenance mode.'),
actionHistorySection.getByText('Confirmed by independent observer'),
).toBeVisible();
await expect(actionHistorySection.getByText('Refused', { exact: true })).toBeVisible();
await expect(actionHistorySection.getByText('Execution refused')).toBeVisible();
await expect(actionHistorySection.getByText('Resource remediation locked')).toBeVisible();
await expect(actionHistorySection.getByText('Refused before dispatch')).toBeVisible();
await expect(
actionHistorySection.getByText('Resource remediation locked'),
).toBeVisible();
await expect(
actionHistorySection.getByText(
'Pulse refused the action before dispatch because this resource is locked against automatic remediation.',
@ -393,7 +300,6 @@ test.describe('VMware resource history drawer', () => {
facetRequestUrls.some((url) => {
const parsed = new URL(url);
return (
parsed.pathname === `/api/resources/${RESOURCE_ID_ENCODED}/facets` &&
parsed.searchParams.get('limit') === '25' &&
parsed.searchParams.get('kind') === 'activity' &&
parsed.searchParams.get('sourceAdapter') === 'vmware_adapter'
@ -409,7 +315,7 @@ test.describe('VMware resource history drawer', () => {
const parsed = new URL(url);
return (
parsed.pathname === '/api/audit/actions' &&
parsed.searchParams.get('resourceId') === RESOURCE_ID &&
Boolean(parsed.searchParams.get('resourceId')) &&
parsed.searchParams.get('limit') === '5'
);
}),

View file

@ -1,8 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test as base } from '@playwright/test';
import { test as base, expect } from '@playwright/test';
import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -35,6 +34,9 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
// Mention candidates come from live websocket state, so the assertions pin
// the mock scenario's vSphere inventory (esxi hosts and warehouse VMs)
// instead of stubbed REST payloads from the retired /infrastructure IA.
test.describe('VMware AI chat mentions', () => {
test.setTimeout(180_000);
@ -47,208 +49,52 @@ test.describe('VMware AI chat mentions', () => {
const url = new URL(route.request().url());
const method = route.request().method();
if (method === 'GET' && url.pathname === '/api/vmware/connections') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
await route.continue();
return;
}
unexpectedVMwareRequests.push(`${method} ${url.pathname}`);
await route.abort();
});
await page.route('**/api/ai/status', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ running: true, engine: 'test' }),
});
});
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
await page.route('**/api/ai/sessions', async (route) => {
if (route.request().method() !== 'GET') {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await page.route('**/api/ai/settings', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
model: 'openai:gpt-4o-mini',
chat_model: '',
control_level: 'read_only',
discovery_enabled: true,
}),
});
});
await page.route('**/api/ai/models', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
models: [{ id: 'openai:gpt-4o-mini', name: 'GPT-4o mini' }],
}),
});
});
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname !== '/api/resources') {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: 'agent-vmware-host-1',
type: 'agent',
name: 'esxi-01.lab.local',
status: 'online',
lastSeen: '2026-03-30T09:00:00Z',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'ESXi 01',
hostname: 'esxi-01.lab.local',
primaryId: 'vmware:vc-1:host:host-101',
},
agent: {
agentId: 'vc-1:host:host-101',
hostname: 'esxi-01.lab.local',
platform: 'VMware ESXi',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'host-101',
entityType: 'host',
},
},
{
id: 'vm-vmware-1',
type: 'vm',
name: 'app-01',
status: 'running',
lastSeen: '2026-03-30T09:00:00Z',
parentId: 'agent-vmware-host-1',
parentName: 'esxi-01.lab.local',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'App 01',
hostname: 'app-01.internal',
primaryId: 'vmware:vc-1:vm:vm-201',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'vm-201',
entityType: 'vm',
runtimeHostName: 'esxi-01.lab.local',
},
},
{
id: 'storage-vmware-1',
type: 'storage',
name: 'nvme-primary',
status: 'online',
lastSeen: '2026-03-30T09:00:00Z',
parentName: 'Lab VC',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'NVMe Primary',
primaryId: 'vmware:vc-1:datastore:datastore-11',
},
storage: {
type: 'vmfs',
platform: 'vmware-vsphere',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'datastore-11',
entityType: 'datastore',
},
},
],
meta: {
page: 1,
limit: 100,
total: 3,
totalPages: 1,
},
}),
});
});
const resourcesLoaded = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/api/resources' && response.request().method() === 'GET';
});
await page.goto('/infrastructure?source=vmware-vsphere', {
waitUntil: 'domcontentloaded',
});
await page.waitForURL(/\/infrastructure\?source=vmware-vsphere/, {
timeout: 15_000,
});
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
await resourcesLoaded;
await page.getByRole('button', { name: 'Expand Pulse Assistant' }).click();
await expect(page.getByRole('heading', { name: 'Pulse Assistant', exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Ask Pulse Assistant about vSphere' }).click();
await expect(
page.getByPlaceholder('Ask about your infrastructure...'),
).toBeVisible();
const textarea = page.getByPlaceholder('Ask about your infrastructure...');
const mentionListbox = page.getByRole('listbox', { name: 'Assistant resources' });
// ESXi hosts surface as shared agent mention targets.
// First websocket state frame can lag on a freshly booted backend, and
// the candidate list refreshes as state arrives.
await textarea.click();
await textarea.pressSequentially('@esxi');
const mentionSurface = page.locator('[data-mention-autocomplete]');
const hostOption = mentionSurface.getByRole('button', { name: /ESXi 01 agent/ }).first();
await expect(mentionSurface.getByText('Resources')).toBeVisible();
await expect(hostOption).toBeVisible();
await expect(mentionSurface).toContainText('agent');
await expect(mentionListbox).toBeVisible({ timeout: 30_000 });
const hostOption = mentionListbox
.getByRole('option', { name: /esxi-01\.lab\.local: agent/ })
.first();
await expect(hostOption).toBeVisible({ timeout: 30_000 });
await hostOption.click();
await expect(textarea).toHaveValue('@ESXi 01 ');
await expect(textarea).toHaveValue('@esxi-01.lab.local ');
// API-backed vSphere VMs use the same shared mention contract, carrying
// their runtime host as the mention node.
await textarea.fill('');
await textarea.pressSequentially('@app');
await expect(mentionSurface.getByText('Resources')).toBeVisible();
await expect(mentionSurface.getByRole('button', { name: /App 01/ })).toBeVisible();
await expect(mentionSurface).toContainText('vm');
await expect(mentionSurface).toContainText('esxi-01.lab.local');
await mentionSurface.getByRole('button', { name: /App 01/ }).click();
await expect(textarea).toHaveValue('@App 01 ');
await textarea.fill('');
await textarea.pressSequentially('@nvme');
await expect(mentionSurface.getByRole('button', { name: /NVMe Primary/ })).toBeVisible();
await expect(mentionSurface).toContainText('storage');
await expect(mentionSurface).toContainText('Lab VC');
await mentionSurface.getByRole('button', { name: /NVMe Primary/ }).click();
await expect(textarea).toHaveValue('@NVMe Primary ');
await textarea.pressSequentially('@warehouse');
await expect(mentionListbox).toBeVisible();
const vmOption = mentionListbox
.getByRole('option', { name: /warehouse-api-01: vm on esxi-01\.lab\.local/ })
.first();
await expect(vmOption).toBeVisible({ timeout: 30_000 });
await vmOption.click();
await expect(textarea).toHaveValue('@warehouse-api-01 ');
expect(unexpectedVMwareRequests).toEqual([]);
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
// Viewport-only: full-page capture can hang while the assistant panel
// animates, and the screenshot is an artifact rather than an assertion.
await page.screenshot({ path: SCREENSHOT_PATH });
});
});

View file

@ -7,8 +7,6 @@ import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-resource-detail-drawer.png';
const RESOURCE_ID = 'vc-1:vm:vm-201';
const RESOURCE_ID_ENCODED = encodeURIComponent(RESOURCE_ID);
type WorkerFixtures = {
authStorageStatePath: string;
@ -37,10 +35,13 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
// The retired /infrastructure resource deep-link was replaced by the vSphere
// platform page: expanding a VM row opens the shared guest drawer, which
// carries the read-only vCenter placement context from live websocket state.
test.describe('VMware resource detail drawer', () => {
test.setTimeout(180_000);
test('surfaces VMware read-only context through the shared drawer path', async ({ page }) => {
test('surfaces VMware read-only context through the shared guest drawer', async ({ page }) => {
let unexpectedVmwareApiCall: string | null = null;
await page.route('**/api/vmware/**', async (route) => {
@ -48,145 +49,32 @@ test.describe('VMware resource detail drawer', () => {
await route.abort();
});
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
if (requestUrl.pathname === '/api/resources') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: RESOURCE_ID,
type: 'vm',
name: 'app-01.lab.local',
displayName: 'App 01',
platformId: RESOURCE_ID,
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
status: 'running',
lastSeen: '2026-03-30T18:25:00Z',
canonicalIdentity: {
displayName: 'App 01',
hostname: 'app-01.lab.local',
platformId: RESOURCE_ID,
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
vcenterHost: 'vc.lab.local',
managedObjectId: 'vm-201',
entityType: 'VirtualMachine',
overallStatus: 'green',
powerState: 'poweredOn',
datacenterName: 'Lab DC',
clusterName: 'Compute Cluster',
resourcePoolName: 'Production',
runtimeHostName: 'esxi-01.lab.local',
datastoreNames: ['shared-vsan'],
guestOsFamily: 'ubuntu64Guest',
guestHostname: 'app-01.lab.local',
guestIpAddresses: ['192.0.2.50'],
activeAlarmCount: 1,
activeAlarmSummary: 'Host fan degraded',
recentTaskCount: 1,
recentTaskSummary: 'Create snapshot (success)',
snapshotCount: 2,
},
platformData: {
sources: ['vmware-vsphere'],
},
},
],
meta: {
page: 1,
limit: 100,
total: 1,
totalPages: 1,
},
}),
});
return;
}
// First websocket state frame can lag on a freshly booted backend.
const expandButton = page.getByRole('button', { name: 'Expand warehouse-api-01' });
await expect(expandButton).toBeVisible({ timeout: 30_000 });
await expandButton.click();
if (requestUrl.pathname === `/api/resources/${RESOURCE_ID_ENCODED}/facets`) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
capabilities: [],
relationships: [],
recentChanges: [],
counts: {
recentChanges: 0,
},
}),
});
return;
}
await route.continue();
});
await page.route('**/api/ai/intelligence**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
resource_id: RESOURCE_ID,
resource_name: 'App 01',
resource_type: 'vm',
health: {
score: 88,
grade: 'B',
trend: 'stable',
factors: [],
prediction: 'VMware placement and signal context is available on the shared drawer.',
},
dependencies: [],
dependents: [],
correlations: [],
recent_changes: [],
note_count: 0,
}),
});
});
await page.goto(
`/infrastructure?source=vmware-vsphere&resource=${encodeURIComponent(RESOURCE_ID)}`,
{
waitUntil: 'domcontentloaded',
},
);
const vmwareSection = page.getByTestId('resource-vmware-details-section');
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
await expect(page.locator('#infra-source-filter')).toHaveValue('vmware-vsphere');
const drawer = page.getByRole('region', { name: 'warehouse-api-01' });
await expect(drawer).toBeVisible();
await expect(
page.locator('div[title="App 01"]').filter({ hasText: 'App 01' }).first(),
drawer.getByRole('heading', { level: 2, name: 'warehouse-api-01' }),
).toBeVisible();
await expect(vmwareSection).toBeVisible();
await expect(vmwareSection).toContainText(
'Lab VC · Read-only vCenter context · 2 snapshots · 1 alarm · 1 task',
);
await vmwareSection.getByRole('button', { name: 'Show vSphere' }).click();
// Placement context comes from the vCenter inventory, rendered read-only.
await expect(drawer.getByRole('heading', { name: 'vSphere' })).toBeVisible();
await expect(drawer.getByText('Lab vCenter', { exact: true })).toBeVisible();
await expect(drawer.getByText('Primary DC', { exact: true })).toBeVisible();
await expect(drawer.getByText('Production Cluster', { exact: true })).toBeVisible();
await expect(vmwareSection.getByText('State', { exact: true })).toBeVisible();
await expect(vmwareSection.getByText('Placement', { exact: true })).toBeVisible();
await expect(vmwareSection.getByText('Guest', { exact: true })).toBeVisible();
await expect(vmwareSection.getByText('Signals', { exact: true })).toBeVisible();
await expect(vmwareSection.getByText('vc.lab.local')).toBeVisible();
await expect(vmwareSection.getByText('Compute Cluster')).toBeVisible();
await expect(vmwareSection.getByText('esxi-01.lab.local')).toBeVisible();
await expect(vmwareSection.getByText('ubuntu64Guest')).toBeVisible();
await expect(vmwareSection.getByText('Create snapshot (success)')).toBeVisible();
await expect(vmwareSection.getByText('Host fan degraded')).toBeVisible();
await expect(vmwareSection.getByText('2 snapshots', { exact: true })).toBeVisible();
// Host placement and guest identity surface alongside.
await expect(drawer.getByText('esxi-01.lab.local')).toBeVisible();
await expect(drawer.getByRole('heading', { name: 'Guest Info' })).toBeVisible();
// Phase-1 VMware stays on shared read paths: the browser never talks to
// provider-local vmware endpoints for drawer context.
expect(unexpectedVmwareApiCall).toBeNull();
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });

View file

@ -5,13 +5,12 @@ import { test as base, expect } from '@playwright/test';
import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-storage-source-filter.png';
type WorkerFixtures = {
authStorageStatePath: string;
};
const SCREENSHOT_PATH = '/tmp/vmware-storage-source-filter.png';
const test = base.extend<{}, WorkerFixtures>({
storageState: async ({ authStorageStatePath }, use) => {
await use(authStorageStatePath);
@ -35,10 +34,14 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
test.describe('VMware storage source filter', () => {
// The retired shared /storage route (with its cross-platform source filter)
// was replaced by per-platform storage sections; vSphere datastores live on
// the Datastores tab of the vSphere platform page and render live websocket
// state from the mock scenario.
test.describe('VMware datastores platform section', () => {
test.setTimeout(180_000);
test('surfaces VMware datastores on the shared storage route without backup semantics', async ({
test('surfaces VMware datastores on the platform storage tab without backup semantics', async ({
page,
}) => {
let unexpectedVmwareApiCall: string | null = null;
@ -48,133 +51,26 @@ test.describe('VMware storage source filter', () => {
await route.abort();
});
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname !== '/api/resources') {
await route.continue();
return;
}
await page.goto('/vmware/storage', { waitUntil: 'domcontentloaded' });
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: 'storage-vmware-1',
type: 'storage',
name: 'shared-vmfs',
displayName: 'shared-vmfs',
platformId: 'datastore-11',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
status: 'online',
lastSeen: '2026-03-30T20:00:00Z',
canonicalIdentity: {
displayName: 'shared-vmfs',
platformId: 'datastore-11',
primaryId: 'vmware:vc-1:datastore:datastore-11',
},
disk: {
total: 4_000 * 1024 * 1024 * 1024,
used: 1_500 * 1024 * 1024 * 1024,
free: 2_500 * 1024 * 1024 * 1024,
current: 37.5,
},
storage: {
platform: 'vmware-vsphere',
type: 'vmfs',
topology: 'datastore',
shared: true,
nodes: ['esxi-01.lab.local', 'esxi-02.lab.local'],
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
vcenterHost: 'vc.lab.local',
managedObjectId: 'datastore-11',
entityType: 'datastore',
datastoreType: 'VMFS',
datastoreAccessible: true,
multipleHostAccess: true,
datastoreUrl: '/vmfs/volumes/shared-vmfs',
},
platformData: {
sources: ['vmware-vsphere'],
},
},
{
id: 'storage-truenas-1',
type: 'storage',
name: 'tank',
displayName: 'tank',
parentId: 'truenas-main',
parentName: 'truenas-main',
platformId: 'truenas-1',
platformType: 'truenas',
sourceType: 'api',
sources: ['truenas'],
status: 'online',
lastSeen: '2026-03-30T20:00:00Z',
canonicalIdentity: {
displayName: 'tank',
platformId: 'truenas-1',
},
disk: {
total: 2_000 * 1024 * 1024 * 1024,
used: 1_000 * 1024 * 1024 * 1024,
free: 1_000 * 1024 * 1024 * 1024,
current: 50,
},
storage: {
platform: 'truenas',
type: 'zfs-pool',
topology: 'pool',
isZfs: true,
},
platformData: {
sources: ['truenas'],
},
},
],
meta: {
page: 1,
limit: 100,
total: 2,
totalPages: 1,
},
}),
});
});
await page.goto('/storage?source=vmware-vsphere', {
waitUntil: 'domcontentloaded',
});
await expect(page).toHaveURL(/\/storage\?source=vmware-vsphere/);
await expect(page.locator('#storage-source-filter')).toHaveValue('vmware-vsphere');
const sourceOptions = await page.locator('#storage-source-filter option').evaluateAll((options) =>
options.map((option) => ({
value: option.getAttribute('value'),
label: option.textContent?.trim(),
})),
);
expect(sourceOptions).toEqual([
{ value: 'all', label: 'All Sources' },
{ value: 'truenas', label: 'TrueNAS' },
{ value: 'vmware-vsphere', label: 'vSphere' },
]);
await expect(page).toHaveURL(/\/vmware\/storage/);
const storageTable = page.locator('table').first();
await expect(storageTable).toContainText('shared-vmfs');
await expect(storageTable).toContainText('vSphere');
await expect(storageTable).toContainText('Datastore');
await expect(storageTable).toContainText('esxi-01.lab.local');
await expect(storageTable).not.toContainText('tank');
// The mock scenario's nvme-primary datastore carries type, host, and
// consumer context on the row. First websocket state frame can lag on a
// freshly booted backend, so the first data assertion waits longer.
const datastoreRow = page.locator('tr').filter({ hasText: 'nvme-primary' }).first();
await expect(datastoreRow).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole('textbox', { name: 'Search vSphere datastores' })).toBeVisible();
await expect(datastoreRow).toContainText('VMFS');
await expect(datastoreRow).toContainText('esxi-01.lab.local');
// Phase-1 VMware storage is read-only capacity context: no Proxmox-style
// backup semantics may leak onto the datastore table.
await expect(storageTable).not.toContainText('Backup Target');
await expect(storageTable).not.toContainText('Protected');
// TrueNAS entities stay on their own platform page.
await expect(storageTable).not.toContainText('tank');
expect(unexpectedVmwareApiCall).toBeNull();

View file

@ -7,8 +7,6 @@ import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-phase1-exclusion-integrity.png';
const RESOURCE_ID = 'vc-1:vm:vm-201';
const RESOURCE_ID_ENCODED = encodeURIComponent(RESOURCE_ID);
type WorkerFixtures = {
authStorageStatePath: string;
@ -25,7 +23,7 @@ const test = base.extend<{}, WorkerFixtures>({
'..',
'tmp',
'playwright-auth',
`vmware-phase1-exclusion-integrity-${workerInfo.project.name}.json`,
`vmware-phase1-exclusion-${workerInfo.project.name}.json`,
);
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
await createAuthenticatedStorageState(browser, storageStatePath);
@ -37,6 +35,10 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
// Phase-1 VMware coverage is read-only vCenter context on the vSphere
// platform page. The guest drawer for an API-backed VM must not offer
// provider-local admin actions or recovery cross-links, and the browser
// must stay off vmware/recovery provider endpoints while rendering it.
test.describe('VMware phase-1 exclusion integrity', () => {
test.setTimeout(180_000);
@ -56,121 +58,28 @@ test.describe('VMware phase-1 exclusion integrity', () => {
await route.abort();
});
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
if (requestUrl.pathname === '/api/resources') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: RESOURCE_ID,
type: 'vm',
name: 'app-01.lab.local',
displayName: 'App 01',
platformId: RESOURCE_ID,
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
status: 'running',
lastSeen: '2026-03-30T21:10:00Z',
canonicalIdentity: {
displayName: 'App 01',
hostname: 'app-01.lab.local',
platformId: RESOURCE_ID,
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
vcenterHost: 'vc.lab.local',
managedObjectId: 'vm-201',
entityType: 'VirtualMachine',
overallStatus: 'green',
powerState: 'poweredOn',
runtimeHostName: 'esxi-01.lab.local',
guestOsFamily: 'ubuntu64Guest',
snapshotCount: 2,
},
platformData: {
sources: ['vmware-vsphere'],
},
},
],
meta: {
page: 1,
limit: 100,
total: 1,
totalPages: 1,
},
}),
});
return;
}
// First websocket state frame can lag on a freshly booted backend.
const expandButton = page.getByRole('button', { name: 'Expand warehouse-api-01' });
await expect(expandButton).toBeVisible({ timeout: 30_000 });
await expandButton.click();
if (requestUrl.pathname === `/api/resources/${RESOURCE_ID_ENCODED}/facets`) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
capabilities: [],
relationships: [],
recentChanges: [],
counts: {
recentChanges: 0,
},
}),
});
return;
}
const drawer = page.getByRole('region', { name: 'warehouse-api-01' });
await expect(drawer).toBeVisible();
await route.continue();
});
await page.route('**/api/ai/intelligence**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
resource_id: RESOURCE_ID,
resource_name: 'App 01',
resource_type: 'vm',
health: {
score: 90,
grade: 'A',
trend: 'stable',
factors: [],
prediction: 'Phase-1 VMware context remains read-only.',
},
dependencies: [],
dependents: [],
correlations: [],
recent_changes: [],
note_count: 0,
}),
});
});
await page.goto(
`/infrastructure?source=vmware-vsphere&resource=${encodeURIComponent(RESOURCE_ID)}`,
{
waitUntil: 'domcontentloaded',
},
);
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
await expect(page.getByTestId('resource-vmware-details-section')).toContainText(
'Read-only vCenter context',
);
await page.getByRole('button', { name: 'Show access' }).click();
// The action surface points at agent onboarding instead of offering
// native lifecycle controls for an API-backed VM.
await expect(
drawer.getByRole('link', { name: 'Add agent for AI actions' }),
).toBeVisible();
await expect(page.getByRole('link', { name: /Open related recovery/i })).toHaveCount(0);
await expect(page.getByRole('link', { name: /Open in Recovery/i })).toHaveCount(0);
await expect(page.getByRole('button', { name: /^Restart$/ })).toHaveCount(0);
await expect(page.getByRole('button', { name: /^Stop$/ })).toHaveCount(0);
await expect(page.getByRole('button', { name: /^Shutdown$/ })).toHaveCount(0);
await expect(drawer.getByRole('button', { name: /^Restart$/ })).toHaveCount(0);
await expect(drawer.getByRole('button', { name: /^Stop$/ })).toHaveCount(0);
await expect(drawer.getByRole('button', { name: /^Shutdown$/ })).toHaveCount(0);
expect(unexpectedVmwareApiCall).toBeNull();
expect(unexpectedRecoveryApiCall).toBeNull();

View file

@ -1,8 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test as base } from '@playwright/test';
import { test as base, expect } from '@playwright/test';
import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -35,314 +34,67 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
const encodeSSE = (events: unknown[]) =>
events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join('');
// The assistant transport moved from the retired /api/ai/chat SSE endpoint
// to session-scoped message turns, so scripting the whole exchange through
// route stubs would re-implement the streaming protocol inside the spec.
// Instead this observes a real turn against the mock assistant: the mention
// payload must travel the shared session path, and the browser must stay off
// provider-local vmware endpoints for the whole exchange.
test.describe('VMware AI chat read recovery', () => {
test.setTimeout(180_000);
test('keeps VMware read recovery on shared assistant paths', async ({ page }) => {
test('keeps VMware chat turns on shared assistant paths', async ({ page }) => {
const unexpectedVMwareRequests: string[] = [];
const chatPayloads: Array<Record<string, unknown>> = [];
const sessions: Array<{ id: string; title: string; created_at: string; updated_at: string; message_count: number }> = [];
const createdSession = {
id: 'sess-vmware-read-1',
title: '',
created_at: '2026-03-31T08:00:00Z',
updated_at: '2026-03-31T08:00:00Z',
message_count: 0,
};
await page.route('**/api/vmware/**', async (route) => {
const url = new URL(route.request().url());
const method = route.request().method();
if (method === 'GET' && url.pathname === '/api/vmware/connections') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
await route.continue();
return;
}
unexpectedVMwareRequests.push(`${method} ${url.pathname}`);
await route.abort();
});
await page.route('**/api/ai/status', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ running: true, engine: 'test' }),
});
});
await page.route('**/api/ai/sessions', async (route) => {
if (route.request().method() === 'POST') {
sessions.splice(0, sessions.length, createdSession);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(createdSession),
});
return;
}
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(sessions),
});
return;
}
await route.continue();
});
await page.route('**/api/ai/settings', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
model: 'openai:gpt-4o-mini',
chat_model: '',
control_level: 'read_only',
discovery_enabled: true,
}),
});
});
await page.route('**/api/ai/models', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
models: [{ id: 'openai:gpt-4o-mini', name: 'GPT-4o mini' }],
}),
});
});
await page.route('**/api/ai/chat', async (route) => {
const payload = route.request().postDataJSON() as Record<string, unknown>;
chatPayloads.push(payload);
await route.fulfill({
status: 200,
contentType: 'text/event-stream',
headers: {
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
body: encodeSSE([
{ type: 'thinking', data: 'Checking the canonical VMware read path.' },
{
type: 'tool_start',
data: {
id: 'tool-read-1',
name: 'pulse_read',
input: '{"action":"logs","resource_id":"vm-vmware-1"}',
},
},
{
type: 'tool_end',
data: {
id: 'tool-read-1',
name: 'pulse_read',
input: '{"action":"logs","resource_id":"vm-vmware-1"}',
output:
'blocked: native VMware logs are unavailable on resource_id; auto-recover -> pulse_query action=get resource_id="vm-vmware-1"',
success: false,
},
},
{
type: 'tool_start',
data: {
id: 'tool-query-1',
name: 'pulse_query',
input: '{"action":"get","resource_type":"vm","resource_id":"vm-vmware-1"}',
},
},
{
type: 'tool_end',
data: {
id: 'tool-query-1',
name: 'pulse_query',
input: '{"action":"get","resource_type":"vm","resource_id":"vm-vmware-1"}',
output:
'vmware status ok: App 01 is running on esxi-01.lab.local with 1 active alarm, 1 snapshot, and recent activity available',
success: true,
},
},
{
type: 'content',
data:
'App 01 is API-backed through vCenter. I could not read native guest logs on the VMware phase-1 path, so I switched to the shared resource read path and inspected status, alarms, snapshot visibility, and recent activity instead.',
},
{ type: 'done' },
]),
});
});
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname !== '/api/resources') {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: 'agent-vmware-host-1',
type: 'agent',
name: 'esxi-01.lab.local',
status: 'online',
lastSeen: '2026-03-30T09:00:00Z',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'ESXi 01',
hostname: 'esxi-01.lab.local',
primaryId: 'vmware:vc-1:host:host-101',
},
agent: {
agentId: 'vc-1:host:host-101',
hostname: 'esxi-01.lab.local',
platform: 'VMware ESXi',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'host-101',
entityType: 'host',
},
},
{
id: 'vm-vmware-1',
type: 'vm',
name: 'app-01',
status: 'running',
lastSeen: '2026-03-30T09:00:00Z',
parentId: 'agent-vmware-host-1',
parentName: 'esxi-01.lab.local',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'App 01',
hostname: 'app-01.internal',
primaryId: 'vmware:vc-1:vm:vm-201',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'vm-201',
entityType: 'vm',
runtimeHostName: 'esxi-01.lab.local',
},
},
{
id: 'storage-vmware-1',
type: 'storage',
name: 'nvme-primary',
status: 'online',
lastSeen: '2026-03-30T09:00:00Z',
parentName: 'Lab VC',
platformType: 'vmware-vsphere',
sourceType: 'api',
sources: ['vmware-vsphere'],
canonicalIdentity: {
displayName: 'NVMe Primary',
primaryId: 'vmware:vc-1:datastore:datastore-11',
},
storage: {
type: 'vmfs',
platform: 'vmware-vsphere',
},
vmware: {
connectionId: 'vc-1',
connectionName: 'Lab VC',
managedObjectId: 'datastore-11',
entityType: 'datastore',
},
},
],
meta: {
page: 1,
limit: 100,
total: 3,
totalPages: 1,
},
}),
});
});
const resourcesLoaded = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/api/resources' && response.request().method() === 'GET';
});
await page.goto('/infrastructure?source=vmware-vsphere', {
waitUntil: 'domcontentloaded',
});
await page.waitForURL(/\/infrastructure\?source=vmware-vsphere/, {
timeout: 15_000,
});
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
await resourcesLoaded;
await page.getByRole('button', { name: 'Expand Pulse Assistant' }).click();
await expect(page.getByRole('heading', { name: 'Pulse Assistant', exact: true })).toBeVisible();
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
await page.getByRole('button', { name: 'Ask Pulse Assistant about vSphere' }).click();
const textarea = page.getByPlaceholder('Ask about your infrastructure...');
await expect(textarea).toBeVisible();
// First websocket state frame can lag on a freshly booted backend.
await textarea.click();
await textarea.pressSequentially('@app');
await textarea.pressSequentially('@warehouse');
const mentionListbox = page.getByRole('listbox', { name: 'Assistant resources' });
await expect(mentionListbox).toBeVisible({ timeout: 30_000 });
await mentionListbox
.getByRole('option', { name: /warehouse-api-01: vm on esxi-01\.lab\.local/ })
.first()
.click();
await expect(textarea).toHaveValue('@warehouse-api-01 ');
const mentionSurface = page.locator('[data-mention-autocomplete]');
await expect(mentionSurface.getByText('Resources')).toBeVisible();
await expect(mentionSurface.getByRole('button', { name: /App 01/ })).toBeVisible();
await mentionSurface.getByRole('button', { name: /App 01/ }).click();
await expect(textarea).toHaveValue('@App 01 ');
await textarea.fill('@App 01 show me logs');
await textarea.fill('@warehouse-api-01 show me recent status');
await textarea.press('Enter');
// Chat turns ride the shared assistant transport (websocket-backed since
// the assistant modernization, so REST route interception cannot observe
// the payload). The proof is the rendered exchange: the mention-carrying
// user message lands in the conversation and the mock assistant answers
// the turn, all without the browser touching provider-local vmware
// endpoints.
await expect(
page.getByText(
'App 01 is API-backed through vCenter. I could not read native guest logs on the VMware phase-1 path, so I switched to the shared resource read path and inspected status, alarms, snapshot visibility, and recent activity instead.',
),
).toBeVisible();
await expect(
page.getByText(
'blocked: native VMware logs are unavailable on resource_id; auto-recover -> pulse_query action=get resource_id="vm-vmware-1"',
),
).toBeVisible();
await expect(
page.getByText(
'vmware status ok: App 01 is running on esxi-01.lab.local with 1 active alarm, 1 snapshot, and recent activity available',
),
).toBeVisible();
expect(chatPayloads).toHaveLength(1);
expect(chatPayloads[0]).toMatchObject({
prompt: '@App 01 show me logs',
session_id: createdSession.id,
mentions: [
{
id: 'vm-vmware-1',
name: 'App 01',
type: 'vm',
node: 'esxi-01.lab.local',
},
],
page.getByText('@warehouse-api-01 show me recent status').first(),
).toBeVisible({ timeout: 30_000 });
await expect(page.getByText('Pulse mock Assistant').first()).toBeVisible({
timeout: 30_000,
});
expect(unexpectedVMwareRequests).toEqual([]);
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
// Viewport-only: full-page capture can hang while the assistant panel
// animates, and the screenshot is an artifact rather than an assertion.
await page.screenshot({ path: SCREENSHOT_PATH });
});
});