From 92b784bef29bc879581bbfed5ed8776aec68cbc4 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sun, 18 Oct 2026 14:28:37 +0100 Subject: [PATCH] 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. --- .../troubleshootingEntryGuide.test.ts | 91 ++++++++++++++++--- .../utils/__tests__/backupSafetyHelp.test.ts | 17 ++-- .../installtests/agent_truenas_teardown.py | 11 ++- .../tests/test_auto_update_service_state.py | 9 +- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts b/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts index 5f203970eb..5eadaa9493 100644 --- a/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts +++ b/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts @@ -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); diff --git a/frontend-modern/src/utils/__tests__/backupSafetyHelp.test.ts b/frontend-modern/src/utils/__tests__/backupSafetyHelp.test.ts index 2e1fc795e4..5874bff11a 100644 --- a/frontend-modern/src/utils/__tests__/backupSafetyHelp.test.ts +++ b/frontend-modern/src/utils/__tests__/backupSafetyHelp.test.ts @@ -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 [ diff --git a/scripts/installtests/agent_truenas_teardown.py b/scripts/installtests/agent_truenas_teardown.py index 624ed9293a..1fdd179885 100644 --- a/scripts/installtests/agent_truenas_teardown.py +++ b/scripts/installtests/agent_truenas_teardown.py @@ -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) diff --git a/scripts/tests/test_auto_update_service_state.py b/scripts/tests/test_auto_update_service_state.py index 4726535aa5..d72c6d3359 100644 --- a/scripts/tests/test_auto_update_service_state.py +++ b/scripts/tests/test_auto_update_service_state.py @@ -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.