Integrate reviewed maintenance verification evidence presentation

Preserve exact Web candidate 067d845244 and its parent-bound browser proof. Existing reports now expose recorded check age and truthful loading, unavailable, denied and empty states; no action or public contract change.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-11 05:46:36 +01:00
6 changed files with 985 additions and 23 deletions
@@ -0,0 +1,416 @@
// Read-only browser acceptance of the changed report presentation/query flow.
// Explicit review/rerun business calls are covered by the runtime component
// suite. Every POST is refused here, so a clock-induced operation fails proof.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const crypto = require('node:crypto');
const { chromium, webkit } = require('playwright');
const root = '/workspace/frontend-modern';
const output = '/workspace/tmp/maintenance-evidence/browser-read-only';
const origin = 'http://127.0.0.1:5347';
const start = Date.parse('2026-10-11T04:00:00Z');
const iso = (offset) => new Date(start + offset).toISOString();
const reports = ['healthy', 'pending', 'needs_review', 'failed_verification'].map((status) => ({
id: 'report:' + status,
resourceId: 'pbs-a',
status,
trigger: 'maintenance_window_ended',
startedAt: iso(-30_000),
completedAt: status === 'pending' ? '0001-01-01T00:00:00Z' : iso(-30_000),
windowStartedAt: iso(-90_000_000),
windowEndedAt: iso(-86_400_000),
evidence: {
activeCriticalAlerts: status === 'failed_verification' ? 1 : 0,
activeWarningAlerts: 2,
activeCriticalFindings: 0,
activeWarningFindings: 1,
failedActionsSinceWindowStart: 0,
metricRecovery: { samplesAfterEnd: 3, trend: 'improving' },
},
linkedFindingIds: [],
linkedAlertIds: [],
linkedActionIds: [],
recommendation: 'Recorded evidence, not a fresh health observation.',
...(status === 'healthy'
? {
userOutcome: 'reviewed',
reviewedAt: iso(-10_000),
reviewedBy: 'fixture-admin',
reviewNote: 'Earlier check retained. A completed backup does not prove guest recovery.',
}
: {}),
}));
async function bounded(promise, label) {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error('Fixture observation deadline: ' + label)),
10_000,
);
}),
]);
} finally {
clearTimeout(timer);
}
}
(async () => {
fs.mkdirSync(output, { recursive: true });
const binding = JSON.parse(
fs.readFileSync('/workspace/tmp/maintenance-evidence/source-binding.json'),
);
for (const [file, digest] of Object.entries(binding.content_sha256)) {
assert.equal(
crypto
.createHash('sha256')
.update(fs.readFileSync('/workspace/' + file))
.digest('hex'),
digest,
file,
);
}
const playwright = require('playwright/package.json').version;
assert.equal(
playwright,
JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
'node_modules/@playwright/test'
].version,
);
const result = {
source: binding,
playwright,
cases: [],
captures: [],
completed: false,
browsersClosed: false,
serverClosed: false,
};
const { createServer } = await import(root + '/node_modules/vite/dist/node/index.js');
const server = await createServer({
root,
configFile: root + '/vite.config.ts',
cacheDir: output + '/vite-cache',
server: { host: '127.0.0.1', port: 5347, strictPort: true, watch: null },
});
let browser, context;
try {
await server.listen();
for (const [name, engine, width, height, dark] of [
['desktop', chromium, 1365, 900, false],
['phone', webkit, 390, 844, true],
]) {
browser = await engine.launch(
engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
context = await browser.newContext({
viewport: { width, height },
isMobile: dark,
hasTouch: dark,
locale: 'en-GB',
timezoneId: 'UTC',
});
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultNavigationTimeout(45_000);
await page.clock.install({ time: new Date(start - 1000) });
await page.clock.pauseAt(new Date(start));
await page.addInitScript((dark) => {
localStorage.clear();
sessionStorage.clear();
document.addEventListener(
'DOMContentLoaded',
() => document.documentElement.classList.toggle('dark', dark),
{ once: true },
);
}, dark);
const record = {
name,
browserVersion: browser.version(),
viewport: { width, height },
dark,
checks: [],
reads: [],
writes: [],
errors: [],
unexpected: [],
offOrigin: [],
completed: false,
contextClosed: false,
};
result.cases.push(record);
let mode = 'pending',
releaseRead,
observeRead;
const readStarted = new Promise((resolve) => {
observeRead = resolve;
});
page.on('pageerror', (error) => record.errors.push(error.stack || error.message));
await page.route('**/*', async (route) => {
const req = route.request(),
url = new URL(req.url());
if (url.origin !== origin) {
record.offOrigin.push(url.origin);
return route.abort();
}
if (!url.pathname.startsWith('/api/')) return route.continue();
if (req.method() !== 'GET') {
record.writes.push(url.pathname);
return route.fulfill({
status: 403,
json: { error: 'Read-only fixture refuses operations' },
});
}
record.reads.push(url.pathname);
let data;
if (url.pathname.endsWith('/maintenance-verifications')) {
if (url.pathname.includes('/pending-resource/')) {
if (mode === 'pending') {
await bounded(
new Promise((resolve) => {
releaseRead = resolve;
observeRead();
}),
'held new-resource read',
);
return route.fulfill({
status: 503,
json: { error: 'Synthetic unavailable reader' },
});
}
if (mode === 'denied')
return route.fulfill({ status: 403, json: { error: 'Synthetic access denial' } });
return route.fulfill({
json: { data: [], meta: { resourceId: 'pending-resource', limit: 25, total: 0 } },
});
}
data = { data: reports, meta: { resourceId: 'pbs-a', limit: 25, total: reports.length } };
} else if (url.pathname.endsWith('/operator-state'))
data = {
configured: true,
state: {
canonicalId: 'pbs-a',
neverAutoRemediate: true,
monitoringMode: 'expected_offline',
lifecycleState: 'retired',
note: 'Keep the saved protection',
setAt: iso(-60_000),
},
};
else if (url.pathname.endsWith('/facets'))
data = { capabilities: [], relationships: [], recentChanges: [], counts: {} };
else if (url.pathname === '/api/ai/intelligence')
data = { resourceId: 'pbs-a', findings: [], investigations: [] };
else if (url.pathname === '/api/audit/actions')
data = { audits: [], count: 0, available: false };
else if (url.pathname === '/api/license/runtime-capabilities')
data = {
capabilities: [],
limits: [],
hosted_mode: false,
runtime: { build: 'community' },
blocked_capabilities: [],
};
else if (url.pathname === '/api/ai/settings') data = { discovery_enabled: false };
else if (url.pathname === '/api/security/status')
data = { hasAuthentication: true, requiresAuth: false, sessionCapabilities: {} };
else {
record.unexpected.push(url.pathname);
return route.fulfill({ status: 404, json: { error: 'Unspecified synthetic read' } });
}
return route.fulfill({ json: data });
});
const check = async (label, action) => {
result.phase = name + ': ' + label;
console.log(result.phase);
await action();
record.checks.push(label);
};
const capture = async (label) => {
const file = output + '/' + name + '-' + label + '.png';
await page.screenshot({ path: file, fullPage: true, animations: 'disabled' });
result.captures.push(file.replace('/workspace/', ''));
};
result.phase = name + ': mount full drawer';
await page.goto(origin + '/browser-tests/maintenance-evidence.html');
await page.getByRole('tab', { name: 'Manage', exact: true }).click();
const section = page.getByTestId('maintenance-verification-section');
await section.getByText('Checked 30s ago', { exact: true }).first().waitFor();
await page.getByLabel('Operator note', { exact: true }).fill('Unsaved protected note');
await check(
'separate recorded check/window/review ages, unknown pending completion and all four verdicts',
async () => {
assert.equal(
await section.getByText('Window ended 1 day ago', { exact: true }).count(),
4,
);
assert.equal(await section.getByText('Checked unknown', { exact: true }).count(), 1);
assert.equal(
await section.getByText('Reviewed 10s ago by fixture-admin', { exact: true }).count(),
1,
);
assert.match(await section.innerText(), /Reports record past checks, not current health/);
for (const status of ['healthy', 'pending', 'needs_review', 'failed_verification'])
assert.equal(
await section.getByTestId('maintenance-verification-status-' + status).count(),
1,
);
},
);
await capture('recorded');
const reads = record.reads.length;
await page.clock.fastForward(60_000);
await check(
'held events age past a minute without reads/writes, changing verdicts or discarding edits',
async () => {
await section.getByText('Checked 1 min ago', { exact: true }).first().waitFor();
assert.equal(
await section.getByText('Reviewed 1 min ago by fixture-admin', { exact: true }).count(),
1,
);
assert.equal(
await page.getByLabel('Operator note', { exact: true }).inputValue(),
'Unsaved protected note',
);
assert.equal(record.reads.length, reads);
assert.equal(record.writes.length, 0);
},
);
await page.clock.fastForward(3_600_000);
await check(
'an hour-old verdict stays historical with its evidence, review admission and saved restrictions',
async () => {
await section.getByText('Checked 1 hour ago', { exact: true }).first().waitFor();
assert.equal(
await section
.getByText('Reviewed 1 hour ago by fixture-admin', { exact: true })
.count(),
1,
);
assert.equal(await section.getByTestId('maintenance-verification-review').count(), 3);
assert.equal(
await page
.getByLabel('Never auto-remediate this resource', { exact: true })
.getAttribute('aria-pressed'),
'true',
);
assert.equal(
await page.getByLabel('Monitoring', { exact: true }).inputValue(),
'expected_offline',
);
assert.equal(await page.getByLabel('Lifecycle', { exact: true }).inputValue(), 'retired');
assert.equal(record.reads.length, reads);
assert.equal(record.writes.length, 0);
},
);
await capture('hour-old');
await page.evaluate(() => window.__maintenanceEvidence.select('pending-resource'));
// The real drawer deliberately remounts at a different canonical ID and
// starts on Overview. Open Manage as an operator would; do not assume a
// held tab or misattribute that existing protection to this repair.
await page.getByRole('tab', { name: 'Manage', exact: true }).click();
await bounded(readStarted, 'new-resource GET observed');
await check(
'changing resource immediately withdraws prior reports while its own read waits',
async () => {
await section.getByText('Loading verification reports…', { exact: true }).waitFor();
assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
assert.equal(await section.getByText(/No verification reports yet/).count(), 0);
await page.clock.fastForward(60_000);
assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
},
);
await capture('other-resource-pending');
releaseRead();
await check(
'a failed first read is unavailable, not empty history or another resource evidence',
async () => {
await section.getByTestId('maintenance-verification-load-error').waitFor();
assert.equal(await section.getByText(/No verification reports yet/).count(), 0);
assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
const before = record.reads.length;
await page.clock.fastForward(60_000);
assert.equal(record.reads.length, before);
},
);
await capture('unavailable');
mode = 'denied';
await section.getByRole('button', { name: 'Retry reports', exact: true }).focus();
await section.getByRole('button', { name: 'Retry reports', exact: true }).press('Enter');
await check(
'explicit keyboard read retry shows bounded denied guidance without reviving reports',
async () => {
await section
.getByText('Access denied. Check your permissions and license plan.', { exact: true })
.waitFor();
const before = record.reads.length;
await page.clock.fastForward(60_000);
assert.equal(record.reads.length, before);
assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
assert.equal(record.writes.length, 0);
},
);
await capture('denied');
mode = 'empty';
await section.getByRole('button', { name: 'Retry reports', exact: true }).click();
await check(
'only a successful empty read is empty history, with no subsequent clock retry',
async () => {
await section.getByText(/No verification reports yet/).waitFor();
assert.equal(await section.getByTestId('maintenance-verification-load-error').count(), 0);
const before = record.reads.length;
await page.clock.fastForward(60_000);
assert.equal(record.reads.length, before);
assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
},
);
await capture('empty');
await check(
'no page error, unspecified/off-origin request, operation or horizontal overflow',
async () => {
assert.deepEqual(record.errors, []);
assert.deepEqual(record.unexpected, []);
assert.deepEqual(record.offOrigin, []);
assert.deepEqual(record.writes, []);
assert.equal(
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
true,
);
},
);
record.completed = true;
await context.close();
record.contextClosed = true;
context = undefined;
await browser.close();
browser = undefined;
}
result.completed = true;
} catch (error) {
result.error = { name: error.name, message: error.message };
throw error;
} finally {
if (context) await context.close();
if (browser) await browser.close();
result.browsersClosed = true;
await server.close();
result.serverClosed = true;
fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
console.log(
JSON.stringify({
completed: result.completed,
phase: result.phase,
checks: result.cases.map((c) => ({
name: c.name,
count: c.checks.length,
errors: c.errors,
})),
captures: result.captures.length,
serverClosed: result.serverClosed,
}),
);
}
})();
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Recorded maintenance evidence</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/maintenance-evidence.tsx"></script>
</body>
</html>
@@ -0,0 +1,53 @@
// The full production resource drawer, transport/query, shared clock and CSS.
// Only snapshots and HTTP responses are synthetic. No real operation runs.
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import { ResourceDetailDrawer } from '../src/components/Infrastructure/ResourceDetailDrawer';
import { DarkModeContext, WebSocketContext } from '../src/contexts/appRuntime';
import { syncAIRuntimeSettings } from '../src/stores/aiRuntimeState';
import type { Resource } from '../src/types/resource';
import '../src/index.css';
syncAIRuntimeSettings({ discovery_enabled: false });
const [resource, setResource] = createSignal<Resource>({
id: 'pbs-a',
name: 'Recorded backup server',
displayName: 'Recorded backup server',
type: 'pbs',
platformId: 'pbs-a',
platformType: 'proxmox-pbs',
sourceType: 'api',
status: 'online',
cpu: { current: 12 },
lastSeen: Date.parse('2026-10-11T04:00:00Z'),
});
(window as any).__maintenanceEvidence = {
withdraw: () =>
setResource((current) => ({ ...current, id: 'empty-resource', name: 'No reports' })),
select: (id: string) =>
setResource((current) => ({
...current,
id,
name: 'Different resource',
displayName: 'Different resource',
})),
};
render(
() => (
<WebSocketContext.Provider
value={{ state: { pmg: [] }, activeAlerts: {}, connected: () => true } as any}
>
<DarkModeContext.Provider value={() => document.documentElement.classList.contains('dark')}>
<main class="mx-auto min-h-screen max-w-5xl space-y-3 bg-surface p-3 text-base-content">
<h1 class="text-base font-semibold">Recorded maintenance evidence</h1>
<p class="text-xs text-muted">
Synthetic local responses. No operation reaches a server.
</p>
<ResourceDetailDrawer resource={resource()} />
</main>
</DarkModeContext.Provider>
</WebSocketContext.Provider>
),
document.getElementById('root')!,
);
@@ -0,0 +1,35 @@
{
"version": 1,
"base_sha": "80ef7a2923b5bee25d3aa48ce678f2d4b286725f",
"verified_at": "2026-10-11T04:29:14.340458Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx": "af1eac63570ab102272f25fb1ec5cade2ee3508284600a131ff71a9c860efa87"
},
"routes": ["/browser-tests/maintenance-evidence.html"],
"viewports": [
{
"width": 1365,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Full production ResourceDetailDrawer Manage, actual report component, query/transport, CSS and shared clock; synthetic local-only snapshots/HTTP. Desktop Chromium 141.0.7390.37/light and phone WebKit 26.0/dark. Playwright 1.56.1 matches integration lock.",
"Checked, Window ended and Reviewed remain distinct historical events through minute/hour boundaries; all four verdicts, evidence, operator locks and unsaved note preserved. Go zero-time completion stays unknown.",
"Loading, 503 unavailable, 403 denied and successful empty read remain distinct. Explicit keyboard read-only retry; no unsolicited retry or mutation on a tick. The full drawer already remounts at a different ID; opening Manage there shows only its own report state. Component-level held-prop isolation and same-resource failed-refresh retention are separately covered by runtime tests.",
"Two completed cases / 16 controls / twelve inspected screenshots, no page error, unspecified/off-origin request, operation or horizontal overflow. No full App, actual authentication, backend review/rerun, appliance, guest recovery, native notification, installed or released acceptance."
],
"interactions": [
"Advance only the page clock with held reports. Observe stable verdict/counts and saved restrictions; preserve unsaved operator note.",
"Synthetic resource selection uses existing keyed drawer reset, then explicitly open Manage. Held GET returns 503, keyboard Retry reports returns 403, another explicit GET returns empty; reports stay absent through ticks.",
"All twelve resulting captures visually inspected. Both contexts, browsers and Vite closed; host descendant cleanup is not independently reported. Earlier failed/cancelled/unknown attempts are retained, not rebound or counted here."
],
"control_count": 16
}
@@ -1,6 +1,8 @@
import { Component, For, Show, createMemo, createSignal } from 'solid-js';
import { createNonSuspendingQuery } from '@/hooks/createNonSuspendingQuery';
import { notificationStore } from '@/stores/notifications';
import { Button } from '@/components/shared/Button';
import { InlineNotice } from '@/components/shared/InlineNotice';
import {
listMaintenanceVerificationsForResource,
rerunMaintenanceVerification,
@@ -9,6 +11,8 @@ import {
type MaintenanceVerificationStatus,
} from '@/api/maintenanceVerification';
import { formatRelativeTime } from '@/utils/format';
import { useRelativeTimeNow } from '@/utils/relativeTimeClock';
import { getAPIReadAccessErrorMessage } from '@/utils/apiAccessError';
/**
* MaintenanceVerificationSection surfaces Maintenance Verification
@@ -50,22 +54,23 @@ const STATUS_CLASSES: Record<MaintenanceVerificationStatus, string> = {
export const MaintenanceVerificationSection: Component<MaintenanceVerificationSectionProps> = (
props,
) => {
const [refreshTick, setRefreshTick] = createSignal(0);
const now = useRelativeTimeNow();
// Age only the recorded events. Ticks neither refresh the report nor change
// its verdict, and a rerun's completion time is not the window's end time.
const reportAge = (timestamp?: string) => {
if (!timestamp || timestamp.startsWith('0001-01-01')) return 'unknown';
return formatRelativeTime(timestamp, { now: now() }) || 'unknown';
};
const query = createNonSuspendingQuery<MaintenanceVerificationReport[], string>({
source: () => (props.resourceId ? `${props.resourceId}:${refreshTick()}` : null),
fetcher: async () => {
try {
const res = await listMaintenanceVerificationsForResource(props.resourceId);
return res.data ?? [];
} catch (err) {
notificationStore.error(
err instanceof Error ? err.message : 'Failed to load Maintenance Verification Reports',
);
return [];
}
source: () => props.resourceId || null,
fetcher: async (resourceId) => {
const res = await listMaintenanceVerificationsForResource(resourceId);
return res.data ?? [];
},
initialValue: [] as MaintenanceVerificationReport[],
// A held report belongs only to its resource. The query withdraws denied
// reads, but retains last-loaded records on a same-resource transient error.
retainPreviousValueOnSourceChange: false,
cacheKey: (key: string) => `maintenance-verifications:${key}`,
});
@@ -75,7 +80,7 @@ export const MaintenanceVerificationSection: Component<MaintenanceVerificationSe
const [rerunning, setRerunning] = createSignal(false);
const [reviewingId, setReviewingId] = createSignal<string | null>(null);
const refresh = () => setRefreshTick((n) => n + 1);
const refresh = () => void query.refetch();
const handleRerun = async () => {
if (rerunning() || !props.resourceId) return;
@@ -121,8 +126,8 @@ export const MaintenanceVerificationSection: Component<MaintenanceVerificationSe
Maintenance verification
</h3>
<p class="text-xs text-muted">
Pulse runs deterministic checks each time a maintenance window ends and writes a
Maintenance Verification Report. The result sticks here for review.
Pulse records the result when a maintenance window ends. Reports record past checks, not
current health.
</p>
</div>
<button
@@ -136,13 +141,47 @@ export const MaintenanceVerificationSection: Component<MaintenanceVerificationSe
</button>
</header>
<Show when={query.error()}>
<InlineNotice
tone="warning"
role="alert"
class="mt-3"
data-testid="maintenance-verification-load-error"
>
<p class="font-medium">Verification reports unavailable</p>
<p>
{getAPIReadAccessErrorMessage(query.error()) ?? 'Could not load verification reports.'}
</p>
<Show when={hasReports()}>
<p>Last loaded reports remain below.</p>
</Show>
<Button
type="button"
variant="secondary"
size="sm"
disabled={query.loading()}
onClick={() => {
if (!query.loading()) refresh();
}}
>
Retry reports
</Button>
</InlineNotice>
</Show>
<Show
when={hasReports()}
fallback={
<div class="mt-3 rounded-sm border border-dashed border-border bg-surface-hover p-3 text-xs text-muted">
No verification reports yet. A report is written automatically the next time this
resource exits a maintenance window.
</div>
<Show when={!query.error()}>
<div
class="mt-3 rounded-sm border border-dashed border-border bg-surface-hover p-3 text-xs text-muted"
role={query.loading() ? 'status' : undefined}
>
{query.loading()
? 'Loading verification reports…'
: 'No verification reports yet. A report is written automatically the next time this resource exits a maintenance window.'}
</div>
</Show>
}
>
<ul class="mt-3 space-y-2">
@@ -160,9 +199,9 @@ export const MaintenanceVerificationSection: Component<MaintenanceVerificationSe
>
{STATUS_LABELS[report.status]}
</span>
<span class="text-xs text-muted">Checked {reportAge(report.completedAt)}</span>
<span class="text-xs text-muted">
Window ended{' '}
{report.windowEndedAt ? formatRelativeTime(report.windowEndedAt) : 'unknown'}
Window ended {reportAge(report.windowEndedAt)}
</span>
</div>
<Show when={!report.userOutcome}>
@@ -178,7 +217,7 @@ export const MaintenanceVerificationSection: Component<MaintenanceVerificationSe
</Show>
<Show when={report.userOutcome === 'reviewed'}>
<span class="text-[11px] text-muted">
Reviewed {report.reviewedAt ? formatRelativeTime(report.reviewedAt) : ''}
Reviewed {reportAge(report.reviewedAt)}
{report.reviewedBy ? ` by ${report.reviewedBy}` : ''}
</span>
</Show>
@@ -0,0 +1,407 @@
import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MaintenanceVerificationReport } from '@/api/maintenanceVerification';
import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
import { RELATIVE_TIME_TICK_MS } from '@/utils/relativeTimeClock';
const api = vi.hoisted(() => ({ apiFetchJSON: vi.fn() }));
vi.mock('@/utils/apiClient', async (importOriginal) => ({
...(await importOriginal<typeof import('@/utils/apiClient')>()),
apiFetchJSON: api.apiFetchJSON,
}));
vi.mock('@/stores/notifications', () => ({
notificationStore: { error: vi.fn(), success: vi.fn() },
}));
import { MaintenanceVerificationSection } from '../MaintenanceVerificationSection';
const start = Date.parse('2026-10-11T04:00:00Z');
const iso = (offset: number) => new Date(start + offset).toISOString();
const resourceId = 'cluster:pve-a:101';
const report = (
overrides: Partial<MaintenanceVerificationReport> = {},
): MaintenanceVerificationReport => ({
id: 'report:window',
resourceId,
trigger: 'maintenance_window_ended',
status: 'healthy',
startedAt: iso(-30_000),
completedAt: iso(-30_000),
windowStartedAt: iso(-3_600_000),
windowEndedAt: iso(-30_000),
evidence: {
activeCriticalAlerts: 0,
activeWarningAlerts: 2,
activeCriticalFindings: 0,
activeWarningFindings: 1,
failedActionsSinceWindowStart: 0,
metricRecovery: { samplesAfterEnd: 3, trend: 'improving' },
},
linkedFindingIds: [],
linkedAlertIds: [],
linkedActionIds: [],
recommendation: 'Recorded recovery evidence',
...overrides,
});
const response = (reports: MaintenanceVerificationReport[]) => ({ data: reports });
const mount = async (reports: MaintenanceVerificationReport[]) => {
api.apiFetchJSON.mockResolvedValue(response(reports));
const rendered = render(() => <MaintenanceVerificationSection resourceId={resourceId} />);
await vi.advanceTimersByTimeAsync(0);
return rendered;
};
const mutations = () =>
api.apiFetchJSON.mock.calls.filter(([, options]) => options?.method === 'POST');
describe('durable maintenance verification evidence', () => {
beforeEach(() => {
vi.useFakeTimers();
resetCreateNonSuspendingQueryCacheForTest();
vi.setSystemTime(start);
api.apiFetchJSON.mockReset();
});
afterEach(() => {
cleanup();
resetCreateNonSuspendingQueryCacheForTest();
vi.useRealTimers();
});
it('ages held window and review events without fetching, rewriting or rerunning the report', async () => {
const saved = report({
userOutcome: 'reviewed',
reviewedAt: iso(-30_000),
reviewedBy: 'fixture-admin',
});
const original = JSON.stringify(saved);
await mount([saved]);
expect(screen.getByText('Window ended 30s ago')).toBeInTheDocument();
expect(screen.getByText('Reviewed 30s ago by fixture-admin')).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(RELATIVE_TIME_TICK_MS);
expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
expect(screen.getByText('Reviewed 1 min ago by fixture-admin')).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(3_600_000);
expect(screen.getByText('Window ended 1 hour ago')).toBeInTheDocument();
expect(screen.getByText('Reviewed 1 hour ago by fixture-admin')).toBeInTheDocument();
expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
'Healthy',
);
expect(screen.queryByTestId('maintenance-verification-review')).not.toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
expect(mutations()).toEqual([]);
expect(JSON.stringify(saved)).toBe(original);
});
it('shows when a check completed, separately from the earlier maintenance window and review', async () => {
await mount([
report({
windowEndedAt: iso(-86_400_000),
userOutcome: 'reviewed',
reviewedAt: iso(-10_000),
}),
]);
expect(screen.getByText('Checked 30s ago')).toBeInTheDocument();
expect(screen.getByText('Window ended 1 day ago')).toBeInTheDocument();
expect(screen.getByText('Reviewed 10s ago')).toBeInTheDocument();
expect(
screen.getByText(/Reports record past checks, not current health\./),
).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.getByText('Checked 1 min ago')).toBeInTheDocument();
expect(screen.getByText('Reviewed 1 min ago')).toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
});
it('keeps every recorded verdict and its counts unchanged as several reports age', async () => {
const states = ['pending', 'healthy', 'needs_review', 'failed_verification'] as const;
const records = states.map((status) => report({ id: `report:${status}`, status }));
const original = JSON.stringify(records);
await mount(records);
await vi.advanceTimersByTimeAsync(120_000);
for (const status of states) {
const row = screen.getByTestId(`maintenance-verification-status-${status}`).closest('li')!;
expect(within(row).getByText('Window ended 2 mins ago')).toBeInTheDocument();
expect(within(row).getByText('Checked 2 mins ago')).toBeInTheDocument();
expect(within(row).getByText('Warning alerts').nextElementSibling).toHaveTextContent('2');
expect(within(row).getByText('Metric samples').nextElementSibling).toHaveTextContent('3');
expect(within(row).getByTestId('maintenance-verification-review')).toBeEnabled();
}
expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
expect(JSON.stringify(records)).toBe(original);
});
it.each(['', 'invalid', '0001-01-01T00:00:00Z'])(
'keeps absent or malformed event times unknown (%j)',
async (time) => {
await mount([
report({
windowEndedAt: time,
completedAt: time,
userOutcome: 'reviewed',
reviewedAt: time,
}),
]);
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.getByText('Window ended unknown')).toBeInTheDocument();
expect(screen.getByText('Checked unknown')).toBeInTheDocument();
expect(screen.getByText('Reviewed unknown')).toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
},
);
it('keeps clock-skewed past events as ages rather than inventing a scheduled operation', async () => {
await mount([report({ completedAt: iso(30_000), windowEndedAt: iso(30_000) })]);
expect(screen.getByText('Checked 0s ago')).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(90_000);
expect(screen.getByText('Checked 1 min ago')).toBeInTheDocument();
expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
expect(mutations()).toEqual([]);
});
it('withdraws the old resource reports and ignores a late old-resource response across ticks', async () => {
let finishOld!: (value: ReturnType<typeof response>) => void;
api.apiFetchJSON.mockImplementation((path: string) =>
path.includes('old-resource')
? new Promise((resolve) => {
finishOld = resolve;
})
: Promise.resolve(response([])),
);
const [id, setId] = createSignal('old-resource');
render(() => <MaintenanceVerificationSection resourceId={id()} />);
await vi.advanceTimersByTimeAsync(0);
setId('empty-resource');
await vi.advanceTimersByTimeAsync(0);
finishOld(response([report()]));
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
expect(screen.getByText(/No verification reports yet/)).toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
});
it('does not revive withdrawn reports after an access-denied resource read', async () => {
api.apiFetchJSON
.mockResolvedValueOnce(response([report()]))
.mockRejectedValueOnce(new Error('Access denied'));
const [id, setId] = createSignal(resourceId);
render(() => <MaintenanceVerificationSection resourceId={id()} />);
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
setId('denied-resource');
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
});
it('requires an explicit review, keeps pending admission disabled and uses the returned review fields', async () => {
const saved = report();
await mount([saved]);
let finishReview!: (value: MaintenanceVerificationReport) => void;
api.apiFetchJSON.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishReview = resolve;
}),
);
const button = screen.getByTestId('maintenance-verification-review');
fireEvent.click(button);
await vi.advanceTimersByTimeAsync(60_000);
expect(button).toBeDisabled();
expect(button).toHaveTextContent('Marking…');
fireEvent.click(button);
expect(mutations()).toHaveLength(1);
expect(mutations()[0][0]).toBe('/api/maintenance-verifications/report%3Awindow/review');
expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
'Healthy',
);
expect(screen.queryByText(/Reviewed/)).not.toBeInTheDocument();
const reviewed = {
...saved,
userOutcome: 'reviewed' as const,
reviewedAt: iso(60_000),
reviewedBy: 'fixture-admin',
};
api.apiFetchJSON.mockResolvedValue(response([reviewed]));
finishReview(reviewed);
await vi.advanceTimersByTimeAsync(0);
expect(screen.queryByTestId('maintenance-verification-review')).not.toBeInTheDocument();
expect(screen.getByText('Reviewed 0s ago by fixture-admin')).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.getByText('Reviewed 1 min ago by fixture-admin')).toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
});
it('reruns only after a click, keeps the pending action disabled and retains the old report beside the new check', async () => {
const saved = report();
await mount([saved]);
let finishRerun!: (value: MaintenanceVerificationReport) => void;
api.apiFetchJSON.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishRerun = resolve;
}),
);
const button = screen.getByTestId('maintenance-verification-rerun');
fireEvent.click(button);
await vi.advanceTimersByTimeAsync(60_000);
expect(button).toBeDisabled();
fireEvent.click(button);
expect(mutations()).toHaveLength(1);
expect(mutations()[0][0]).toBe(
'/api/resources/cluster%3Apve-a%3A101/maintenance-verifications/rerun',
);
const rerun = report({
id: 'report:window-rerun-1',
completedAt: iso(60_000),
status: 'needs_review',
});
api.apiFetchJSON.mockResolvedValue(response([rerun, saved]));
finishRerun(rerun);
await vi.advanceTimersByTimeAsync(0);
expect(button).toBeEnabled();
expect(screen.getAllByTestId('maintenance-verification-report')).toHaveLength(2);
expect(screen.getByText('Checked 0s ago')).toBeInTheDocument();
expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
'Healthy',
);
expect(screen.getByTestId('maintenance-verification-status-needs_review')).toHaveTextContent(
'Needs review',
);
await vi.advanceTimersByTimeAsync(60_000);
expect(mutations()).toHaveLength(1);
expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
});
it.each(['review', 'rerun'])(
'does not retry a refused %s on a clock tick or alter the report',
async (action) => {
await mount([report()]);
api.apiFetchJSON.mockRejectedValueOnce(new Error('Synthetic write refusal'));
fireEvent.click(screen.getByTestId(`maintenance-verification-${action}`));
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.getByTestId(`maintenance-verification-${action}`)).toBeEnabled();
expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
'Healthy',
);
expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
expect(mutations()).toHaveLength(1);
expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
},
);
it('does not describe a pending first read as an empty report history', async () => {
let finishRead!: (value: ReturnType<typeof response>) => void;
api.apiFetchJSON.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishRead = resolve;
}),
);
render(() => <MaintenanceVerificationSection resourceId={resourceId} />);
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByText('Loading verification reports…')).toBeInTheDocument();
expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
finishRead(response([]));
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByText(/No verification reports yet/)).toBeInTheDocument();
});
it('withdraws loaded reports immediately while another resource read is pending', async () => {
api.apiFetchJSON
.mockResolvedValueOnce(response([report()]))
.mockImplementationOnce(() => new Promise(() => {}));
const [id, setId] = createSignal(resourceId);
render(() => <MaintenanceVerificationSection resourceId={id()} />);
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
setId('new-resource-pending');
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
expect(screen.getByText('Loading verification reports…')).toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
});
it('distinguishes a failed first read from empty history and retries only after an explicit read request', async () => {
api.apiFetchJSON.mockRejectedValueOnce(
Object.assign(new Error('Synthetic server internals must not render'), { status: 503 }),
);
render(() => <MaintenanceVerificationSection resourceId={resourceId} />);
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
'Verification reports unavailable',
);
expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
expect(screen.queryByText(/Synthetic server internals/)).not.toBeInTheDocument();
await vi.advanceTimersByTimeAsync(60_000);
expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
let finishRetry!: (value: ReturnType<typeof response>) => void;
api.apiFetchJSON.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishRetry = resolve;
}),
);
fireEvent.click(screen.getByRole('button', { name: 'Retry reports' }));
expect(screen.getByRole('button', { name: 'Retry reports' })).toBeDisabled();
await vi.advanceTimersByTimeAsync(60_000);
expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
finishRetry(response([report()]));
await vi.advanceTimersByTimeAsync(0);
expect(screen.queryByTestId('maintenance-verification-load-error')).not.toBeInTheDocument();
expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
expect(mutations()).toEqual([]);
});
it('retains qualified historical reports after a same-resource refresh fails, without inventing a successful review', async () => {
const saved = report();
await mount([saved]);
api.apiFetchJSON
.mockResolvedValueOnce(saved)
.mockRejectedValueOnce(Object.assign(new Error('Synthetic read failure'), { status: 503 }));
fireEvent.click(screen.getByTestId('maintenance-verification-review'));
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
'Last loaded reports remain below.',
);
expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
'Healthy',
);
expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
expect(screen.queryByText(/Reviewed/)).not.toBeInTheDocument();
expect(mutations()).toHaveLength(1);
expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
});
it.each([401, 403])(
'withdraws reports on a denied same-resource refresh (%s), rather than offering them as authorised history',
async (status) => {
const saved = report();
await mount([saved]);
api.apiFetchJSON
.mockResolvedValueOnce(saved)
.mockRejectedValueOnce(
Object.assign(new Error('Synthetic access internals must not render'), { status }),
);
fireEvent.click(screen.getByTestId('maintenance-verification-rerun'));
await vi.advanceTimersByTimeAsync(60_000);
expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
status === 401
? 'Sign in again or check your API token.'
: 'Access denied. Check your permissions and license plan.',
);
expect(screen.queryByText(/Synthetic access internals/)).not.toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
const reviewed = report({ userOutcome: 'reviewed', reviewedAt: iso(60_000) });
api.apiFetchJSON.mockResolvedValueOnce(response([reviewed]));
fireEvent.click(screen.getByRole('button', { name: 'Retry reports' }));
await vi.advanceTimersByTimeAsync(0);
expect(screen.getByText('Reviewed 0s ago')).toBeInTheDocument();
expect(screen.queryByTestId('maintenance-verification-load-error')).not.toBeInTheDocument();
expect(api.apiFetchJSON).toHaveBeenCalledTimes(4);
expect(mutations()).toHaveLength(1);
},
);
});