Files
pulse/scripts/tests/test_agent_log_docs.py
pulse-triage[bot] fcc633337e Bound copied log diagnostics by bytes as well as records
Cap server and agent excerpts at 256 KiB before display or request-ID search. Buffer only a bounded owner-only temporary file, preserve reader failures, and apply the hard deadline to the complete read-only pipeline. Exercise exact-size, overflow, stream, privacy, cleanup and closed-pipe deadline controls without accessing a real journal, daemon or service.

Contract-Neutral: Documentation-only passive log recipe bounds and executable copied-recipe tests; no runtime, API, authentication, installer or monitoring contract change.
Change-source: pulse-maintainer
2026-10-11 02:24:14 +01:00

130 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Exercise the copied agent log reader against synthetic journal processes.
No real journal, service, credential, agent or runtime is accessed. These
controls establish recipe bounds and failure handling, not native recovery.
"""
from pathlib import Path
import re
import subprocess
import unittest
from test_troubleshooting_logs import exercise_log_recipe
ROOT = Path(__file__).resolve().parents[2]
DOC = ROOT / "docs/UNIFIED_AGENT.md"
LOG = "Podman runtime detected\n"
PRIVATE_ERROR = "synthetic-private-endpoint-and-token\n"
def section(heading):
text = DOC.read_text(encoding="utf-8")
marker = "### " + heading + "\n"
if marker not in text:
raise AssertionError(f"missing section: {heading}")
return re.split(r"\n#{2,3} ", text.split(marker, 1)[1], 1)[0]
def recipe():
blocks = re.findall(r"```bash\n(.*?)```", section("Collect agent logs safely"), re.DOTALL)
if len(blocks) != 1:
raise AssertionError("expected one shared systemd journal reader")
return blocks[0]
class AgentLogDocsTest(unittest.TestCase):
def exercise(self, **settings):
return exercise_log_recipe("journalctl", recipe(), **settings)
def test_shipped_guide_matches_source(self):
self.assertEqual(DOC.read_bytes(),
(ROOT / "frontend-modern/public/docs/UNIFIED_AGENT.md").read_bytes())
def test_copied_reader_is_bounded_and_read_only(self):
copied = recipe()
subprocess.run(["bash", "-n", "-c", copied], check=True, capture_output=True)
result, argv = self.exercise(stdout=LOG)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, LOG)
self.assertEqual(result.stderr, "")
self.assertEqual(argv, ["-u", "pulse-agent.service", "--since", "15 minutes ago",
"--lines", "200", "--no-pager", "--output=cat"])
self.assertIn("timeout --signal=KILL 8s", copied)
self.assertIn("2>&1", copied)
self.assertNotRegex(copied, r"\b(?:grep|curl|docker|systemctl|printenv|sudo)\b|--follow|--token")
def test_both_streams_survive_success_without_claiming_redaction(self):
result, _ = self.exercise(stdout=LOG, stderr=PRIVATE_ERROR)
self.assertEqual(result.returncode, 0, result.stderr)
# Independently buffered streams do not establish a total order.
self.assertCountEqual(result.stdout.splitlines(), (LOG + PRIVATE_ERROR).splitlines())
self.assertEqual(result.stderr, "")
def test_reader_failure_withholds_partial_logs_and_keeps_exit(self):
for output in ("", LOG):
with self.subTest(partial=bool(output)):
result, _ = self.exercise(stdout=output, stderr=PRIVATE_ERROR, exit_code=2)
self.assertEqual(result.returncode, 2)
self.assertEqual(result.stdout, "")
self.assertIn("Agent log read unavailable (exit 2)", result.stderr)
self.assertNotIn(PRIVATE_ERROR.strip(), result.stderr)
def test_hung_reader_is_stopped_by_the_documented_deadline(self):
result, _ = self.exercise(stdout=LOG, stderr=PRIVATE_ERROR, hang="term")
self.assertEqual(result.returncode, 137, result.stderr)
self.assertEqual(result.stdout, "")
self.assertIn("Agent log read unavailable (exit 137)", result.stderr)
self.assertNotIn(PRIVATE_ERROR.strip(), result.stderr)
def test_reader_ignoring_term_is_killed_at_the_hard_deadline(self):
result, _ = self.exercise(stdout=LOG, hang="ignore-term")
self.assertEqual(result.returncode, 137, result.stderr)
self.assertEqual(result.stdout, "")
self.assertIn("Agent log read unavailable (exit 137)", result.stderr)
def test_missing_timeout_does_not_start_the_reader(self):
result, argv = self.exercise(missing_timeout=True)
self.assertIsNone(argv)
self.assertEqual(result.returncode, 1)
self.assertEqual(result.stdout, "")
self.assertIn("no unbounded fallback", result.stderr)
def test_successful_empty_read_is_inconclusive(self):
result, _ = self.exercise()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout + result.stderr, "")
self.assertIn("successful empty read is inconclusive", section("Collect agent logs safely"))
def test_shared_reader_preserves_scope_and_private_output_boundaries(self):
text = " ".join(section("Collect agent logs safely").split())
for boundary in ("Linux systemd agent host", "already authorised", "affected agent host",
"not the Pulse server", "actual service name", "original time window",
"does not contact Pulse", "partial output is withheld", "not sanitised",
"manually redacted", "Tokens, cookies, URLs", "do not post the full excerpt",
"service environment, connection files or saved identity",
"broaden permissions", "Do not restart, re-enrol or enable debug logging",
"container agent or a non-systemd host", "evidence as unavailable"):
with self.subTest(boundary=boundary):
self.assertIn(boundary, text)
def test_diagnostic_entry_points_reuse_the_reader_without_live_followers(self):
text = DOC.read_text(encoding="utf-8")
log_level = text.split("### Agent log level\n", 1)[1].split("\n## ", 1)[0]
for entry in (log_level, section("Agent Not Updating"),
section("Commands enabled but remote control blocked"),
section("Docker Swarm Not Detected")):
self.assertIn("[bounded agent log reader](#collect-agent-logs-safely)", entry)
self.assertNotIn("journalctl", entry)
self.assertIn("not evidence-collection steps", log_level)
self.assertIn("restart changes the run", log_level)
self.assertNotRegex(text, r"journalctl[^\n]*(?:\s-f\b|--follow|\|\s*grep)")
swarm = " ".join(section("Docker Swarm Not Detected").split())
self.assertIn("An empty search does not establish", swarm)
self.assertIn("not enable debug logging, restart the agent or change its runtime", swarm)
if __name__ == "__main__":
unittest.main()