Keep stale agent memory out of current Web readings

Honour the server-owned stale signal only for the selected Pulse Agent observation. Preserve its original value and time as last known in rows, sorting, metric search and both History consumers, while independent Proxmox/QGA sources remain current. Add regression and connected browser fixtures without a client TTL, wire rewrite or guest operation.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-13 07:20:33 +01:00
parent 8d0682881a
commit 3e5cfc3df3
11 changed files with 716 additions and 7 deletions
@@ -20,6 +20,29 @@
## Purpose
### History honours explicit selected-source staleness (13 October 2026)
The shared memory presentation reads the server's explicit `agent.stale`
invalidation only when the selected observation is from `agent`. A dated
current carrier then becomes visibly last known, with its original source,
time and measured value, including zero. It does not supply current sorting,
metric-filter pressure or guest/canonical History fallbacks. The visible source
label explains that the supplying Pulse Agent stopped reporting. Missing or
invalid provenance remains unknown; unavailable telemetry stays unavailable.
QGA and Proxmox observations remain independent of a stale linked Pulse Agent
and of filesystem deferrals. No client TTL, source timestamp renewal, metadata
rewrite, API schema, History point or guest operation is added. The source
can restore current measured zero when it reports again. This is presentation
safety, not native restart/thaw recovery or release availability.
`staleAgentMemory.test.ts` connects qualification to both History models,
sorting and metric search, while `GuestRow.test.tsx` mounts same-ID source
updates in bars and sparklines. The static production-component browser
fixture `guest-memory-agent-stale.cjs` exercises agent-facet-only canonical
deltas through the actual Workloads projection, both drawers and trusted
keyboard/touch controls at desktop and phone widths.
### Patrol setup panel uses existing form primitives
`PatrolSetupPanel.tsx` builds the provider choice from `SelectionCardGroup`
@@ -5911,7 +5911,8 @@
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts",
"frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts",
"frontend-modern/src/components/Workloads/__tests__/nodeDrawerModel.branchcov0713.test.ts",
"frontend-modern/src/features/docker/__tests__/dockerHostDrawerModel.test.ts"
"frontend-modern/src/features/docker/__tests__/dockerHostDrawerModel.test.ts",
"frontend-modern/src/utils/__tests__/staleAgentMemory.test.ts"
]
},
{
@@ -0,0 +1,251 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const crypto = require('node:crypto');
const { chromium, webkit } = require('playwright');
const {
createPublicationFixtureServer,
} = require('/workspace/frontend-modern/browser-tests/publication-fixture-server.cjs');
const root = '/workspace/frontend-modern';
const output = '/workspace/tmp/guest-memory-agent-stale';
const origin = 'http://127.0.0.1:5344';
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const settle = (page) =>
page.evaluate(async () => {
await document.fonts.ready;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
(async () => {
fs.mkdirSync(output, { recursive: true });
const result = {
result: 'incomplete',
playwright: require('playwright/package.json').version,
cases: [],
captures: [],
cleanup: {},
driver_sha256: hash(root + '/browser-tests/guest-memory-agent-stale.cjs'),
limits:
'Real canonical delta merger, Workloads projection, row/bar/sparkline, guest drawer and canonical History with synthetic source-facet updates and empty History API fixtures. No collector, native restart, thaw, guest operation, installed or release acceptance.',
};
let server, browser;
try {
assert.equal(
result.playwright,
JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
'node_modules/@playwright/test'
].version,
);
server = createPublicationFixtureServer(
root,
5344,
'browser-tests/guest-memory-agent-stale.html',
);
result.compiled = server.binding;
await server.listen();
for (const [name, engine, width, dark] of [
['desktop', chromium, 1365, false],
['phone', webkit, 390, true],
]) {
browser = await engine.launch(
engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
const context = await browser.newContext({
viewport: { width, height: 900 },
isMobile: width < 768,
hasTouch: width < 768,
locale: 'en-GB',
timezoneId: 'UTC',
reducedMotion: 'reduce',
});
const page = await context.newPage();
page.setDefaultTimeout(10_000);
const record = {
name,
width,
browser: browser.version(),
checks: [],
requests: [],
errors: [],
};
result.cases.push(record);
page.on('pageerror', (error) => record.errors.push(error.message));
await page.route('**/*', (route) => {
const request = route.request(),
url = new URL(request.url());
assert.equal(url.origin, origin, 'external request refused');
if (!url.pathname.startsWith('/api/')) return route.continue();
record.requests.push({ path: url.pathname, method: request.method() });
assert.equal(request.method(), 'GET', 'no mutations');
assert(!/diagnostics|\/run|guest-agent|qemu/.test(url.pathname), 'no guest operations');
let body = { success: true, data: [], alerts: [], anomalies: [], config: null };
if (url.pathname.includes('/charts/'))
body = {
resourceType: 'vm',
resourceId: url.searchParams.get('resourceId') || 'fixture:pve1:101',
range: url.searchParams.get('range') || '24h',
start: 1791633600000,
end: 1791720000000,
source: 'store',
metrics: {},
};
if (url.pathname === '/api/license/runtime-capabilities')
body = {
capabilities: [],
limits: [],
max_history_days: 7,
hosted_mode: false,
runtime: { build: 'community' },
blocked_capabilities: [],
};
return route.fulfill({ json: body });
});
await page.addInitScript(
(dark) =>
document.addEventListener('DOMContentLoaded', () =>
document.documentElement.classList.toggle('dark', dark),
),
dark,
);
await page.goto(origin, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__staleAgentMemory?.evidence().projected);
const row = page.getByRole('region', { name: 'Workload row', exact: true });
const workload = page.getByRole('region', { name: 'Workload drawer', exact: true });
const canonical = page.getByRole('region', { name: 'Canonical drawer', exact: true });
const notice = row.locator('[data-workload-memory-read-status]');
assert.equal(await notice.count(), 0);
await page.evaluate(() => window.__staleAgentMemory.stale());
await notice.waitFor();
assert.match(await notice.innerText(), width < 768 ? /Prior/ : /Last known/);
assert.match(await notice.textContent(), /stopped reporting/);
await workload
.getByText(/Last known · Pulse Agent \(stopped reporting\)/)
.first()
.waitFor();
const evidence = await page.evaluate(() => window.__staleAgentMemory.evidence());
assert.equal(evidence.projected.agentStale, true);
assert.equal(
evidence.projected.memory.observation.state,
'current',
'wire carrier not rewritten',
);
assert.equal(evidence.projected.memory.observation.observedAt, '2026-10-10T12:00:00Z');
assert.equal(evidence.filterHigh, false);
assert.deepEqual(evidence.sort, ['independent', 'fixture:pve1:101']);
record.source_facet = evidence;
record.checks.push(
'agent-only canonical delta invalidates its current carrier; original source/time and value survive; filter/sort exclude it',
);
const capture = async (label, locator) => {
await settle(page);
const file = name + '-' + label + '.png';
await locator.screenshot({ path: output + '/' + file });
result.captures.push({ file, sha256: hash(output + '/' + file) });
};
await capture('stale-overview', workload);
await page.getByLabel('Metric display').selectOption('sparklines');
await row
.getByRole('img', { name: 'linked-guest memory history, last known 95%', exact: true })
.waitFor();
await page.getByLabel('Memory basis').selectOption('host');
await row
.getByRole('img', {
name: 'linked-guest host memory share history, last known 48%',
exact: true,
})
.waitFor();
await capture('stale-row', row);
record.checks.push(
'real display/basis controls preserve retained bar/sparkline accessibility',
);
const historyTab = async (surface) => {
const tab = surface.getByRole('tab', { name: 'History', exact: true });
if (width < 768) await tab.tap();
else {
await tab.focus();
await page.keyboard.press('Enter');
}
const chart = surface.locator('[data-history-group="utilization"]');
await chart.locator('[data-history-last-known="memory"]').waitFor();
assert.equal(await chart.locator('[data-history-current="memory"]').count(), 0);
assert.match(
await chart.locator('[data-history-last-known="memory"]').innerText(),
/95\.0%.*last known/s,
);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/stopped reporting/,
);
assert.match(
await chart.locator('[data-history-deferred="memory"]').innerText(),
/2026-10-10 12:00:00 UTC/,
);
return chart;
};
const guestChart = await historyTab(workload);
const resourceChart = await historyTab(canonical);
await capture('stale-history', canonical);
record.checks.push(
'keyboard/touch History in both real drawers retains the original value, not a current fallback or invented trend',
);
for (const source of ['guest-agent-meminfo', 'status-mem']) {
await page.evaluate((source) => window.__staleAgentMemory.reading(source, 10), source);
await guestChart.locator('[data-history-current="memory"]').waitFor();
await resourceChart.locator('[data-history-current="memory"]').waitFor();
assert.equal(await guestChart.locator('[data-history-last-known="memory"]').count(), 0);
assert.equal(await resourceChart.locator('[data-history-last-known="memory"]').count(), 0);
assert.equal(
(await page.evaluate(() => window.__staleAgentMemory.evidence())).projected.agentStale,
true,
);
}
record.checks.push(
'unrelated QGA/Proxmox readings stay current while linked agent remains stale',
);
await page.evaluate(() => window.__staleAgentMemory.reading('agent', 0));
await guestChart.locator('[data-history-last-known="memory"]').waitFor();
assert.match(
await guestChart.locator('[data-history-last-known="memory"]').innerText(),
/0\.0%.*last known/s,
);
await page.evaluate(() => window.__staleAgentMemory.recover());
await guestChart.locator('[data-history-current="memory"]').waitFor();
await resourceChart.locator('[data-history-current="memory"]').waitFor();
assert.match(
await guestChart.locator('[data-history-current="memory"]').innerText(),
/0\.0%.*current/s,
);
assert.match(
await resourceChart.locator('[data-history-current="memory"]').innerText(),
/0\.0%.*current/s,
);
assert.equal(await notice.count(), 0);
await capture('reporting-zero', canonical);
record.checks.push(
'same-ID reporting source restores measured zero in both drawers; stale zero was not missing or healthy current',
);
assert.deepEqual(record.errors, []);
const layout = await page.evaluate(() => ({
client: document.documentElement.clientWidth,
scroll: document.documentElement.scrollWidth,
}));
assert(layout.scroll <= layout.client + 1, JSON.stringify(layout));
record.layout = layout;
await context.close();
await browser.close();
browser = null;
}
result.result = 'passed';
result.verified_at = new Date().toISOString();
} catch (error) {
result.error = String(error.stack || error);
throw error;
} finally {
if (browser) await browser.close();
result.cleanup.browser = true;
if (server) await server.close();
result.cleanup.server = true;
fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
}
})();
@@ -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>Source-owned agent memory fixture</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/browser-tests/guest-memory-agent-stale.tsx"></script>
</body>
</html>
@@ -0,0 +1,207 @@
// A source-facet-only update through the real canonical merger and Workloads
// projection. No guest operations, native restart or recovery are simulated.
import { createSignal, Show } from 'solid-js';
import { render } from 'solid-js/web';
import { Router, Route } from '@solidjs/router';
import { DarkModeContext } from '../src/contexts/appRuntime';
import { useWorkloads } from '../src/hooks/useWorkloads';
import { mergeCanonicalResourceDeltaSnapshot } from '../src/utils/resourceStateAdapters';
import { GuestRow } from '../src/components/Workloads/GuestRow';
import { GuestDrawer } from '../src/components/Workloads/GuestDrawer';
import { ResourceDetailDrawer } from '../src/components/Infrastructure/ResourceDetailDrawer';
import { getWorkloadTableLayoutModeForContainer } from '../src/components/Workloads/guestRowModel';
import { createWorkloadSortComparator } from '../src/components/Workloads/workloadSelectors';
import { evaluateFilterStack, parseFilterStack } from '../src/utils/searchQuery';
import type { Resource } from '../src/types/resource';
import type { MemoryObservation } from '../src/types/api';
import '../src/index.css';
const capacity = 4 * 1024 ** 3;
const observedAt = '2026-10-10T12:00:00Z';
const initial: Resource = {
id: 'fixture:pve1:101',
type: 'vm',
name: 'linked-guest',
status: 'online',
platformType: 'proxmox-pve',
platformScopes: ['proxmox-pve'],
sources: ['proxmox', 'agent'],
metricsTarget: { resourceType: 'vm', resourceId: 'fixture:pve1:101' },
lastSeen: Date.parse(observedAt),
cpu: { current: 10 },
memory: {
current: 95,
used: capacity * 0.95,
total: capacity,
observation: { state: 'current', source: 'agent', observedAt },
},
disk: { current: 50, used: capacity / 2, total: capacity },
proxmox: {
vmid: 101,
nodeName: 'pve1',
instance: 'fixture',
runtimeStatus: 'running',
guestAgentStatus: 'deferred',
diskStatusReason: 'prev-vm-locked',
lock: 'backup',
},
agent: { stale: false, version: 'fixture-version', lastReportAt: observedAt },
};
// Only the transport is synthetic; the canonical merger, mapper and mounted
// presentation use production code. No websocket connection is attempted.
window.__pulseWsStore = {
state: { resources: [], activeAlerts: [], recentlyResolved: [] },
connected: () => false,
initialDataReceived: () => true,
resourceSnapshotReceived: () => false,
resourceChange: () => ({ version: 0, changedIds: null, changedKeys: null }),
changedResourceMetaSince: () => null,
changedResourceIdsSince: () => null,
shutdown: () => {},
} as unknown as NonNullable<typeof window.__pulseWsStore>;
function Fixture() {
const [snapshot, setSnapshot] = createSignal<Resource[]>([initial]);
const workloads = useWorkloads(() => true, { resourceSnapshot: snapshot });
const guest = () => workloads.workloads()[0];
const [mode, setMode] = createSignal<'bars' | 'sparklines'>('bars');
const [basis, setBasis] = createSignal<'guest' | 'host'>('guest');
const layout = getWorkloadTableLayoutModeForContainer(Math.min(window.innerWidth, 896) - 40);
const update = (patch: Partial<Resource>, keys: (keyof Resource)[]) =>
setSnapshot((previous) =>
mergeCanonicalResourceDeltaSnapshot(
[{ id: initial.id, ...patch } as Resource],
previous,
new Set([initial.id]),
new Map([[initial.id, keys]]),
),
);
(window as any).__staleAgentMemory = {
stale: () =>
update(
{
agent: { ...snapshot()[0].agent, stale: true },
lastSeen: Date.parse('2026-10-11T12:00:00Z'),
},
['agent', 'lastSeen'],
),
reading: (source: string, usage: number, state = 'current') =>
update(
{
memory: {
current: usage,
used: (capacity * usage) / 100,
total: capacity,
observation: { state, source, observedAt } as MemoryObservation,
},
},
['memory'],
),
recover: () =>
update(
{
agent: { ...snapshot()[0].agent, stale: false, lastReportAt: '2026-10-11T12:00:00Z' },
memory: {
current: 0,
used: 0,
total: capacity,
observation: { state: 'current', source: 'agent', observedAt: '2026-10-11T12:00:00Z' },
},
},
['agent', 'memory'],
),
evidence: () => ({
canonical: snapshot()[0],
projected: guest(),
filterHigh: evaluateFilterStack(guest(), parseFilterStack('memory>90')),
sort: [
guest(),
{
...guest(),
id: 'independent',
memory: {
...guest().memory,
usage: 10,
observation: { state: 'current', source: 'guest-agent-meminfo', observedAt },
},
},
]
.sort(createWorkloadSortComparator('memory', 'desc')!)
.map((value) => value.id),
}),
};
return (
<main class="mx-auto min-h-screen max-w-4xl space-y-3 bg-surface p-3 text-base-content">
<h1 class="text-base font-semibold">Source-owned memory evidence</h1>
<p class="text-xs text-muted">Synthetic source updates. No guest checks are sent.</p>
<div class="flex flex-wrap gap-3 text-xs">
<label>
Display{' '}
<select
aria-label="Metric display"
class="border border-border bg-surface p-1"
onChange={(event) => setMode(event.currentTarget.value as 'bars' | 'sparklines')}
>
<option value="bars">Bars</option>
<option value="sparklines">Sparklines</option>
</select>
</label>
<label>
Memory basis{' '}
<select
aria-label="Memory basis"
class="border border-border bg-surface p-1"
onChange={(event) => setBasis(event.currentTarget.value as 'guest' | 'host')}
>
<option value="guest">Guest allocation</option>
<option value="host">Host capacity</option>
</select>
</label>
</div>
<Show when={guest()}>
<section
aria-label="Workload row"
class="rounded-sm border border-border bg-surface-raised p-2"
>
<table class="w-full table-fixed">
<tbody>
<GuestRow
guest={guest()}
visibleColumnIds={['name', 'memory']}
workloadTableLayoutMode={layout}
metricDisplayMode={mode()}
memoryDisplayBasis={basis()}
parentMemoryTotal={capacity * 2}
onClick={() => {}}
/>
</tbody>
</table>
</section>
<section aria-label="Workload drawer">
<GuestDrawer guest={guest()} onClose={() => {}} />
</section>
<section aria-label="Canonical drawer">
<ResourceDetailDrawer resource={snapshot()[0]} presentation="table-row" />
</section>
</Show>
</main>
);
}
render(
() => (
<Router>
<Route
path="*"
component={() => (
<DarkModeContext.Provider
value={() => document.documentElement.classList.contains('dark')}
>
<Fixture />
</DarkModeContext.Provider>
)}
/>
</Router>
),
document.getElementById('root')!,
);
@@ -11,6 +11,8 @@ const runtimePaths = [
'src/components/Workloads/GuestRow.tsx',
'src/components/Workloads/MetricMiniSparkline.tsx',
'src/components/Workloads/guestDrawerModel.ts',
'src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts',
'src/utils/memoryObservation.ts',
'src/features/patrol/PatrolAttentionWorkbench.tsx',
];
const entryPaths = [
@@ -18,6 +20,7 @@ const entryPaths = [
'browser-tests/guest-disk-provenance.html',
'browser-tests/guest-reading-help.html',
'browser-tests/guest-row-memory-provenance.html',
'browser-tests/guest-memory-agent-stale.html',
'browser-tests/patrol-rule-removal.html',
];
const harnessPaths = [
@@ -28,6 +31,7 @@ const harnessPaths = [
'browser-tests/guest-disk-provenance.cjs',
'browser-tests/guest-reading-help.cjs',
'browser-tests/guest-row-memory-provenance.cjs',
'browser-tests/guest-memory-agent-stale.cjs',
'browser-tests/patrol-rule-removal.cjs',
'browser-tests/patrol-rule-removal-final.cjs',
];
@@ -171,6 +171,7 @@ const memoryReading = (resource: Resource) =>
resource.memory ? finiteMetric(getMemoryPercent(resource)) : undefined,
resource.memory?.observation,
isProxmoxGuest(resource),
{ agentStale: resource.agent?.stale === true },
);
const guestDiskReading = (resource: Resource) => {
@@ -1219,6 +1219,49 @@ describe('GuestRow', () => {
const observation = (state: string, date = observedAt, source = 'guest-agent-meminfo') =>
makeMemory({ observation: { state, source, observedAt: date } });
const noticeSelector = '[data-workload-memory-read-status]';
it.each(['bars', 'sparklines'] as const)(
'invalidates only a stale agent-owned carrier in the mounted %s row',
(mode) => {
const [guest, setGuest] = createSignal(
makeGuest({ memory: observation('current', observedAt, 'agent') }),
);
const view = render(() => (
<table>
<tbody>
<GuestRow
guest={guest()}
visibleColumnIds={['name', 'memory']}
metricDisplayMode={mode}
/>
</tbody>
</table>
));
expect(view.container.querySelector(noticeSelector)).toBeNull();
setGuest({ ...guest(), agentStale: true, lastSeen: '2026-10-01T12:00:00Z' });
const notice = view.container.querySelector(noticeSelector)!;
expect(notice).toHaveTextContent('Last known');
expect(notice).toHaveTextContent('stopped reporting');
expect(notice).toHaveTextContent('2026-09-30 11:00:00 UTC');
expect(notice).not.toHaveTextContent('2026-10-01');
if (mode === 'bars')
expect(screen.getByTestId('memory-bar')).toHaveAttribute(
'data-reading-state',
'last-known',
);
else
expect(
screen.getByRole('img', { name: 'test-vm memory history, last known 50%' }),
).toBeInTheDocument();
setGuest({ ...guest(), memory: observation('current') });
expect(view.container.querySelector(noticeSelector)).toBeNull();
setGuest({
...guest(),
agentStale: false,
memory: observation('current', observedAt, 'agent'),
});
expect(view.container.querySelector(noticeSelector)).toBeNull();
},
);
const history = {
getGuestMetricSeries: vi.fn(() => [
{
@@ -255,7 +255,7 @@ describe('current workload memory selection', () => {
expect(iso).not.toHaveBeenCalled();
});
it.each(['guest-agent-meminfo', 'agent', 'status-mem'])(
it.each(['guest-agent-meminfo', 'status-mem'])(
'selects current %s memory independently of a QEMU disk deferral',
(source) => {
const input = current({
@@ -268,6 +268,12 @@ describe('current workload memory selection', () => {
},
);
it('selects a reporting agent independently of a QEMU disk deferral', () => {
const input = current({ diskStatusReason: 'prev-vm-locked', lock: 'backup' });
input.memory.observation!.source = 'agent';
expect(getCurrentWorkloadMemoryUsage(input)).toBe(25);
});
it('cannot use current metadata to revive unavailable telemetry', () => {
const input = current();
input.memory.usageUnavailable = true;
@@ -0,0 +1,148 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import type { WorkloadGuest } from '@/types/workloads';
import {
getCurrentWorkloadMemoryUsage,
getWorkloadMemoryObservationPresentation,
} from '@/utils/memoryObservation';
import { evaluateFilterStack, parseFilterStack } from '@/utils/searchQuery';
import { createWorkloadSortComparator } from '@/components/Workloads/workloadSelectors';
import {
getGuestDrawerCurrentMetrics,
getGuestDrawerDeferredMetrics,
} from '@/components/Workloads/guestDrawerModel';
import {
getResourceMetricsHistoryCurrentMetrics,
getResourceMetricsHistoryDeferredMetrics,
} from '@/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel';
const observedAt = '2026-10-10T12:00:00Z';
const guest = (source = 'agent', state = 'current', usage = 95): WorkloadGuest =>
({
id: 'fixture:pve:101',
name: 'linked-guest',
type: 'qemu',
status: 'running',
vmid: 101,
node: 'pve',
instance: 'fixture',
platformScopes: ['proxmox-pve'],
agentStale: true,
// Another producer can renew this without a new agent report.
lastSeen: '2026-10-11T12:00:00Z',
memory: {
total: 100,
used: usage,
free: 100 - usage,
usage,
observation: { state, source, observedAt },
},
}) as WorkloadGuest;
const resource = (input: WorkloadGuest): Resource => ({
id: input.id,
name: input.name,
type: 'vm',
status: 'online',
platformType: 'proxmox-pve',
lastSeen: Date.parse(input.lastSeen!),
agent: { stale: input.agentStale },
memory: { current: input.memory.usage, observation: input.memory.observation },
});
describe('source-owned stale agent memory', () => {
it.each([0, 95])(
'retains a value (%s) without asserting a current agent measurement',
(usage) => {
const input = guest('agent', 'current', usage);
const original = structuredClone(input);
const reading = getWorkloadMemoryObservationPresentation(input, { includeCurrent: false });
expect(reading?.state).toBe('last-known');
expect(reading?.summary).toContain('Last known · Pulse Agent (stopped reporting)');
expect(reading?.summary).toContain('2026-10-10 12:00:00 UTC');
expect(reading?.message).toContain('stopped reporting');
expect(reading?.message).not.toContain('2026-10-11');
expect(getCurrentWorkloadMemoryUsage(input)).toBeNull();
expect(input).toEqual(original);
},
);
it.each(['guest-agent-meminfo', 'guest-agent-meminfo-derived', 'status-mem', 'available-field'])(
'does not downgrade independent current %s memory when the linked agent is stale',
(source) => {
const input = guest(source);
expect(getCurrentWorkloadMemoryUsage(input)).toBe(95);
expect(getWorkloadMemoryObservationPresentation(input)?.state).toBe('current');
expect(getWorkloadMemoryObservationPresentation(input)?.message).not.toContain(
'stopped reporting',
);
expect(getResourceMetricsHistoryCurrentMetrics(resource(input)).memory).toBe(95);
},
);
it('never supplies missing provenance or revives unavailable telemetry', () => {
for (const state of ['unavailable', 'future-state']) {
const input = guest('agent', state);
expect(getCurrentWorkloadMemoryUsage(input)).toBeNull();
expect(getWorkloadMemoryObservationPresentation(input)?.state).toBe(
state === 'unavailable' ? 'unavailable' : 'unknown',
);
}
for (const time of [undefined, 'invalid', '2099-01-01T00:00:00Z']) {
const input = guest();
input.memory.observation!.observedAt = time;
expect(getCurrentWorkloadMemoryUsage(input)).toBeNull();
expect(getWorkloadMemoryObservationPresentation(input)?.state).toBe('unknown');
}
const input = guest();
input.memory.usageUnavailable = true;
expect(getWorkloadMemoryObservationPresentation(input)?.state).toBe('unavailable');
});
it('does not rank or filter a retained high reading as current pressure', () => {
const stale = guest();
const current = { ...guest('guest-agent-meminfo', 'current', 10), id: 'current' };
for (const direction of ['asc', 'desc'] as const) {
expect([stale, current].sort(createWorkloadSortComparator('memory', direction)!)).toEqual([
current,
stale,
]);
}
for (const query of ['memory>90', 'memory<100', 'memory=95']) {
expect(evaluateFilterStack(stale, parseFilterStack(query))).toBe(false);
}
});
it.each([0, 95])(
'keeps the value (%s) as original last-known evidence in both History consumers',
(usage) => {
const input = guest('agent', 'current', usage);
const canonical = resource(input);
expect(getGuestDrawerCurrentMetrics(input).memory).toBeUndefined();
expect(getResourceMetricsHistoryCurrentMetrics(canonical).memory).toBeUndefined();
for (const deferred of [
getGuestDrawerDeferredMetrics(input),
getResourceMetricsHistoryDeferredMetrics(canonical),
]) {
expect(deferred.memory.lastKnownValue).toBe(usage);
expect(deferred.memory.message).toContain('2026-10-10 12:00:00 UTC');
expect(deferred.memory.message).toContain('stopped reporting');
}
},
);
it('restores current measured zero only when its source reports again', () => {
const input = guest('agent', 'current', 0);
input.agentStale = false;
input.memory.observation!.observedAt = '2026-10-11T12:00:00Z';
expect(getCurrentWorkloadMemoryUsage(input)).toBe(0);
expect(getWorkloadMemoryObservationPresentation(input)?.state).toBe('current');
expect(getWorkloadMemoryObservationPresentation(input)?.message).not.toContain(
'stopped reporting',
);
expect(getGuestDrawerCurrentMetrics(input).memory).toBe(0);
expect(getResourceMetricsHistoryCurrentMetrics(resource(input)).memory).toBe(0);
expect(getGuestDrawerDeferredMetrics(input)).not.toHaveProperty('memory');
expect(getResourceMetricsHistoryDeferredMetrics(resource(input))).not.toHaveProperty('memory');
});
});
+18 -5
View File
@@ -75,13 +75,16 @@ const memoryObservationState = (
value: number | undefined,
observation: MemoryObservation | undefined,
timestamp: number | null,
agentStale = false,
): MemoryObservationPresentation['state'] =>
!isMemoryPercent(value) || observation?.state === 'unavailable'
? 'unavailable'
: observation?.state === 'last-known'
? 'last-known'
: observation?.state === 'current' && timestamp !== null
? 'current'
? agentStale && observation.source === 'agent'
? 'last-known'
: 'current'
: 'unknown';
const requiresWorkloadMemoryObservation = (guest: WorkloadGuest): boolean => {
@@ -116,6 +119,7 @@ export const getCurrentWorkloadMemoryUsage = (guest: WorkloadGuest): number | nu
memory.usage,
memory.observation,
memoryObservationTime(memory.observation),
guest.agentStale === true,
) !== 'current'
) {
return null;
@@ -142,13 +146,17 @@ export const getMemoryObservationPresentation = (
value: number | undefined,
observation: MemoryObservation | undefined,
requiresObservation = false,
options: { includeCurrent?: boolean; guestKind?: MemoryGuestKind } = {},
options: { includeCurrent?: boolean; guestKind?: MemoryGuestKind; agentStale?: boolean } = {},
): MemoryObservationPresentation | null => {
// Unannotated unrelated platforms retain their existing behaviour. Legacy
// Proxmox readings lack authority to assert current freshness.
if (!requiresObservation && !observation) return null;
const timestamp = memoryObservationTime(observation);
const state = memoryObservationState(value, observation, timestamp);
// An agent-facet update can arrive without replacing the selected memory
// carrier. Honour the server's explicit source invalidation, but only for
// that agent's own reading: QGA and Proxmox remain independent. Do not add a
// client lease, renew the observation time or mutate its wire metadata.
const state = memoryObservationState(value, observation, timestamp, options.agentStale);
// An unavailable reading shows no number, so there is nothing to qualify.
const mayIncludeCache =
state !== 'unavailable' &&
@@ -180,10 +188,14 @@ export const getMemoryObservationPresentation = (
: 'Unknown source';
const time = observed ? `Observed: ${observed}.` : 'Observation time unknown.';
const cacheNote = mayIncludeCache ? getCacheNote(options.guestKind) : undefined;
const sourceStatus =
state !== 'unavailable' && options.agentStale && observation?.source === 'agent'
? ' The Pulse Agent that supplied this reading stopped reporting.'
: '';
return {
state,
summary: `${label} · ${source}${mayIncludeCache ? ' (may include cache)' : ''}${observed ? ` · ${observed}` : ' · time unknown'}`,
message: `${label}. Source: ${source}. ${time}${state === 'last-known' || state === 'unknown' ? ' Not a current measurement.' : ''}${cacheNote ? ` ${cacheNote}` : ''}`,
summary: `${label} · ${source}${sourceStatus ? ' (stopped reporting)' : ''}${mayIncludeCache ? ' (may include cache)' : ''}${observed ? ` · ${observed}` : ' · time unknown'}`,
message: `${label}. Source: ${source}. ${time}${state === 'last-known' || state === 'unknown' ? ' Not a current measurement.' : ''}${sourceStatus}${cacheNote ? ` ${cacheNote}` : ''}`,
mayIncludeCache,
...(cacheNote ? { cacheNote } : {}),
};
@@ -216,6 +228,7 @@ export const getWorkloadMemoryObservationPresentation = (
{
...options,
guestKind: getWorkloadMemoryGuestKind(guest),
agentStale: guest.agentStale === true,
},
);
};