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.