Keep drawer History pointer values at one observed time

Resolve pointer inspection against the group's actual stored timestamps. Missing series stay unavailable instead of borrowing neighbours or live readings, while keyboard focus, single samples and scoped refresh recovery remain intact. Add mounted regressions, the shared contract and exact-content browser receipts.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-10-01 01:38:17 +01:00
parent 0dc858cb36
commit 02090fe351
5 changed files with 651 additions and 70 deletions

View file

@ -7577,3 +7577,23 @@ Mounted inspection regressions cover these boundaries. The direct-renderer
browser fixture verifies native keys, focus, pointer coexistence and touch at
desktop/phone widths across Chromium, Firefox and WebKit; it is not installed
PBS collection or a screen-reader announcement-quality claim.
### Drawer History pointer values share one observed time
Pointer inspection snaps to the nearest actual stored timestamp across the
group's series, with equidistant observations resolved to the earlier time.
Every displayed value and marker must belong to that timestamp. Missing series
remain unavailable at the inspected time, not a nearest neighbour, latest
observation or current reading. A lone stored sample, including zero, can be
inspected without fabricating a trend. The SVG's dated description follows the
same active time as the visible legend. Leaving the plot restores the normal
latest/current legend; focused native inspection still takes precedence.
Matching refreshes reconcile the pointer with the current set of actual
observations; resource/range replacement still clears pointer state. Mounted
`GuestDrawerHistory.pointer.test.tsx` regressions cover sparse and disjoint
series, zero rates, single observations, ties, focus precedence and replacement.
`browser-tests/history-pointer.cjs` verifies the production renderer and query
in Chromium/Firefox desktop and Chromium/WebKit phone emulation in both themes,
including failed refreshes and source replacement. Scripted APIs establish
presentation accuracy, not real PBS collection or installed #1723 acceptance.

View file

@ -0,0 +1,272 @@
// Production History/query/CSS with synthetic API data, not installed PBS acceptance.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const { chromium, firefox, webkit } = require('playwright');
(async () => {
const root = '/workspace/frontend-modern';
process.chdir(root);
const engine =
process.argv.find((arg) => arg.startsWith('--engine='))?.split('=')[1] || 'chromium';
assert.ok(['chromium', 'firefox', 'webkit'].includes(engine));
const width = process.argv.includes('--phone') ? 390 : 1365;
const artifacts = path.join(root, 'node_modules', `history-pointer-${engine}-${width}`);
fs.mkdirSync(artifacts, { recursive: true });
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
const server = await createServer({
root,
configFile: path.join(root, 'vite.config.ts'),
cacheDir: fs.mkdtempSync(path.join(artifacts, 'vite-cache-')),
server: { host: '127.0.0.1', port: 5223, strictPort: true },
});
let browser;
const observations = [];
const sourceFile = path.join(root, 'src/components/Workloads/GuestDrawerHistory.tsx');
const sourceHash = createHash('sha256').update(fs.readFileSync(sourceFile)).digest('hex');
const playwrightVersion = require('playwright/package.json').version;
const integrationVersion = JSON.parse(
fs.readFileSync('/workspace/tests/integration/package-lock.json'),
).packages['node_modules/@playwright/test'].version;
assert.equal(playwrightVersion, integrationVersion);
try {
await server.listen();
browser = await { chromium, firefox, webkit }[engine].launch(
engine === 'chromium'
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
for (const theme of ['light', 'dark']) {
const page = await browser.newPage({
viewport: { width, height: width === 390 ? 844 : 900 },
locale: 'en-GB',
timezoneId: 'UTC',
...(width === 390 ? { isMobile: true, hasTouch: true } : {}),
});
page.setDefaultTimeout(20_000);
const requests = [],
errors = [];
let kind = 'original',
held;
page.on('pageerror', (error) => errors.push(error.message));
const time = 1_700_000_000_000;
const point = (offset, value) => ({
timestamp: time + offset,
value,
min: value,
max: value,
});
const metrics = (state) => {
if (state === 'empty') return {};
if (state === 'service') return { cpu: [point(0, 77), point(120_000, 88)] };
return {
cpu: [point(120_000, 43), point(0, 11), point(60_000, 22)],
memory: [point(30_000, state === 'updated' ? 31 : 30), point(120_000, 55)],
disk: [point(45_000, 0)],
netin: [point(0, 0), point(120_000, 2048)],
netout: [point(30_000, 4096)],
diskread: [point(0, 2048), point(120_000, 8192)],
diskwrite: [point(0, 0), point(120_000, 0)],
temperature: [point(0, 42), point(120_000, 57)],
};
};
const response = (id, range, state) => ({
resourceType: 'agent',
resourceId: id,
range,
start: time,
end: time + 120_000,
source: 'store',
metrics: metrics(state),
});
await page.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (url.origin !== 'http://127.0.0.1:5223') return route.abort();
if (!url.pathname.startsWith('/api/')) return route.continue();
if (url.pathname === '/api/metrics-store/history') {
const id = url.searchParams.get('resourceId'),
range = url.searchParams.get('range');
assert.ok(['agent-three', 'pbs-three'].includes(id));
const state = kind;
requests.push({ id, range, state, method: route.request().method() });
if (state === 'hold') {
held = route;
return;
}
if (state === 'failure')
return route.fulfill({ status: 503, json: { error: 'Private fixture detail' } });
return route.fulfill({ json: response(id, range, state) });
}
if (url.pathname === '/api/license/runtime-capabilities')
return route.fulfill({
json: {
capabilities: [],
limits: [],
max_history_days: 7,
hosted_mode: false,
runtime: { build: 'community', label: 'Pulse Community runtime' },
blocked_capabilities: [],
},
});
return route.fulfill({ json: { data: [], enabled: false } });
});
await page.goto('http://127.0.0.1:5223/browser-tests/pbs-history-refresh.html', {
waitUntil: 'domcontentloaded',
});
await page.evaluate(
(dark) => document.documentElement.classList.toggle('dark', dark),
theme === 'dark',
);
const fixture = page.getByTestId('history-refresh-fixture');
const utilization = fixture.locator('[data-history-group="utilization"]');
const slider = utilization.getByRole('slider', { name: 'Inspect Utilization history' });
await slider.waitFor();
const readsBefore = requests.length;
const plot = utilization.getByTestId('guest-history-plot');
const hover = async (chart, offset) => {
await chart.scrollIntoViewIfNeeded();
const box = await chart.getByTestId('guest-history-plot').boundingBox();
assert.ok(box);
await page.mouse.move(
box.x + ((34 + (offset / 120_000) * 318) / 360) * box.width,
box.y + box.height / 2,
);
};
const text = async () => (await utilization.innerText()).replace(/\s+/g, ' ');
const sparseTime = async (offset, expected, absent) => {
await hover(utilization, offset);
assert.match(await text(), expected);
assert.match(await text(), absent);
const markers = await plot
.locator('circle[r="3"]')
.evaluateAll((nodes) =>
nodes.map((node) => ({ x: node.getAttribute('cx'), y: node.getAttribute('cy') })),
);
assert.equal(new Set(markers.map((marker) => marker.x)).size, 1);
return markers;
};
// Capture the exact base misattribution too, before assertions fail.
await hover(utilization, 30_000);
const observed = { theme, sourceHash, text: await text(), browser: browser.version() };
console.log(JSON.stringify({ observedSparsePointer: observed }));
// Element screenshots scroll a tall phone fixture and end its hover.
// Capture the current viewport without disturbing the active pointer.
await page.screenshot({ path: path.join(artifacts, `sparse-pointer-${theme}.png`) });
assert.match(await text(), /CPU\s*- Memory\s*30\.0% Disk\s*-/);
assert.equal(await plot.locator('circle[r="3"]').count(), 1);
assert.equal(await utilization.getByTestId('guest-history-hover-time').innerText(), '22:13');
const descriptionId = await plot.getAttribute('aria-describedby');
assert.match(
await page.locator(`[id="${descriptionId}"]`).innerText(),
/14\/11\/2023, 22:13:50\. CPU no observation\. Memory 30\.0%\. Disk no observation\./,
);
await sparseTime(0, /CPU\s*11\.0%/, /Memory\s*- Disk\s*-/);
await sparseTime(45_000, /Disk\s*0\.0%/, /CPU\s*- Memory\s*-/);
assert.equal(
await plot.locator('path').count(),
2,
'lone disk sample must not become a path',
);
await hover(utilization, 120_000);
assert.match(await text(), /CPU\s*43\.0% Memory\s*55\.0% Disk\s*-/);
assert.equal(await plot.locator('circle[r="3"]').count(), 2);
assert.equal(await utilization.getByTestId('guest-history-hover-time').innerText(), '22:15');
const network = fixture.locator('[data-history-group="network"]');
await hover(network, 0);
assert.match((await network.innerText()).replace(/\s+/g, ' '), /In\s*0 B\/s Out\s*-/);
await hover(network, 30_000);
assert.match((await network.innerText()).replace(/\s+/g, ' '), /In\s*- Out\s*4\.00 KB\/s/);
await page.mouse.move(1, 1);
assert.match(await text(), /CPU\s*43\.0% Memory\s*55\.0% Disk\s*0\.0%/);
assert.equal(await fixture.getByTestId('guest-history-hover-time').count(), 0);
assert.equal(requests.length, readsBefore, 'pointer inspection must not request data');
// Native keyboard selection remains authoritative even with the pointer on another time.
await slider.focus();
await slider.press('Home');
await hover(utilization, 30_000);
assert.match(await text(), /CPU\s*11\.0% Memory\s*- Disk\s*-/);
await slider.press('ArrowRight');
assert.match(
await slider.getAttribute('aria-valuetext'),
/CPU no observation\. Memory 30\.0%\. Disk no observation/,
);
await slider.press('Tab');
await hover(utilization, 45_000);
assert.match(await text(), /CPU\s*- Memory\s*- Disk\s*0\.0%/);
if (width === 390) {
const box = await slider.boundingBox();
assert.ok(box.height >= 44);
await page.touchscreen.tap(box.x + 3, box.y + box.height / 2);
assert.match(
await slider.getAttribute('aria-valuetext'),
/CPU 11\.0%\. Memory no observation/,
);
await page.touchscreen.tap(box.x + box.width - 3, box.y + box.height / 2);
assert.match(await slider.getAttribute('aria-valuetext'), /CPU 43\.0%\. Memory 55\.0%/);
await slider.press('Tab');
}
await page.mouse.move(1, 1);
kind = 'failure';
await fixture.getByRole('button', { name: 'Refresh history' }).evaluate((el) => el.click());
await fixture
.getByText('History refresh failed. Showing previously loaded history.')
.waitFor();
await sparseTime(30_000, /Memory\s*30\.0%/, /CPU\s*-/);
await page.screenshot({
path: path.join(artifacts, `retained-sparse-pointer-${theme}.png`),
});
assert.equal((await fixture.innerText()).includes('Private fixture detail'), false);
kind = 'updated';
await fixture.getByRole('button', { name: 'Retry history' }).evaluate((el) => el.click());
await page.waitForFunction(() =>
[...document.querySelectorAll('[data-history-group="utilization"] span')].some(
(el) => el.textContent === '31.0%',
),
);
assert.match(await text(), /CPU\s*- Memory\s*31\.0% Disk\s*-/);
kind = 'hold';
await page
.getByRole('button', { name: 'Switch to service target' })
.evaluate((el) => el.click());
await page.waitForFunction(
() =>
[...document.querySelectorAll('[data-history-group="utilization"] path')].length === 0,
);
assert.equal(await fixture.getByTestId('guest-history-hover-time').count(), 0);
assert.equal(await plot.locator('circle[r="3"]').count(), 0);
assert.match(await text(), /CPU\s*42\.0% Memory\s*53\.0% Disk\s*-/);
assert.ok(held);
kind = 'service';
await held.fulfill({ json: response('pbs-three', '24h', 'service') });
await slider.waitFor();
await hover(utilization, 0);
assert.match(await text(), /CPU\s*77\.0% Memory\s*- Disk\s*-/);
const dimensions = await page.evaluate(() => ({
scroll: document.documentElement.scrollWidth,
inner: innerWidth,
}));
assert.ok(dimensions.scroll <= dimensions.inner + 1, JSON.stringify(dimensions));
assert.deepEqual(errors, []);
observations.push({ theme, requests, dimensions, sourceHash });
await page.close();
}
const result = {
engine,
width,
browserVersion: browser.version(),
playwrightVersion,
sourceHash,
observations,
result: 'passed',
};
fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result));
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View file

@ -1,16 +1,16 @@
{
"version": 1,
"base_sha": "0d3e5a801ab8fd1c1d224ef561cbf99e169c589f",
"verified_at": "2026-09-30T23:28:26Z",
"base_sha": "0dc858cb366a394b159d2c911c5d85b3020e7881",
"verified_at": "2026-10-01T00:37:51Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "327e75cabb99edffc712302c00645f06d92dc44dbebdd00aef08074e7fa53eb2"
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "e1658b5843ab22e0ea83d1bf27e8c1565a16d9f528eb3832524a67318d281d6a"
},
"routes": [
"/browser-tests/pbs-history-refresh.html (direct production GuestDrawerHistory/query with production CSS and synthetic metrics API data; not a complete PBS drawer or installed acceptance)"
"/browser-tests/pbs-history-refresh.html (production GuestDrawerHistory/query/CSS with synthetic host/service metrics API responses; not a complete PBS drawer or installed acceptance)"
],
"viewports": [
{
@ -23,57 +23,133 @@
}
],
"states": [
"light/dark ordered and sparse stored series",
"native focus with a 2px blue outline, measured contrast 3.68:1 light and 3.98:1 dark against the chart surface",
"missing series at a selected timestamp stay missing, not nearby or live values",
"same-source refresh adds samples without changing the selected timestamp or keyboard focus",
"failed refresh retains inspectable matching observations and honest warning",
"single stored observation has a dated description but no fabricated trend or pointless slider",
"empty store/current readings expose no inspection control",
"held replacement target has no former-host points or selection; new target starts at its own latest observation",
"licence-locked range has no reads or inspection action"
"light/dark sparse CPU, memory and lone disk observations with one common inspected timestamp",
"a missing metric at the inspected time stays unavailable instead of borrowing nearby, latest or live data",
"zero-valued lone observations can be inspected without a synthetic trend",
"genuinely aligned series share cursor geometry and their own observed values",
"network directions preserve independently missing and zero rates",
"native keyboard/touch selection takes precedence over pointer movement",
"failed same-source refresh retains correctly attributed observations and a visible warning",
"a successful refresh updates the hovered timestamp values without choosing other latest metrics",
"held service-target replacement has no former-host observations, cursor or dated pointer description"
],
"interactions": [
"Tab from History range through Refresh to the native observation slider; Home, ArrowRight, End and ArrowLeft select actual stored times with no extra API reads",
"refresh through the existing real query while the input stays focused; new sample indexes do not move the selected timestamp",
"move and leave the plot while the slider is focused; then blur and confirm existing mouse hover still works",
"Chromium/WebKit phone touch taps at both ends of the 44px-high native control update the selected stored values",
"fail a refresh, inspect retained observations; replace with a single sample and then no history; switch resource while its GET is held; load the service response; select locked 14d"
"move a real pointer to sparse, aligned and single-sample timestamps; inspect visible legends, exact-time SVG descriptions and marker geometry",
"leave the plot to restore latest/current legends; verify pointer inspection makes no API reads",
"focus native range input and use Home/ArrowRight while moving the pointer elsewhere; blur to restore pointer inspection",
"Chromium/WebKit phone-emulated touch taps at each end of the native observation input, retaining its 44px minimum hit area",
"fail a real query refresh, inspect retained samples; successfully refresh the same source while hovering; switch resource with its GET held, then fulfil the service response"
],
"command": "pulse-worker-browser frontend-modern/browser-tests/history-inspection.cjs --engine=chromium; the same command with --phone; --engine=firefox; --engine=webkit --phone. All from the assigned workspace root, Vite 6.4.3 / Playwright 1.56.1 (integration-lock parity verified).",
"command": "pulse-worker-browser frontend-modern/browser-tests/history-pointer.cjs --engine=chromium; the same with --phone; --engine=firefox; --engine=webkit --phone. All from the assigned workspace root, Vite 6.4.3 / Playwright 1.56.1 (integration-lock parity verified).",
"browser_versions": {
"chromium": "141.0.7390.37",
"firefox": "142.0.1",
"webkit": "26.0"
},
"artifacts": [
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-chromium-desktop.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-chromium-phone.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-firefox-desktop.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-webkit-phone.log"
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-1365/retained-sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-1365/retained-sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-1365/sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-1365/sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-390/retained-sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-390/retained-sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-390/sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/chromium-390/sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/firefox-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/firefox-1365/retained-sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/firefox-1365/retained-sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/firefox-1365/sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/firefox-1365/sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/webkit-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/webkit-390/retained-sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/webkit-390/retained-sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/webkit-390/sparse-pointer-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/browser-final/webkit-390/sparse-pointer-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/receipts/browser-chromium-desktop-final.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/receipts/browser-chromium-phone.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/receipts/browser-firefox-desktop.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-gc5fjc3_/receipts/browser-webkit-phone-isolated.log"
],
"notes": "All 16 final screenshots inspected. Eight engine/viewport/theme cases pass native keyboard, touch, state and layout assertions with no page errors. Semantic snapshots exercise native slider names/value text, not actual screen-reader speech quality. Initial dependency-copy/install failures, unsupported helper invocation, preliminary browser sequence exit 143 with unknown cause and the corrected dark-focus-colour defect remain in output receipts. No real PBS telemetry, full drawer, in-browser update, installed upgrade/rollback, containing-line or release availability claim. Copy-lint failure source-zhg72ioz at unshared 7c625696... is retained. Its semicolon-separated accessible value text is replaced by sentences; that unshared candidate was abandoned at the supplied base, not amended or integrated. All four browser commands and all 16 inspected screenshots now bind the corrected runtime. No failed earlier pass or shared history is rewritten.",
"notes": "All 16 final screenshots inspected. Eight engine/viewport/theme cases pass with no page errors or horizontal overflow. Exact-base source-5g7tw1c0 has eight failing assertions and two passing replacement controls; a separate Chromium base screenshot reproduces the misattribution. The first phone run captured correct hover text before an element screenshot scrolled the tall fixture and ended its hover; the final runner captures the viewport without changing pointer state. The first WebKit navigation timed out during overlapping browser runs; its cause is not established, and a subsequent isolated run completes both themes. Adverse attempts remain in internal output receipts. These synthetic API proofs establish shared-renderer presentation, not installed PBS collection, all three #1723 drawers, VirtualBox/disk/network/I/O collection, update or upgrade/rollback acceptance, screen-reader speech, containing-line delivery or release availability.",
"prior_verifications": [
{
"version": 1,
"base_sha": "0d3e5a801ab8fd1c1d224ef561cbf99e169c589f",
"verified_at": "2026-09-30T23:28:26Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "327e75cabb99edffc712302c00645f06d92dc44dbebdd00aef08074e7fa53eb2"
},
"routes": [
"/browser-tests/pbs-history-refresh.html (direct production GuestDrawerHistory/query with production CSS and synthetic metrics API data; not a complete PBS drawer or installed acceptance)"
],
"viewports": [
{
"width": 1365,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"light/dark ordered and sparse stored series",
"native focus with a 2px blue outline, measured contrast 3.68:1 light and 3.98:1 dark against the chart surface",
"missing series at a selected timestamp stay missing, not nearby or live values",
"same-source refresh adds samples without changing the selected timestamp or keyboard focus",
"failed refresh retains inspectable matching observations and honest warning",
"single stored observation has a dated description but no fabricated trend or pointless slider",
"empty store/current readings expose no inspection control",
"held replacement target has no former-host points or selection; new target starts at its own latest observation",
"licence-locked range has no reads or inspection action"
],
"interactions": [
"Tab from History range through Refresh to the native observation slider; Home, ArrowRight, End and ArrowLeft select actual stored times with no extra API reads",
"refresh through the existing real query while the input stays focused; new sample indexes do not move the selected timestamp",
"move and leave the plot while the slider is focused; then blur and confirm existing mouse hover still works",
"Chromium/WebKit phone touch taps at both ends of the 44px-high native control update the selected stored values",
"fail a refresh, inspect retained observations; replace with a single sample and then no history; switch resource while its GET is held; load the service response; select locked 14d"
],
"command": "pulse-worker-browser frontend-modern/browser-tests/history-inspection.cjs --engine=chromium; the same command with --phone; --engine=firefox; --engine=webkit --phone. All from the assigned workspace root, Vite 6.4.3 / Playwright 1.56.1 (integration-lock parity verified).",
"browser_versions": {
"chromium": "141.0.7390.37",
"firefox": "142.0.1",
"webkit": "26.0"
},
"artifacts": [
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/chromium-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/firefox-1365/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/selected-sparse-time-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/failed-refresh-inspection-light.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/selected-sparse-time-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/failed-refresh-inspection-dark.png",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/browser-final/webkit-390/result.json",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-chromium-desktop.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-chromium-phone.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-firefox-desktop.log",
"/var/lib/pulse-maintainer/worker-outputs/web-product-frhttefs/receipts/browser-webkit-phone.log"
],
"notes": "All 16 final screenshots inspected. Eight engine/viewport/theme cases pass native keyboard, touch, state and layout assertions with no page errors. Semantic snapshots exercise native slider names/value text, not actual screen-reader speech quality. Initial dependency-copy/install failures, unsupported helper invocation, preliminary browser sequence exit 143 with unknown cause and the corrected dark-focus-colour defect remain in output receipts. No real PBS telemetry, full drawer, in-browser update, installed upgrade/rollback, containing-line or release availability claim. Copy-lint failure source-zhg72ioz at unshared 7c625696... is retained. Its semicolon-separated accessible value text is replaced by sentences; that unshared candidate was abandoned at the supplied base, not amended or integrated. All four browser commands and all 16 inspected screenshots now bind the corrected runtime. No failed earlier pass or shared history is rewritten."
},
{
"version": 1,
"base_sha": "f3c92a242ebf9b7062dfc9f6ff711a56639f536c",

View file

@ -187,18 +187,18 @@ const getGuestDrawerHistoryY = (
return GUEST_DRAWER_HISTORY_PLOT_TOP + (1 - (bounded - scale.minValue) / valueSpan) * plotHeight;
};
const findClosestGuestDrawerHistoryPoint = (
points: readonly AggregatedMetricPoint[],
const findClosestGuestDrawerHistoryTimestamp = (
timestamps: readonly number[],
timestamp: number,
): AggregatedMetricPoint | null => {
if (points.length === 0) return null;
): number | null => {
if (timestamps.length === 0) return null;
let closest = points[0];
let closestDistance = Math.abs(points[0].timestamp - timestamp);
for (const point of points.slice(1)) {
const distance = Math.abs(point.timestamp - timestamp);
let closest = timestamps[0];
let closestDistance = Math.abs(closest - timestamp);
for (const candidate of timestamps.slice(1)) {
const distance = Math.abs(candidate - timestamp);
if (distance < closestDistance) {
closest = point;
closest = candidate;
closestDistance = distance;
}
}
@ -235,26 +235,38 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
const times = observationTimes();
const selected = selectedTimestamp();
if (selected === null) return Math.max(0, times.length - 1);
let index = 0;
for (let i = 1; i < times.length; i++) {
if (Math.abs(times[i] - selected) < Math.abs(times[index] - selected)) index = i;
}
return index;
const closest = findClosestGuestDrawerHistoryTimestamp(times, selected);
return closest === null ? 0 : times.indexOf(closest);
});
const inspectionTimestamp = createMemo(() =>
selectedTimestamp() === null ? null : (observationTimes()[selectedIndex()] ?? null),
);
const inspectionValueText = createMemo(() => {
const timestamp = observationTimes()[selectedIndex()];
// Resolve the pointer to one real time across the whole group. Independent
// nearest-series reads would attribute values from different times to the
// first metric's timestamp, and can even borrow a live fallback reading.
const activeTimestamp = createMemo(() => {
const inspected = inspectionTimestamp();
if (inspected !== null) return inspected;
const hovered = hoverTimestamp();
return hovered === null
? null
: findClosestGuestDrawerHistoryTimestamp(observationTimes(), hovered);
});
const observationValueText = (timestamp: number | undefined): string => {
if (timestamp === undefined) return 'No stored history observations.';
const values = series().map((item) => {
const point = item.points.find((point) => point.timestamp === timestamp);
return `${item.label} ${point ? formatHistoryChartTooltipValue(point.value, item.unit) : 'no observation'}`;
});
return `${new Date(timestamp).toLocaleString()}. ${values.join('. ')}.`;
});
};
const inspectionValueText = createMemo(() =>
observationValueText(observationTimes()[selectedIndex()]),
);
const chartDescription = createMemo(() => {
if (props.loading) return 'Loading history.';
const timestamp = activeTimestamp();
if (timestamp !== null) return observationValueText(timestamp);
const count = observationTimes().length;
if (count === 0) return inspectionValueText();
return `${count} stored observation ${count === 1 ? 'time' : 'times'}. ${inspectionValueText()}`;
@ -264,16 +276,13 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
const bounds = createMemo(() => getGuestDrawerHistoryRangeBounds(series()));
const hasDrawableData = createMemo(() => drawableSeries().length > 0 && bounds() !== null);
const hoveredSeries = createMemo(() => {
const timestamp = inspectionTimestamp() ?? hoverTimestamp();
const timestamp = activeTimestamp();
const rangeBounds = bounds();
if (timestamp === null || !rangeBounds) return [];
return (inspectionTimestamp() === null ? drawableSeries() : series())
return series()
.map((item) => {
const point =
inspectionTimestamp() === null
? findClosestGuestDrawerHistoryPoint(item.points, timestamp)
: item.points.find((point) => point.timestamp === timestamp);
const point = item.points.find((point) => point.timestamp === timestamp);
if (!point) return null;
return {
...item,
@ -300,7 +309,7 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
...item,
valueLabel: hovered
? getGuestDrawerHistoryValueLabel([hovered.point], item.unit)
: inspectionTimestamp() !== null
: activeTimestamp() !== null
? '-'
: item.points.length > 0
? getGuestDrawerHistoryValueLabel(item.points, item.unit)
@ -311,8 +320,8 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
}),
);
const hoverTimeLabel = createMemo(() => {
const point = hoveredSeries()[0]?.point;
return point ? formatHistoryChartTimeLabel(point.timestamp, props.range) : '';
const timestamp = activeTimestamp();
return timestamp === null ? '' : formatHistoryChartTimeLabel(timestamp, props.range);
});
const handleHoverMove = (event: MouseEvent & { currentTarget: SVGSVGElement }) => {

View file

@ -0,0 +1,204 @@
import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ChartsAPI, type AllMetricsHistoryResponse, type HistoryTimeRange } from '@/api/charts';
import { HOST_METRICS_HISTORY_GROUPS } from '@/components/shared/hostMetricsHistoryModel';
import { formatHistoryChartTimeLabel } from '@/components/shared/historyChartModel';
import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
import { GuestDrawerHistory } from '../GuestDrawerHistory';
import type { GuestDrawerHistoryTarget } from '../guestDrawerModel';
vi.mock('@/stores/license', () => ({
loadRuntimeCapabilities: vi.fn(async () => undefined),
maxHistoryDays: () => 7,
isRangeLocked: (range: HistoryTimeRange) => ['14d', '30d', '90d'].includes(range),
}));
afterEach(() => {
cleanup();
resetCreateNonSuspendingQueryCacheForTest();
vi.restoreAllMocks();
vi.useRealTimers();
});
const time = 1_700_000_000_000;
const target: GuestDrawerHistoryTarget = { resourceType: 'agent', resourceId: 'pbs-host' };
const point = (offset: number, value: number) => ({
timestamp: time + offset,
value,
min: value,
max: value,
});
const response = (
metrics: AllMetricsHistoryResponse['metrics'] = {
cpu: [point(120_000, 43), point(0, 11), point(60_000, 22)],
memory: [point(30_000, 30), point(120_000, 55)],
disk: [point(45_000, 0)],
},
): AllMetricsHistoryResponse => ({
...target,
range: '24h',
start: time,
end: time + 120_000,
source: 'store',
metrics,
});
const utilization = () => screen.getAllByTestId('guest-history-group-chart')[0];
const hover = (chart: HTMLElement, offset: number, duration = 120_000) => {
const plot = within(chart).getByTestId('guest-history-plot');
vi.spyOn(plot, 'getBoundingClientRect').mockReturnValue({ left: 0, width: 360 } as DOMRect);
fireEvent.mouseMove(plot, { clientX: 34 + (offset / duration) * 318 });
return plot;
};
describe('GuestDrawerHistory common-time pointer inspection', () => {
it('chooses the nearest actual observation across all series, not the first metric', async () => {
const fetch = vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => <GuestDrawerHistory target={target} range="24h" />);
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
hover(utilization(), 30_000);
expect(within(utilization()).getByTestId('guest-history-hover-time')).toHaveTextContent(
formatHistoryChartTimeLabel(time + 30_000, '24h'),
);
expect(utilization()).toHaveTextContent('CPU-');
expect(utilization()).toHaveTextContent('Memory30.0%');
expect(utilization().querySelectorAll('circle[r="3"]')).toHaveLength(1);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('does not borrow neighbouring or current readings for a missing metric at the chosen time', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => (
<GuestDrawerHistory
target={target}
range="24h"
currentMetrics={{ cpu: 99, memory: 98, disk: 88 }}
/>
));
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
const plot = hover(utilization(), 0);
expect(utilization()).toHaveTextContent('CPU11.0%');
expect(utilization()).toHaveTextContent('Memory-');
expect(utilization()).toHaveTextContent('Disk-');
expect(utilization()).not.toHaveTextContent('98.0%');
expect(
within(utilization()).getByRole('img', { name: 'Utilization history' }),
).toHaveAccessibleDescription(
`${new Date(time).toLocaleString()}. CPU 11.0%. Memory no observation. Disk no observation.`,
);
fireEvent.pointerLeave(plot);
expect(utilization()).toHaveTextContent('CPU43.0%');
expect(utilization()).toHaveTextContent('Memory55.0%');
expect(within(utilization()).queryByTestId('guest-history-hover-time')).toBeNull();
});
it('can inspect a lone zero observation without drawing a synthetic trend', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => <GuestDrawerHistory target={target} range="24h" />);
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
hover(utilization(), 45_000);
expect(utilization()).toHaveTextContent('CPU-');
expect(utilization()).toHaveTextContent('Memory-');
expect(utilization()).toHaveTextContent('Disk0.0%');
expect(utilization().querySelectorAll('circle[r="3"]')).toHaveLength(1);
expect(utilization().querySelectorAll('path')).toHaveLength(2);
});
it('renders all metrics that really share a timestamp at the same cursor position', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => <GuestDrawerHistory target={target} range="24h" />);
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
hover(utilization(), 120_000);
expect(utilization()).toHaveTextContent('CPU43.0%');
expect(utilization()).toHaveTextContent('Memory55.0%');
expect(utilization()).toHaveTextContent('Disk-');
const markers = [...utilization().querySelectorAll('circle[r="3"]')];
expect(markers).toHaveLength(2);
expect(markers[0].getAttribute('cx')).toBe(markers[1].getAttribute('cx'));
});
it('snaps between observations and resolves a tie to the earlier actual time', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => <GuestDrawerHistory target={target} range="24h" />);
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
hover(utilization(), 37_500);
expect(utilization()).toHaveTextContent('CPU-');
expect(utilization()).toHaveTextContent('Memory30.0%');
expect(utilization()).toHaveTextContent('Disk-');
});
it('keeps keyboard selection authoritative and restores pointer inspection after blur', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response());
render(() => <GuestDrawerHistory target={target} range="24h" />);
const slider = await screen.findByRole('slider', { name: 'Inspect Utilization history' });
slider.focus();
fireEvent.input(slider, { target: { value: '0' } });
hover(utilization(), 30_000);
expect(utilization()).toHaveTextContent('CPU11.0%');
expect(utilization()).toHaveTextContent('Memory-');
fireEvent.blur(slider);
hover(utilization(), 30_000);
expect(utilization()).toHaveTextContent('CPU-');
expect(utilization()).toHaveTextContent('Memory30.0%');
});
it('reconciles a hovered time with a refreshed series instead of substituting every latest metric', async () => {
vi.useFakeTimers();
vi.spyOn(ChartsAPI, 'getMetricsHistory')
.mockResolvedValueOnce(response())
.mockResolvedValueOnce(response({ memory: [point(30_000, 31)], cpu: [point(120_000, 88)] }));
render(() => <GuestDrawerHistory target={target} range="24h" />);
await vi.advanceTimersByTimeAsync(0);
hover(utilization(), 30_000);
await vi.advanceTimersByTimeAsync(30_000);
expect(utilization()).toHaveTextContent('CPU-');
expect(utilization()).toHaveTextContent('Memory31.0%');
expect(utilization().querySelectorAll('path')).toHaveLength(0);
expect(utilization().querySelectorAll('circle[r="3"]')).toHaveLength(1);
});
it.each(['target', 'range'] as const)(
'clears pointer state on %s replacement',
async (change) => {
vi.spyOn(ChartsAPI, 'getMetricsHistory')
.mockResolvedValueOnce(response())
.mockImplementation(() => new Promise(() => {}));
const [current, setTarget] = createSignal(target);
const [range, setRange] = createSignal<HistoryTimeRange>('24h');
render(() => <GuestDrawerHistory target={current()} range={range()} />);
await screen.findByRole('slider', { name: 'Inspect Utilization history' });
hover(utilization(), 30_000);
if (change === 'target') setTarget({ ...target, resourceId: 'replacement' });
else setRange('6h');
await waitFor(() =>
expect(within(utilization()).queryByTestId('guest-history-hover-time')).toBeNull(),
);
expect(utilization().querySelectorAll('circle[r="3"]')).toHaveLength(0);
},
);
it('inspects real zero rates and temperatures while leaving missing sibling directions unavailable', async () => {
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(
response({
netin: [point(0, 0), point(120_000, 2048)],
netout: [point(30_000, 4096)],
temperature: [point(0, 42), point(120_000, 57)],
}),
);
render(() => (
<GuestDrawerHistory target={target} range="24h" groups={HOST_METRICS_HISTORY_GROUPS} />
));
await screen.findByRole('slider', { name: 'Inspect Network I/O history' });
const groups = screen.getAllByTestId('guest-history-group-chart');
const network = groups.find((group) => group.dataset.historyGroup === 'network')!;
hover(network, 0);
expect(network).toHaveTextContent('In0 B/s');
expect(network).toHaveTextContent('Out-');
hover(network, 30_000);
expect(network).toHaveTextContent('In-');
expect(network).toHaveTextContent('Out4.00 KB/s');
const thermals = groups.find((group) => group.dataset.historyGroup === 'thermals')!;
hover(thermals, 0);
expect(thermals).toHaveTextContent('CPU42°C');
});
});