Restore Diagnostics integration repair for exact acceptance

Compose the explicitly retained three-commit test candidate additively with current guest safety work. Production safeguards and unrelated integration type failures remain unchanged; final browser and exact-source acceptance follow.

Contract-Neutral: Test-only candidate restoration; no runtime or subsystem contract delta.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-04 15:37:21 +01:00
3 changed files with 672 additions and 176 deletions
@@ -0,0 +1,201 @@
// Run the actual integration spec and full app shell offline. Only unrelated
// bootstrap/auth/state APIs are synthetic; the spec owns diagnostics responses.
// This is frontend integration proof, not backend authentication or appliance proof.
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const http = require('node:http');
const { spawn } = require('node:child_process');
const { chromium, webkit } = require('playwright');
const { WebSocketServer } = require('/workspace/tests/integration/node_modules/ws');
const root = '/workspace';
const out = path.join(root, 'tmp/diagnostics-integration-proof');
const origin = 'http://127.0.0.1:5393';
const backendOrigin = 'http://127.0.0.1:5394';
const state = {
resources: [],
connectedInfrastructure: [],
activeAlerts: [],
recentlyResolved: [],
lastUpdate: Date.now(),
};
const bootstrap = {
'/api/health': { status: 'healthy' },
'/api/state': state,
'/api/security/status': {
hasAuthentication: true,
requiresAuth: false,
settingsCapabilities: {
infrastructureRead: true,
systemSettingsRead: true,
apiAccessRead: true,
apiAccessWrite: true,
authenticationRead: true,
authenticationWrite: true,
},
sessionCapabilities: { isAdmin: true },
presentationPolicy: {
hideCommercial: true,
hideUpgrade: true,
demoMode: false,
readOnly: false,
},
},
'/api/config': { nodes: [], pbs: [], pmg: [], dockerHosts: [], theme: 'light' },
'/api/version': { version: '6.4.5' },
'/api/updates/check': { currentVersion: '6.4.5', latestVersion: '6.4.5', updateAvailable: false },
'/api/license/status': { valid: false, tier: 'free', features: [] },
'/api/ai/settings': { enabled: false },
'/api/alerts/active': [],
};
(async () => {
fs.mkdirSync(out, { recursive: true });
const version = require('playwright/package.json').version;
const lock = JSON.parse(fs.readFileSync(path.join(root, 'tests/integration/package-lock.json')));
if (version !== lock.packages['node_modules/@playwright/test'].version)
throw new Error('Playwright version mismatch');
const paths = [
'tests/integration/tests/69-diagnostics-onboarding.spec.ts',
'tests/integration/playwright.config.ts',
'frontend-modern/src/App.tsx',
'frontend-modern/src/components/Settings/DiagnosticsPanel.tsx',
'frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx',
'frontend-modern/src/components/Settings/diagnosticsModel.ts',
'frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts',
'frontend-modern/src/utils/diagnosticsPresentation.ts',
'frontend-modern/package-lock.json',
'tests/integration/package-lock.json',
'frontend-modern/browser-tests/diagnostics-integration.cjs',
];
const source = Object.fromEntries(
paths.map((p) => [
p,
crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(root, p)))
.digest('hex'),
]),
);
const requests = [];
const backend = http.createServer((req, res) => {
const pathname = new URL(req.url, backendOrigin).pathname;
requests.push({ method: req.method, path: pathname });
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(bootstrap[pathname] ?? {}));
});
const sockets = new WebSocketServer({ server: backend });
sockets.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'initialState', data: state }));
socket.on('message', () => socket.send(JSON.stringify({ type: 'pong' })));
});
process.env.PULSE_DEV_API_URL = backendOrigin;
process.env.PULSE_DEV_WS_URL = backendOrigin.replace('http:', 'ws:');
const frontend = path.join(root, 'frontend-modern');
const { createServer } = await import(
path.join(frontend, 'node_modules/vite/dist/node/index.js')
);
const server = await createServer({
root: frontend,
configFile: path.join(frontend, 'vite.config.ts'),
cacheDir: path.join(out, 'vite-cache'),
server: { host: '127.0.0.1', port: 5393, strictPort: true },
});
// A cookie-backed synthetic bootstrap fixture avoids password/auth mutation.
const cookieState = path.join(out, 'synthetic-session.json');
fs.writeFileSync(
cookieState,
JSON.stringify({
cookies: [
{
name: 'pulse_session',
value: 'synthetic-offline-only',
domain: '127.0.0.1',
path: '/',
expires: -1,
httpOnly: true,
secure: false,
sameSite: 'Lax',
},
],
origins: [],
}),
);
const config = path.join(out, 'playwright.config.cjs');
fs.writeFileSync(
config,
`
const { devices } = require('/workspace/tests/integration/node_modules/@playwright/test');
module.exports = {
testDir: '/workspace/tests/integration/tests', testMatch: '69-diagnostics-onboarding.spec.ts',
workers: 1, retries: 0, timeout: 180000, expect: { timeout: 20000 },
outputDir: '${out}/results', reporter: [['list'], ['json', { outputFile: '${out}/result.json' }]],
use: { baseURL: '${origin}', trace: 'off', screenshot: 'only-on-failure', acceptDownloads: true },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'], launchOptions: { executablePath: ${JSON.stringify(chromium.executablePath())}, args: ['--no-sandbox'] } } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'], launchOptions: { executablePath: ${JSON.stringify(chromium.executablePath())}, args: ['--no-sandbox'] } } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'], launchOptions: { executablePath: ${JSON.stringify(webkit.executablePath())} } } },
],
};
`,
);
let result, cleanup;
try {
await new Promise((resolve) => backend.listen(5394, '127.0.0.1', resolve));
await server.listen();
result = await new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
path.join(root, 'tests/integration/node_modules/@playwright/test/cli.js'),
'test',
'--config',
config,
],
{
cwd: path.join(root, 'tests/integration'),
env: {
...process.env,
PLAYWRIGHT_BASE_URL: origin,
PULSE_E2E_COOKIE_STATE_PATH: cookieState,
PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY: 'false',
PULSE_E2E_RUNTIME_STATE_PATH: path.join(out, 'unused-runtime.json'),
},
stdio: 'inherit',
},
);
child.on('error', reject);
child.on('exit', (exit, signal) => resolve({ exit, signal }));
});
} finally {
for (const socket of sockets.clients) socket.terminate();
await new Promise((resolve) => sockets.close(resolve));
await server.close();
await new Promise((resolve) => backend.close(resolve));
cleanup = {
frontendClosed: true,
socketsClosed: true,
backendClosed: true,
runnerTerminal: Boolean(result),
};
fs.writeFileSync(
path.join(out, 'receipt.json'),
JSON.stringify(
{
source,
playwright: version,
result,
cleanup,
boundary:
'Actual full app shell and exact integration spec; synthetic offline session/bootstrap/state, no native API/guest-agent requests or production authentication.',
requests,
},
null,
2,
) + '\n',
);
}
if (result.exit !== 0 || result.signal) throw new Error('Diagnostics integration failed');
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@@ -1,8 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test as base, type Page } from '@playwright/test';
import { createAuthenticatedStorageState } from './helpers';
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test as base, type Page } from "@playwright/test";
import { createAuthenticatedStorageState } from "./helpers";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -10,6 +10,11 @@ type WorkerFixtures = {
authStorageStatePath: string;
};
type DiagnosticsTraffic = {
reads: number;
writes: string[];
};
type OverflowAudit = {
viewportWidth: number;
pageWidth: number;
@@ -18,22 +23,22 @@ type OverflowAudit = {
};
const DIAGNOSTICS_PAYLOAD = {
version: '6.0.0',
runtime: 'go',
version: "6.0.0",
runtime: "go",
uptime: 3600,
nodes: [],
pbs: [],
system: {
os: 'linux',
arch: 'amd64',
goVersion: 'go1.25',
os: "linux",
arch: "amd64",
goVersion: "go1.25",
numCPU: 8,
numGoroutine: 32,
memoryMB: 128,
},
metricsStore: {
enabled: true,
status: 'healthy',
status: "healthy",
dbSize: 4 * 1024 * 1024,
rawCount: 12,
minuteCount: 24,
@@ -45,7 +50,7 @@ const DIAGNOSTICS_PAYLOAD = {
},
commercialFunnel: {
enabled: true,
status: 'active',
status: "active",
windowDays: 30,
summary: {
pricing_viewed: 3,
@@ -58,13 +63,13 @@ const DIAGNOSTICS_PAYLOAD = {
license_activated: 1,
license_activation_failed: 0,
period: {
from: '2026-03-19T00:00:00Z',
to: '2026-04-18T00:00:00Z',
from: "2026-03-19T00:00:00Z",
to: "2026-04-18T00:00:00Z",
},
},
daily: [
{
day: '2026-04-17',
day: "2026-04-17",
pricing_viewed: 1,
paywall_viewed: 0,
trial_started: 0,
@@ -78,7 +83,7 @@ const DIAGNOSTICS_PAYLOAD = {
],
surfaces: [
{
key: 'settings_self_hosted_billing_compare_prompt',
key: "settings_self_hosted_billing_compare_prompt",
pricing_viewed: 0,
paywall_viewed: 0,
trial_started: 0,
@@ -92,7 +97,7 @@ const DIAGNOSTICS_PAYLOAD = {
],
capabilities: [
{
key: 'self_hosted_plan',
key: "self_hosted_plan",
pricing_viewed: 3,
paywall_viewed: 0,
trial_started: 0,
@@ -104,11 +109,13 @@ const DIAGNOSTICS_PAYLOAD = {
license_activation_failed: 0,
},
],
notes: ['Local pricing and activation events show at least one completed conversion.'],
notes: [
"Local pricing and activation events show at least one completed conversion.",
],
},
infrastructureOnboarding: {
enabled: true,
status: 'warning',
status: "warning",
windowDays: 30,
summary: {
opened: 4,
@@ -120,13 +127,13 @@ const DIAGNOSTICS_PAYLOAD = {
catalog_selected: 2,
credentials_opened: 1,
period: {
from: '2026-03-19T00:00:00Z',
to: '2026-04-18T00:00:00Z',
from: "2026-03-19T00:00:00Z",
to: "2026-04-18T00:00:00Z",
},
},
daily: [
{
day: '2026-04-17',
day: "2026-04-17",
opened: 2,
api_path_selected: 1,
agent_path_selected: 1,
@@ -137,7 +144,7 @@ const DIAGNOSTICS_PAYLOAD = {
credentials_opened: 0,
},
{
day: '2026-04-18',
day: "2026-04-18",
opened: 2,
api_path_selected: 1,
agent_path_selected: 0,
@@ -149,18 +156,18 @@ const DIAGNOSTICS_PAYLOAD = {
},
],
paths: [
{ key: 'api', count: 2 },
{ key: 'agent', count: 1 },
{ key: "api", count: 2 },
{ key: "agent", count: 1 },
],
platforms: [
{ key: 'truenas', catalog_selected: 2, credentials_opened: 1 },
platforms: [{ key: "truenas", catalog_selected: 2, credentials_opened: 1 }],
notes: [
"More probed addresses miss than detect a supported API-backed platform.",
],
notes: ['More probed addresses miss than detect a supported API-backed platform.'],
},
discovery: {
enabled: true,
configuredSubnet: 'auto',
scanInterval: '5m',
configuredSubnet: "auto",
scanInterval: "5m",
lastResultServers: 3,
},
apiTokens: {
@@ -202,79 +209,168 @@ const EXPORT_DIAGNOSTICS_PAYLOAD = {
...DIAGNOSTICS_PAYLOAD,
nodes: [
{
id: 'node-raw-id',
name: 'pve-01',
host: '10.0.0.5',
type: 'pve',
authMethod: 'token',
id: "node-raw-id",
name: "pve-01",
host: "10.0.0.5",
type: "pve",
authMethod: "token",
connected: false,
error: 'dial tcp 10.0.0.5:8006: connect: connection refused',
error: "dial tcp 10.0.0.5:8006: connect: connection refused",
},
],
discovery: {
enabled: true,
configuredSubnet: '10.0.0.0/24',
activeSubnet: '10.0.1.0/24',
environmentOverride: 'PULSE_DISCOVERY_SUBNET=10.0.2.0/24',
subnetAllowlist: ['10.0.0.0/24'],
subnetBlocklist: ['10.0.3.0/24'],
configuredSubnet: "10.0.0.0/24",
activeSubnet: "10.0.1.0/24",
environmentOverride: "PULSE_DISCOVERY_SUBNET=10.0.2.0/24",
subnetAllowlist: ["10.0.0.0/24"],
subnetBlocklist: ["10.0.3.0/24"],
history: [
{
startedAt: '2026-04-17T10:00:00Z',
completedAt: '2026-04-17T10:00:05Z',
duration: '5s',
startedAt: "2026-04-17T10:00:00Z",
completedAt: "2026-04-17T10:00:05Z",
duration: "5s",
durationMs: 5000,
subnet: '10.0.0.0/24',
subnet: "10.0.0.0/24",
serverCount: 3,
errorCount: 1,
blocklistLength: 1,
status: 'completed',
status: "completed",
},
],
},
errors: ['probe failed for 10.0.0.10 after timeout'],
apiTokens: {
...DIAGNOSTICS_PAYLOAD.apiTokens,
tokens: [
{
id: "synthetic-private-token-a",
name: "synthetic-private-purpose-a",
hint: "synthetic-private-hint-a",
},
{
id: "synthetic-private-token-b",
name: "synthetic-private-purpose-b",
hint: "synthetic-private-hint-b",
},
],
// Deliberately reverse token order: aliases must follow identity, not position.
usage: [
{
tokenId: "synthetic-private-token-b",
agentCount: 1,
agents: ["synthetic-private-agent"],
},
{
tokenId: "synthetic-private-token-a",
agentCount: 1,
agents: ["synthetic-private-agent"],
},
],
},
dockerAgents: {
...DIAGNOSTICS_PAYLOAD.dockerAgents,
attention: [
{
agentId: "synthetic-private-agent-id",
name: "synthetic-private-agent",
tokenHint: "synthetic-private-hint",
issues: [],
},
],
},
nodeSnapshots: [
{
instance: "synthetic-private-instance",
node: "synthetic-private-node",
memorySource: "agent",
memory: { used: 256 },
raw: { total: 512 },
},
],
guestSnapshots: [
{
instance: "synthetic-private-instance",
node: "synthetic-private-node",
name: "synthetic-private-guest",
vmid: 9501,
guestType: "vm",
memorySource: "agent",
memory: { used: 128 },
raw: { hostAgentUsed: 128 },
notes: [],
},
],
memorySources: [
{ instance: "synthetic-private-instance", scope: "node", count: 1 },
],
memorySourceBreakdown: [
{
instance: "synthetic-private-instance",
scope: "node",
source: "agent",
trust: "host",
count: 1,
fallback: false,
fallbackReasons: [],
},
],
errors: ["probe failed for 10.0.0.10 after timeout"],
};
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',
`diagnostics-onboarding-${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",
`diagnostics-onboarding-${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" },
],
});
async function prepareDiagnosticsRoute(
page: Page,
payload = DIAGNOSTICS_PAYLOAD,
): Promise<void> {
await page.route('**/api/diagnostics', async (route) => {
payload: typeof DIAGNOSTICS_PAYLOAD | typeof EXPORT_DIAGNOSTICS_PAYLOAD =
DIAGNOSTICS_PAYLOAD,
): Promise<DiagnosticsTraffic> {
const traffic: DiagnosticsTraffic = { reads: 0, writes: [] };
page.on("request", (request) => {
if (!["GET", "HEAD"].includes(request.method()))
traffic.writes.push(new URL(request.url()).pathname);
});
await page.route("**/api/diagnostics", async (route) => {
const requestUrl = new URL(route.request().url());
if (route.request().method() !== 'GET' || requestUrl.pathname !== '/api/diagnostics') {
if (
route.request().method() !== "GET" ||
requestUrl.pathname !== "/api/diagnostics"
) {
await route.continue();
return;
}
traffic.reads += 1;
await route.fulfill({
status: 200,
contentType: 'application/json',
contentType: "application/json",
body: JSON.stringify(payload),
});
});
return traffic;
}
async function scrollToBottom(page: Page): Promise<void> {
@@ -299,7 +395,10 @@ async function scrollToBottom(page: Page): Promise<void> {
async function auditHorizontalOverflow(page: Page): Promise<OverflowAudit> {
return page.evaluate(() => {
const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const viewportWidth = Math.max(
document.documentElement.clientWidth,
window.innerWidth || 0,
);
const pageWidth = Math.max(
document.body.scrollWidth,
document.documentElement.scrollWidth,
@@ -307,21 +406,27 @@ async function auditHorizontalOverflow(page: Page): Promise<OverflowAudit> {
document.documentElement.offsetWidth,
);
const offenders = Array.from(document.querySelectorAll('body *'))
const offenders = Array.from(document.querySelectorAll("body *"))
.map((el) => {
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return null;
const style = window.getComputedStyle(el);
if (style.position === 'fixed' || style.position === 'absolute') return null;
if (style.position === "fixed" || style.position === "absolute")
return null;
const overflow = rect.right - viewportWidth;
if (overflow <= 1) return null;
return {
tag: el.tagName.toLowerCase(),
className: (el.getAttribute('class') || '').trim().slice(0, 120),
className: (el.getAttribute("class") || "").trim().slice(0, 120),
overflow: Number(overflow.toFixed(1)),
};
})
.filter((entry): entry is { tag: string; className: string; overflow: number } => Boolean(entry))
.filter(
(
entry,
): entry is { tag: string; className: string; overflow: number } =>
Boolean(entry),
)
.slice(0, 8);
return {
@@ -333,130 +438,320 @@ async function auditHorizontalOverflow(page: Page): Promise<OverflowAudit> {
});
}
test.describe('Diagnostics onboarding analytics', () => {
async function openDiagnostics(page: Page): Promise<void> {
await page.goto("/settings/support/diagnostics", {
waitUntil: "domcontentloaded",
});
await page.waitForURL(/\/settings\/support\/diagnostics$/, {
timeout: 15_000,
});
await expect(
page.getByRole("heading", { name: "Diagnostics & Health" }),
).toBeVisible();
}
async function describedBy(
button: ReturnType<Page["getByRole"]>,
): Promise<string> {
return button.evaluate((element) =>
(element.getAttribute("aria-describedby") || "")
.split(/\s+/)
.map((id) => document.getElementById(id)?.textContent || "")
.join(" "),
);
}
async function downloadJSON(
page: Page,
label: string,
kind: string,
keyboard = false,
) {
const button = page.getByRole("button", { name: label, exact: true });
await expect(button).toBeEnabled();
// Register the observer before activation. Keyboard and pointer use the same
// public accessible control, on desktop and both mobile projects.
const pending = page.waitForEvent("download");
if (keyboard) {
await button.focus();
await expect(button).toBeFocused();
await button.press("Enter");
} else {
await button.click();
}
const download = await pending;
expect(download.suggestedFilename()).toMatch(
new RegExp(`^pulse-diagnostics-${kind}-\\d{4}-\\d{2}-\\d{2}\\.json$`),
);
const file = await download.path();
expect(file).not.toBeNull();
return JSON.parse(fs.readFileSync(file!, "utf8"));
}
async function assertControlsInViewport(
page: Page,
mobile: boolean,
): Promise<void> {
const audit = await auditHorizontalOverflow(page);
expect(
audit.pageWidth,
`Diagnostics overflow: ${JSON.stringify(audit)}`,
).toBeLessThanOrEqual(audit.viewportWidth + 1);
for (const button of await page
.getByRole("button", {
name: /Run Diagnostics|Full \(private\)|GitHub \(review first\)/,
})
.all()) {
const bounds = await button.boundingBox();
expect(bounds).not.toBeNull();
expect(bounds!.x).toBeGreaterThanOrEqual(0);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(
audit.viewportWidth + 1,
);
if (mobile) expect(bounds!.height).toBeGreaterThanOrEqual(44);
}
}
test.describe("Diagnostics safe collection and local exports", () => {
test.setTimeout(180_000);
// The Commercial Funnel and Infrastructure Onboarding analytics cards were
// deliberately removed from the user-facing diagnostics page (c6bcad255,
// "Remove internal analytics from diagnostics"); the UI strips
// commercialFunnel and infrastructureOnboarding from the payload before both
// rendering and export. These specs pin that boundary.
test('keeps internal onboarding analytics out of the rendered diagnostics page', async ({
test("keeps internal analytics absent and connects safety guidance to keyboard-accessible controls", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only diagnostics shell coverage');
await prepareDiagnosticsRoute(page);
await page.goto('/settings/support/diagnostics', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/settings\/support\/diagnostics$/, { timeout: 15_000 });
await expect(page.getByRole('heading', { name: 'Diagnostics & Health' })).toBeVisible();
await page.getByRole('button', { name: 'Run Diagnostics', exact: true }).first().click();
// The run completed and the payload rendered (metrics-store card proves it).
await expect(page.getByText('Metrics Store', { exact: false }).first()).toBeVisible({
timeout: 30_000,
const traffic = await prepareDiagnosticsRoute(page);
await openDiagnostics(page);
expect(traffic.reads).toBe(0);
await expect(
page.getByRole("button", { name: "Full (private)", exact: true }),
).toHaveCount(0);
// Both the toolbar and empty-state run actions need the same warning.
const actions = page.getByRole("button", {
name: "Run Diagnostics",
exact: true,
});
await expect(page.getByText('Commercial Funnel', { exact: true })).toHaveCount(0);
await expect(page.getByText('Infrastructure Onboarding', { exact: true })).toHaveCount(0);
await expect(page.getByText('Credentials Opened', { exact: true })).toHaveCount(0);
await expect(actions).toHaveCount(2);
for (const button of await actions.all()) {
expect(await describedBy(button)).toMatch(
/Do not run it during a backup, freeze\/thaw/,
);
}
await expect(
page.getByRole("link", { name: "Safe diagnostics and sharing" }),
).toHaveAttribute("href", /#collect-diagnostics-safely$/);
await actions.first().focus();
await expect(actions.first()).toBeFocused();
await actions.first().press("Enter");
await expect(
page.getByText("Metrics Store", { exact: false }).first(),
).toBeVisible();
expect(traffic.reads).toBe(1);
for (const label of [
"Commercial Funnel",
"Infrastructure Onboarding",
"Credentials Opened",
]) {
await expect(page.getByText(label, { exact: true })).toHaveCount(0);
}
for (const label of ["Full (private)", "GitHub (review first)"]) {
expect(
await describedBy(
page.getByRole("button", { name: label, exact: true }),
),
).toMatch(
/Nothing is uploaded.*Keep the full file private.*Review even a sanitised file/,
);
}
await assertControlsInViewport(
page,
testInfo.project.name.startsWith("mobile-"),
);
});
test('desktop diagnostics exports full and sanitized onboarding analytics JSON', async ({
test("downloads the displayed private and review-first JSON without another live request or upload", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only diagnostics export coverage');
const traffic = await prepareDiagnosticsRoute(
page,
EXPORT_DIAGNOSTICS_PAYLOAD,
);
await openDiagnostics(page);
await page
.getByRole("button", { name: "Run Diagnostics", exact: true })
.first()
.click();
await expect(
page.getByText("Metrics Store", { exact: false }).first(),
).toBeVisible();
const writesBeforeExport = [...traffic.writes];
await prepareDiagnosticsRoute(page, EXPORT_DIAGNOSTICS_PAYLOAD);
const full = await downloadJSON(page, "Full (private)", "full", true);
const {
infrastructureOnboarding: _onboarding,
commercialFunnel: _funnel,
...expectedFull
} = EXPORT_DIAGNOSTICS_PAYLOAD;
expect(full).toEqual(expectedFull);
expect(traffic.reads).toBe(1);
expect(traffic.writes).toEqual(writesBeforeExport);
await expect(
page.getByRole("heading", {
name: "Full diagnostics downloaded — keep this file private",
exact: true,
}),
).toBeVisible();
await page.goto('/settings/support/diagnostics', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/settings\/support\/diagnostics$/, { timeout: 15_000 });
await page.getByRole('button', { name: 'Run Diagnostics', exact: true }).first().click();
// Wait for a rendered card that survives the analytics strip.
await expect(page.getByText('Metrics Store', { exact: false }).first()).toBeVisible({
timeout: 30_000,
const sanitised = await downloadJSON(
page,
"GitHub (review first)",
"sanitized",
);
expect(sanitised.nodes[0]).toMatchObject({
id: "node-1",
name: "node-1",
host: "node-1",
});
const [fullDownload] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Full', exact: true }).click(),
expect(sanitised.discovery).toMatchObject({
configuredSubnet: "[REDACTED_SUBNET]",
activeSubnet: "[REDACTED_SUBNET]",
environmentOverride: "[REDACTED]",
subnetAllowlist: ["[REDACTED_SUBNET]"],
subnetBlocklist: ["[REDACTED_SUBNET]"],
history: [
expect.objectContaining({
subnet: "[REDACTED_SUBNET]",
serverCount: 3,
}),
],
});
expect(sanitised.errors).toEqual([
"probe failed for [REDACTED_IP] after timeout",
]);
expect(fullDownload.suggestedFilename()).toMatch(
new RegExp(`^pulse-diagnostics-full-${new Date().toISOString().slice(0, 10)}\\.json$`),
expect(sanitised.apiTokens.tokens[0]).toMatchObject({
id: "token-1",
name: "token-1",
hint: "[REDACTED]",
});
expect(sanitised.apiTokens.usage[0].tokenId).toBe(
sanitised.apiTokens.tokens[1].id,
);
const fullPath = await fullDownload.path();
expect(fullPath).not.toBeNull();
const fullPayload = JSON.parse(fs.readFileSync(fullPath!, 'utf8')) as typeof EXPORT_DIAGNOSTICS_PAYLOAD;
expect(fullPayload.nodes[0]?.host).toBe('10.0.0.5');
expect(fullPayload.discovery?.configuredSubnet).toBe('10.0.0.0/24');
// Internal analytics are stripped from the full export too, not just the
// rendered card.
expect(fullPayload.infrastructureOnboarding).toBeUndefined();
expect(
(fullPayload as Record<string, unknown>).commercialFunnel,
).toBeUndefined();
const [sanitizedDownload] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'GitHub', exact: true }).click(),
]);
expect(sanitizedDownload.suggestedFilename()).toMatch(
new RegExp(`^pulse-diagnostics-sanitized-${new Date().toISOString().slice(0, 10)}\\.json$`),
expect(sanitised.apiTokens.usage[1].tokenId).toBe(
sanitised.apiTokens.tokens[0].id,
);
const sanitizedPath = await sanitizedDownload.path();
expect(sanitizedPath).not.toBeNull();
const sanitizedPayload = JSON.parse(fs.readFileSync(sanitizedPath!, 'utf8')) as typeof EXPORT_DIAGNOSTICS_PAYLOAD;
expect(sanitizedPayload.nodes[0]).toEqual(
expect.objectContaining({
id: 'node-1',
name: 'node-1',
host: 'node-1',
expect(sanitised.apiTokens.usage[0].agents).toEqual(
sanitised.apiTokens.usage[1].agents,
);
expect(sanitised.apiTokens.usage[0].agents[0]).toBe(
sanitised.dockerAgents.attention[0].name,
);
expect(sanitised.guestSnapshots[0]).toMatchObject({
instance: sanitised.nodeSnapshots[0].instance,
node: sanitised.nodeSnapshots[0].node,
});
expect(sanitised.guestSnapshots[0].vmid).toBeUndefined();
expect(sanitised.guestSnapshots[0].memory).toEqual(
EXPORT_DIAGNOSTICS_PAYLOAD.guestSnapshots[0].memory,
);
expect(sanitised.memorySources[0].instance).toBe(
sanitised.nodeSnapshots[0].instance,
);
expect(sanitised.memorySourceBreakdown[0].instance).toBe(
sanitised.nodeSnapshots[0].instance,
);
for (const payload of [full, sanitised]) {
expect(payload.infrastructureOnboarding).toBeUndefined();
expect(payload.commercialFunnel).toBeUndefined();
expect(payload.metricsStore).toEqual(
EXPORT_DIAGNOSTICS_PAYLOAD.metricsStore,
);
}
expect(JSON.stringify(sanitised)).not.toContain("synthetic-private-");
expect(JSON.stringify(sanitised)).not.toMatch(/10\.0\.[0-3]\./);
expect(traffic.reads).toBe(1);
expect(traffic.writes).toEqual(writesBeforeExport);
await expect(
page.getByRole("heading", {
name: "Sanitised diagnostics downloaded — review before sharing",
exact: true,
}),
).toBeVisible();
// A full export after sanitisation must still be the original displayed
// data: redaction may not mutate it.
expect(await downloadJSON(page, "Full (private)", "full")).toEqual(
expectedFull,
);
expect(sanitizedPayload.discovery).toEqual(
expect.objectContaining({
configuredSubnet: '[REDACTED_SUBNET]',
activeSubnet: '[REDACTED_SUBNET]',
environmentOverride: '[REDACTED]',
}),
expect(traffic.reads).toBe(1);
expect(traffic.writes).toEqual(writesBeforeExport);
await assertControlsInViewport(
page,
testInfo.project.name.startsWith("mobile-"),
);
expect(sanitizedPayload.errors).toEqual(['probe failed for [REDACTED_IP] after timeout']);
expect(sanitizedPayload.infrastructureOnboarding).toBeUndefined();
expect(
(sanitizedPayload as Record<string, unknown>).commercialFunnel,
).toBeUndefined();
await page.screenshot({
path: testInfo.outputPath("diagnostics-safe-export.png"),
fullPage: true,
});
});
test('mobile diagnostics keeps the populated onboarding analytics page inside the viewport', async ({
test("disables exports during a refresh and retains the previous result after a failed live check", async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.startsWith('mobile-'), 'Mobile-only diagnostics overflow coverage');
await prepareDiagnosticsRoute(page);
await page.goto('/settings/support/diagnostics', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/settings\/support\/diagnostics$/, { timeout: 15_000 });
await page.getByRole('button', { name: 'Run', exact: true }).click();
await expect(page.getByText('Metrics Store', { exact: false }).first()).toBeVisible({
timeout: 30_000,
let reads = 0;
let release: (() => void) | undefined;
const held = new Promise<void>((resolve) => {
release = resolve;
});
await page.route("**/api/diagnostics", async (route) => {
reads += 1;
if (reads === 1) return route.fulfill({ json: DIAGNOSTICS_PAYLOAD });
await held;
return route.fulfill({
status: 400,
json: { error: "Synthetic live check failed" },
});
});
await openDiagnostics(page);
const run = page
.getByRole("button", { name: "Run Diagnostics", exact: true })
.first();
await run.click();
const full = page.getByRole("button", {
name: "Full (private)",
exact: true,
});
const github = page.getByRole("button", {
name: "GitHub (review first)",
exact: true,
});
await expect(full).toBeEnabled();
await run.focus();
await run.press("Space");
await expect(
page.getByRole("button", { name: "Running...", exact: true }),
).toBeDisabled();
await expect(full).toBeDisabled();
await expect(github).toBeDisabled();
expect(reads).toBe(2);
release!();
await expect(
page.getByRole("heading", {
name: "Synthetic live check failed",
exact: true,
}),
).toBeVisible();
await expect(full).toBeEnabled();
await expect(github).toBeEnabled();
const {
infrastructureOnboarding: _onboarding,
commercialFunnel: _funnel,
...expectedFull
} = DIAGNOSTICS_PAYLOAD;
expect(await downloadJSON(page, "Full (private)", "full")).toEqual(
expectedFull,
);
expect(reads).toBe(2);
await scrollToBottom(page);
const audit = await auditHorizontalOverflow(page);
expect(
audit.pageWidth,
`Mobile diagnostics overflow (viewport=${audit.viewportWidth}, page=${audit.pageWidth}, offenders=${JSON.stringify(audit.offenders)})`,
).toBeLessThanOrEqual(audit.viewportWidth + 1);
await assertControlsInViewport(
page,
testInfo.project.name.startsWith("mobile-"),
);
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM"],
"moduleResolution": "node",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,