mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Stabilize RC runtime ownership
Contract-Neutral: RC qualification fixes preserve existing public API, tenant, monitoring, and organization contracts while correcting canonical runtime ownership and test fixtures.
This commit is contained in:
parent
6eb559905d
commit
c73e78aeea
16 changed files with 513 additions and 169 deletions
|
|
@ -128,10 +128,17 @@ const prepareOrganizationAuditFixture = async (
|
|||
{
|
||||
method: "POST",
|
||||
data: { userId: "mobile-audit-viewer", role: "viewer" },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Org-ID": source.id,
|
||||
"X-Pulse-Org-ID": source.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(invitation.ok(), "mobile access fixture invitation").toBeTruthy();
|
||||
expect(
|
||||
invitation.ok(),
|
||||
`mobile access fixture invitation: ${invitation.status()} ${await invitation.text()}`,
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
if (route === "/settings/organization/sharing") {
|
||||
|
|
@ -147,10 +154,17 @@ const prepareOrganizationAuditFixture = async (
|
|||
resourceName: "Mobile Audit Shared View",
|
||||
accessRole: "viewer",
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Org-ID": source.id,
|
||||
"X-Pulse-Org-ID": source.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(share.ok(), "mobile sharing fixture").toBeTruthy();
|
||||
expect(
|
||||
share.ok(),
|
||||
`mobile sharing fixture: ${share.status()} ${await share.text()}`,
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
return createdOrgIDs;
|
||||
|
|
@ -330,10 +344,6 @@ test.describe("Settings mobile optimization audit", () => {
|
|||
await expect(organizationSwitcher).toHaveValue("default", {
|
||||
timeout: 20000,
|
||||
});
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(
|
||||
page.getByRole("combobox", { name: "Organization" }),
|
||||
).toHaveValue("default", { timeout: 20000 });
|
||||
}
|
||||
}
|
||||
for (const orgID of [...createdOrgIDs].reverse()) {
|
||||
|
|
@ -348,9 +358,11 @@ test.describe("Settings mobile optimization audit", () => {
|
|||
},
|
||||
},
|
||||
);
|
||||
const responseBody =
|
||||
response.status() === 204 ? "" : await response.text();
|
||||
expect(
|
||||
[204, 404].includes(response.status()),
|
||||
`cleanup organization ${orgID}`,
|
||||
`cleanup organization ${orgID}: ${response.status()} ${responseBody}`,
|
||||
).toBeTruthy();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import path from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
import { installVmwareWorkloadResourceRoute } from './vmware-workload-fixture';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCREENSHOT_PATH = '/tmp/vmware-ai-chat-read-recovery.png';
|
||||
|
|
@ -15,23 +16,26 @@ const test = base.extend<{}, WorkerFixtures>({
|
|||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`vmware-ai-chat-read-recovery-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`vmware-ai-chat-read-recovery-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
});
|
||||
|
||||
// The assistant transport moved from the retired /api/ai/chat SSE endpoint
|
||||
|
|
@ -57,6 +61,8 @@ test.describe('VMware AI chat read recovery', () => {
|
|||
await route.abort();
|
||||
});
|
||||
|
||||
await installVmwareWorkloadResourceRoute(page);
|
||||
|
||||
await page.goto('/vmware', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('[data-testid="vmware-page"]')).toBeVisible();
|
||||
|
||||
|
|
@ -67,7 +73,9 @@ test.describe('VMware AI chat read recovery', () => {
|
|||
// First websocket state frame can lag on a freshly booted backend.
|
||||
await textarea.focus();
|
||||
await textarea.fill('@warehouse');
|
||||
const mentionListbox = page.getByRole('listbox', { name: 'Assistant resources' });
|
||||
const mentionListbox = page.getByRole('listbox', {
|
||||
name: 'Assistant resources',
|
||||
});
|
||||
await expect(mentionListbox).toBeVisible({ timeout: 30_000 });
|
||||
const vmOption = mentionListbox
|
||||
.getByRole('option')
|
||||
|
|
@ -77,12 +85,15 @@ test.describe('VMware AI chat read recovery', () => {
|
|||
await page.keyboard.press('Enter');
|
||||
await expect(textarea).toHaveValue('@warehouse-api-01 ');
|
||||
|
||||
await textarea.fill('@warehouse-api-01 show me recent status');
|
||||
await textarea.pressSequentially('show me recent status');
|
||||
await expect(textarea).toHaveValue('@warehouse-api-01 show me recent status');
|
||||
// Submit through the composer control that phone users actually tap.
|
||||
// A page-global keyboard action is not a stable submission primitive in
|
||||
// mobile WebKit and can remain pending after the textarea has accepted the
|
||||
// completed prompt.
|
||||
await page.getByRole('button', { name: 'Send message' }).click();
|
||||
const sendButton = page.getByRole('button', { name: 'Send message' });
|
||||
await expect(sendButton).toBeEnabled();
|
||||
await sendButton.click();
|
||||
|
||||
// Chat turns ride the shared assistant transport (websocket-backed since
|
||||
// the assistant modernization, so REST route interception cannot observe
|
||||
|
|
@ -90,9 +101,9 @@ test.describe('VMware AI chat read recovery', () => {
|
|||
// 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('@warehouse-api-01 show me recent status').first(),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
await expect(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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
|
||||
const VMWARE_WORKLOAD_RESOURCE_URL =
|
||||
/\/api\/resources\?type=vm(?:,|%2C)system-container(?:,|%2C)app-container(?:,|%2C)pod&page=1&limit=200$/i;
|
||||
|
||||
const VMWARE_WORKLOAD_RESOURCE = {
|
||||
id: 'vmware:vc-mock-1:vm:vm-201',
|
||||
type: 'vm',
|
||||
|
|
@ -57,14 +54,23 @@ const VMWARE_WORKLOAD_RESOURCE = {
|
|||
} as const;
|
||||
|
||||
export async function installVmwareWorkloadResourceRoute(page: Page): Promise<void> {
|
||||
await page.route(VMWARE_WORKLOAD_RESOURCE_URL, async (route) => {
|
||||
await page.route('**/api/resources?*', async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const resourceTypes = (url.searchParams.get('type') ?? '').split(',');
|
||||
if (!resourceTypes.includes('vm')) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
const pageNumber = Number(url.searchParams.get('page') ?? '1');
|
||||
const limit = Number(url.searchParams.get('limit') ?? '100');
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
data: [VMWARE_WORKLOAD_RESOURCE],
|
||||
data: pageNumber === 1 ? [VMWARE_WORKLOAD_RESOURCE] : [],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
page: pageNumber,
|
||||
limit,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue