Keep reliability checks tied to workflows and selected tools

Require the existing troubleshooting jobs and their destinations without
making additional help entries break unrelated safety checks. Keep the
opening backup warning scoped to its actual entry point.

Run closed service and middleware models through the containing fixture's
selected Python interpreter without site startup. Retain every safety
scenario and deadline while qualifying the pending maintenance source.

Refs #2888

Contract-Neutral: Test-only navigation assertions and confined model interpreter selection. Production behavior and safety deadlines are unchanged.
This commit is contained in:
courtmanr@gmail.com
2026-10-18 14:28:37 +01:00
parent 98817ebce4
commit 92b784bef2
4 changed files with 104 additions and 24 deletions
@@ -39,12 +39,77 @@ function entry(): HTMLElement {
const prose = (element: HTMLElement): string =>
(element.textContent ?? '').replace(/\s+/g, ' ').trim();
const workflow = (section: HTMLElement, label: string): HTMLElement => {
const matches = [...section.querySelectorAll('li')].filter(
(row) => row.querySelector('strong')?.textContent === label,
);
expect(matches, label).toHaveLength(1);
return matches[0];
};
describe('troubleshooting workflow entry', () => {
it('puts guest incident safety before the symptom links and any collection recipe', () => {
const section = entry();
const link = section.querySelector('a');
expect(link?.getAttribute('href')).toBe('/docs/VM_DISK_MONITORING#backup-safety');
expect(section.querySelectorAll('li')).toHaveLength(9);
// New, independently verified workflows must not break unrelated safety
// checks. Require every existing job once, without relying on list offsets.
const requiredWorkflows: [string, string[]][] = [
[
'Cannot sign in:',
[
'/docs/TROUBLESHOOTING#i-forgot-my-password',
'/docs/TROUBLESHOOTING#cannot-login--401-unauthorized',
],
],
[
'Update failed or its result is uncertain:',
[
'/docs/AUTO_UPDATE#update-failed',
'/docs/DOCKER#check-a-failed-or-pending-workload-update',
],
],
[
'Agent host is missing or reporting is rejected:',
['/docs/UNIFIED_AGENT#agent-lookup-returns-http-401-or-403'],
],
[
'Guest readings are missing or disagree:',
[
'/docs/VM_DISK_MONITORING#current-retained-and-unavailable-disk-readings',
'/docs/TROUBLESHOOTING#guest-memory-is-high-but-available-memory-is-plentiful',
],
],
[
'Proxmox data belongs to the wrong host, or healthy jobs look Pending:',
[
'/docs/TROUBLESHOOTING#monitoring-is-mixed-between-proxmox-installations',
'/docs/TROUBLESHOOTING#replication-jobs-are-pending-stale-or-missing',
],
],
['TrueNAS readings are stale or absent:', ['/docs/TRUENAS#stale-truenas-data']],
[
'Pulse itself uses more memory, CPU or disk writes:',
[
'/docs/TROUBLESHOOTING#memory-use-keeps-growing',
'/docs/TROUBLESHOOTING#excessive-cpu-writes-or-database-growth',
],
],
[
'Notification Test works but alerts do not arrive:',
['/docs/TROUBLESHOOTING#test-succeeds-but-real-alerts-are-missing'],
],
[
'Private information was posted:',
['/docs/TROUBLESHOOTING#if-you-already-posted-private-information'],
],
];
for (const [label, expectedLinks] of requiredWorkflows) {
const actual = [...workflow(section, label).querySelectorAll('a[href]')].map((link) =>
link.getAttribute('href'),
);
expect(actual, label).toEqual(expect.arrayContaining(expectedLinks));
}
expect(section.querySelector('pre')).toBeNull();
const beforeList = [...section.children]
.slice(
@@ -62,30 +127,28 @@ describe('troubleshooting workflow entry', () => {
it('keeps server updates, workload updates and reporting acceptance distinct', () => {
const section = entry();
const rows = section.querySelectorAll('li');
expect(prose(rows[1] as HTMLElement)).toContain(
'An update of a monitored container is a different job',
);
const updateLinks = [...rows[1].querySelectorAll('a')].map((link) => link.getAttribute('href'));
const update = workflow(section, 'Update failed or its result is uncertain:');
expect(prose(update)).toContain('An update of a monitored container is a different job');
const updateLinks = [...update.querySelectorAll('a')].map((link) => link.getAttribute('href'));
expect(updateLinks).toEqual([
'/docs/AUTO_UPDATE#update-failed',
'/docs/DOCKER#check-a-failed-or-pending-workload-update',
]);
expect(prose(rows[1] as HTMLElement)).toContain(
'Preserve the current state and rollback evidence',
expect(prose(update)).toContain('Preserve the current state and rollback evidence');
expect(prose(workflow(section, 'Agent host is missing or reporting is rejected:'))).toContain(
'Installer success is not an accepted report',
);
expect(prose(rows[2] as HTMLElement)).toContain('Installer success is not an accepted report');
expect(prose(rows[3] as HTMLElement)).toContain(
expect(prose(workflow(section, 'Guest readings are missing or disagree:'))).toContain(
'a working Pulse Agent view does not prove API recovery',
);
expect(prose(rows[6] as HTMLElement)).toContain(
'Guest usage and database size are different measurements',
);
expect(
prose(workflow(section, 'Pulse itself uses more memory, CPU or disk writes:')),
).toContain('Guest usage and database size are different measurements');
});
it('routes every entry to an existing shipped heading in the production viewer', () => {
const links = [...entry().querySelectorAll('a[href]')];
expect(links).toHaveLength(16);
expect(links.length).toBeGreaterThan(0);
for (const link of links) {
const href = link.getAttribute('href')!;
const match = /^\/docs\/([^#]+)#(.+)$/.exec(href);
@@ -136,15 +136,16 @@ describe('manual backup safety help', () => {
expect(read('frontend-modern/public/docs/VM_DISK_MONITORING.md')).toBe(guide);
const troubleshooting = read('docs/TROUBLESHOOTING.md');
expect(read('frontend-modern/public/docs/TROUBLESHOOTING.md')).toBe(troubleshooting);
// The symptom-first navigation also links to this precaution. Require both
// entry points without rejecting other symptom-specific safety links.
const links = render(troubleshooting, 'TROUBLESHOOTING').querySelectorAll(
'a[href="/docs/VM_DISK_MONITORING#backup-safety"]',
);
// The opening precaution must precede the workflow list. Other symptoms
// can also link to backup safety, so document-wide label counts do not
// establish whether this entry point is present or correctly placed.
const article = render(troubleshooting, 'TROUBLESHOOTING');
const openingLink = article.querySelector('h2 + p + p a');
expect(openingLink?.textContent).toBe('Backup safety');
expect(openingLink?.getAttribute('href')).toBe('/docs/VM_DISK_MONITORING#backup-safety');
const links = article.querySelectorAll('a[href="/docs/VM_DISK_MONITORING#backup-safety"]');
const labels = [...links].map((link) => link.textContent);
for (const label of ['Backup safety', 'manual backup precaution']) {
expect(labels.filter((text) => text === label)).toHaveLength(1);
}
expect(labels.filter((text) => text === 'manual backup precaution')).toHaveLength(1);
const target = render(guide, 'VM_DISK_MONITORING');
expect(target.querySelector('#backup-safety')?.textContent).toBe('Backup safety');
for (const fragment of [
+10 -1
View File
@@ -11,6 +11,7 @@ from pathlib import Path
import shlex
import stat
import subprocess
import sys
import tempfile
import unittest
@@ -147,8 +148,16 @@ class AgentTrueNASTeardown(unittest.TestCase):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
tools = root / "tools"; tools.mkdir()
models = root / "models"; models.mkdir()
for name in ("midclt", "ps", "systemctl"):
file = tools / name; file.write_text(MODEL); file.chmod(0o700)
model = models / name; model.write_text(MODEL)
# These closed stdlib models share the interpreter selected by
# the containing suite. The host's /usr/bin/python3 may differ
# and pays its site startup cost on every observed operation.
file = tools / name
file.write_text("#!/bin/sh\nexec " + shlex.quote(sys.executable)
+ " -I -S " + shlex.quote(str(model)) + ' "$@"\n')
file.chmod(0o700)
if fault == "missing-midclt": (tools / "midclt").unlink()
for name in ("state", "helper", "profile", "unit"):
directory = root / name; directory.mkdir(mode=0o700)
@@ -10,6 +10,7 @@ import json
import os
from pathlib import Path
import shutil
import shlex
import subprocess
import sys
import tempfile
@@ -180,7 +181,13 @@ class UpdaterFixture(unittest.TestCase):
f = Path(directory)
for name in ("tools", "tmp", "install/bin", "config"):
(f / name).mkdir(parents=True)
(f / "tools/systemctl").write_text(MANAGER)
(f / "manager.py").write_text(MANAGER)
# Use the suite's selected interpreter, without site startup, for
# this stdlib-only model. Re-resolving python3 for every manager
# observation can select a different, slower host installation.
(f / "tools/systemctl").write_text(
"#!/bin/sh\nexec " + shlex.quote(sys.executable) + " -I -S "
+ shlex.quote(str(f / "manager.py")) + ' "$@"\n')
(f / "tools/systemctl").chmod(0o755)
# The production log() remains intact in real-log controls. Only
# its external sink is confined; no host journal is contacted.