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:
rcourtman 2026-07-20 07:50:22 +01:00
parent 6eb559905d
commit c73e78aeea
16 changed files with 513 additions and 169 deletions

View file

@ -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();
}
}

View file

@ -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,
});

View file

@ -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,
},