Files
hermes-webui/tests/test_logs_endpoint.py
2026-05-05 01:51:05 +00:00

119 lines
4.1 KiB
Python

import json
import urllib.error
import urllib.parse
import urllib.request
from tests._pytest_port import BASE, TEST_STATE_DIR
def _get_logs(file="agent", tail=200):
url = f"{BASE}/api/logs?file={urllib.parse.quote(str(file))}&tail={urllib.parse.quote(str(tail))}"
with urllib.request.urlopen(url, timeout=10) as r:
return json.loads(r.read()), r.status
def _get_logs_error(file="agent", tail=200):
url = f"{BASE}/api/logs?file={urllib.parse.quote(str(file))}&tail={urllib.parse.quote(str(tail))}"
try:
with urllib.request.urlopen(url, timeout=10) as r:
return json.loads(r.read()), r.status
except urllib.error.HTTPError as e:
return json.loads(e.read()), e.code
def test_logs_endpoint_tails_whitelisted_synthetic_agent_log():
logs_dir = TEST_STATE_DIR / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
(logs_dir / "agent.log").write_text(
"\n".join(
[f"2026-05-04 INFO synthetic-log-marker line {i}" for i in range(105)]
+ ["2026-05-04 ERROR synthetic-log-marker failed safely"]
) + "\n",
encoding="utf-8",
)
data, status = _get_logs("agent", 100)
assert status == 200
assert data["file"] == "agent"
assert data["tail"] == 100
assert len(data["lines"]) == 100
assert data["lines"][0] == "2026-05-04 INFO synthetic-log-marker line 6"
assert data["lines"][-1] == "2026-05-04 ERROR synthetic-log-marker failed safely"
assert data["truncated"] is False
assert data["total_bytes"] > 0
assert data["mtime"] > 0
assert data.get("hint") == ""
def test_logs_endpoint_rejects_path_traversal_and_unknown_files():
for bad_file in ("../../etc/passwd", "agent.log", "private", "/tmp/agent"):
data, status = _get_logs_error(bad_file, 200)
assert status == 400
assert "error" in data
def test_logs_endpoint_missing_file_returns_empty_lines_with_safe_hint():
missing = TEST_STATE_DIR / "logs" / "gateway.log"
if missing.exists():
missing.unlink()
data, status = _get_logs("gateway", 200)
assert status == 200
assert data["file"] == "gateway"
assert data["lines"] == []
assert data["truncated"] is False
assert data["total_bytes"] == 0
assert data["mtime"] is None
assert "not found" in data["hint"].lower()
assert str(TEST_STATE_DIR) not in data["hint"]
def test_logs_endpoint_tail_selector_is_allowlisted_and_defaults_to_200():
logs_dir = TEST_STATE_DIR / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
(logs_dir / "errors.log").write_text(
"\n".join(f"2026-05-04 ERROR synthetic-log-marker line {i}" for i in range(250)) + "\n",
encoding="utf-8",
)
default_data, default_status = _get_logs("errors", "not-a-number")
capped_data, capped_status = _get_logs("errors", 999999)
allowed_data, allowed_status = _get_logs("errors", 100)
assert default_status == capped_status == allowed_status == 200
assert default_data["tail"] == 200
assert len(default_data["lines"]) == 200
assert capped_data["tail"] == 200
assert len(capped_data["lines"]) == 200
assert allowed_data["tail"] == 100
assert len(allowed_data["lines"]) == 100
def test_logs_endpoint_reads_bounded_window_and_reports_truncation():
logs_dir = TEST_STATE_DIR / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
huge_prefix = "x" * (4 * 1024 * 1024 + 64)
(logs_dir / "gateway.log").write_text(
huge_prefix + "\n2026-05-04 INFO synthetic-log-marker tail survives\n",
encoding="utf-8",
)
data, status = _get_logs("gateway", 1000)
assert status == 200
assert data["tail"] == 1000
assert data["truncated"] is True
assert data["lines"][-1] == "2026-05-04 INFO synthetic-log-marker tail survives"
assert data["total_bytes"] > 4 * 1024 * 1024
def test_logs_endpoint_tests_use_only_synthetic_fixture_content():
source = __import__("pathlib").Path(__file__).read_text(encoding="utf-8")
assert "synthetic-log-marker" in source
assert "/home/" + "michael" not in source
assert "~/" + ".hermes/logs" not in source
assert "TOK" + "EN=" not in source
assert "PASS" + "WORD=" not in source