From 3f18d78ea018c23b0be4554982cdd57a28fa29cc Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Jul 2026 15:37:30 +0100 Subject: [PATCH] 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 --- tests/integration/playwright.config.ts | 8 - .../37-vmware-resource-history-drawer.spec.ts | 256 +++++--------- .../tests/38-vmware-ai-chat-mentions.spec.ts | 224 ++---------- .../39-vmware-resource-detail-drawer.spec.ts | 158 ++------- .../40-vmware-storage-source-filter.spec.ts | 148 ++------ ...-vmware-phase1-exclusion-integrity.spec.ts | 133 ++----- .../42-vmware-ai-chat-read-recovery.spec.ts | 324 ++---------------- 7 files changed, 220 insertions(+), 1031 deletions(-) diff --git a/tests/integration/playwright.config.ts b/tests/integration/playwright.config.ts index ff3437f19..21aa0c3c0 100644 --- a/tests/integration/playwright.config.ts +++ b/tests/integration/playwright.config.ts @@ -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', diff --git a/tests/integration/tests/37-vmware-resource-history-drawer.spec.ts b/tests/integration/tests/37-vmware-resource-history-drawer.spec.ts index 174489354..b9d60db39 100644 --- a/tests/integration/tests/37-vmware-resource-history-drawer.spec.ts +++ b/tests/integration/tests/37-vmware-resource-history-drawer.spec.ts @@ -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' ); }), diff --git a/tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts b/tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts index 32a87c2a7..c424a9dd7 100644 --- a/tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts +++ b/tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts @@ -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 }); }); }); diff --git a/tests/integration/tests/39-vmware-resource-detail-drawer.spec.ts b/tests/integration/tests/39-vmware-resource-detail-drawer.spec.ts index 6809decca..677050290 100644 --- a/tests/integration/tests/39-vmware-resource-detail-drawer.spec.ts +++ b/tests/integration/tests/39-vmware-resource-detail-drawer.spec.ts @@ -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 }); diff --git a/tests/integration/tests/40-vmware-storage-source-filter.spec.ts b/tests/integration/tests/40-vmware-storage-source-filter.spec.ts index 0eedc41a2..3c75e4c4e 100644 --- a/tests/integration/tests/40-vmware-storage-source-filter.spec.ts +++ b/tests/integration/tests/40-vmware-storage-source-filter.spec.ts @@ -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(); diff --git a/tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts b/tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts index 68fe4dd91..154e9d137 100644 --- a/tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts +++ b/tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts @@ -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(); diff --git a/tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts b/tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts index f8da828ee..c79c2f2cc 100644 --- a/tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts +++ b/tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts @@ -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> = []; - 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; - 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 }); }); });