Files
pulse/scripts/tests/test_faq_deployment_docs.py
pulse-triage[bot] 3c8fb29af8 Explain the boundary between alert Off and Patrol findings
The FAQ promised every metric could be disabled globally or per resource with -1. Clarify supported editor controls, overriding thresholds, unchanged collection, queued notifications and the existing Patrol fallback. Keep ordinary notification help reachable without generating new alerts.

Contract-Neutral: Existing help and its source/rendering/browser checks only; no alert, notification, Patrol, settings or API behaviour changes.
Change-source: pulse-maintainer
2026-10-11 19:03:47 +01:00

111 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Bind the short deployment answers to source and the safer detailed paths.
Documentation checks, not an installed port change or environment cutover.
"""
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
def faq():
return (ROOT / "docs/FAQ.md").read_text(encoding="utf-8")
def answer(heading):
return " ".join(faq().split(f"### {heading}\n", 1)[1].split("\n### ", 1)[0].split())
class FAQDeploymentDocsTest(unittest.TestCase):
def test_alert_off_is_scoped_and_does_not_promise_universal_overrides(self):
text = answer("Can I disable alerts for specific metrics?")
for phrase in ("controls offered in **Alerts → Thresholds**", "save, then reopen",
"not every default has a per-resource control",
"rather than treating `-1` as a universal value",
"does not hide its readings, stop collection",
"Check both the default and any override",
"explicit threshold can remain active",
"warning and critical levels separately"):
with self.subTest(phrase=phrase):
self.assertIn(phrase, text)
runtime = (ROOT / "internal/alerts/config_runtime.go").read_text()
self.assertIn("if override.Memory != nil", runtime)
self.assertIn("result.Memory = ensureHysteresisThreshold(cloneThreshold(override.Memory))", runtime)
def test_alert_off_does_not_promise_patrol_or_queue_suppression(self):
text = answer("Can I disable alerts for specific metrics?")
for phrase in ("Patrol findings are separate from threshold alerts",
"CPU, memory, guest-disk and storage usage",
"built-in threshold when the corresponding alert default is Off",
"Disabling a notification destination is different",
"Already queued notifications can reflect earlier settings",
"Do not induce an alert, send another Test or retry the queue"):
with self.subTest(phrase=phrase):
self.assertIn(phrase, text)
adapter = (ROOT / "internal/ai/alert_threshold_adapter.go").read_text()
self.assertIn("ok && trigger > 0", adapter)
self.assertIn("return defaultVal", adapter)
for method in ("GetNodeCPUThreshold", "GetNodeMemoryThreshold", "GetGuestMemoryThreshold",
"GetGuestDiskThreshold", "GetStorageThreshold"):
self.assertIn(f"func (a *AlertThresholdAdapter) {method}", adapter)
def test_ports_distinguish_listener_mapping_and_server_host(self):
text = answer("How do I change the port?")
for phrase in ("`FRONTEND_PORT`", "default **7655**", "`PULSE_AGENT_INGEST_PORT`",
"`8080:7655`", "`PULSE_PORT`", "inside the Pulse container",
"not on the Proxmox host", "active Pulse service",
"service-manager reload", "maintenance window"):
with self.subTest(phrase=phrase):
self.assertIn(phrase, text)
config = (ROOT / "internal/config/config.go").read_text()
self.assertIn('envconfig:"FRONTEND_PORT" default:"7655"', config)
self.assertIn('envconfig:"PULSE_AGENT_INGEST_PORT"', config)
compose = (ROOT / "docker-compose.yml").read_text()
self.assertIn('${PULSE_PORT:-7655}:7655', compose)
def test_existing_deployment_and_security_survive_port_change(self):
text = answer("How do I change the port?")
for phrase in ("existing container manager", "recreate/redeploy",
"preserving the same image, data mount and other settings",
"`docker restart` does not apply a new port mapping",
"Do not delete a data volume", "fresh `docker run` command",
"reverse proxy's upstream port separately",
"do not expose Pulse directly", "inspections private",
"TROUBLESHOOTING.md#port-change-didnt-take-effect"):
with self.subTest(phrase=phrase):
self.assertIn(phrase, text)
def test_override_handover_is_scoped_applied_and_verified(self):
text = answer("Why can't I change settings in the UI?")
for phrase in ("warning that names an environment variable", "If it is intentional, keep it",
"do not remove unrelated authentication or security overrides",
"remove only its override", "active deployment source",
"Docker require recreation/redeployment, not `docker restart`",
"service-manager reload and restart", "Preserve the image, data mount, credentials",
"does not change the running process's environment",
"check both the warning and effective value",
"does not necessarily erase the saved value", "`ALLOWED_ORIGINS`",
"no environment warning", "Do not post full environment"):
with self.subTest(phrase=phrase):
self.assertIn(phrase, text)
config = (ROOT / "internal/config/config.go").read_text()
self.assertIn('cfg.EnvOverrides["ALLOWED_ORIGINS"] = true', config)
form = (ROOT / "frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx").read_text()
self.assertIn('props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()', form)
self.assertIn('sectionPresentation().environmentOverrideMessage', form)
def test_shipped_copy_and_detailed_destinations_exist(self):
self.assertEqual(faq(), (ROOT / "frontend-modern/public/docs/FAQ.md").read_text())
for guide, heading in (("TROUBLESHOOTING", "### Port change didn't take effect"),
("TROUBLESHOOTING", "### CORS errors"),
("CONFIGURATION", "### Common Overrides (Environment Variables)")):
with self.subTest(guide=guide, heading=heading):
self.assertIn(heading, (ROOT / "docs" / f"{guide}.md").read_text())
if __name__ == "__main__":
unittest.main()