fix(debug): say where a client-side log lives instead of "(file not found)" (#78687)

hermes debug share runs on the backend. A desktop app connected to a
remote, docker, or SSH backend writes desktop.log on the client machine,
so the bundle can never contain it — and the report rendered that as a
bare "(file not found)", which reads as "the app logged nothing" and
sends triage after a client-side bug it cannot see.

Name the writer and the path to collect by hand. Backend-written logs
are unchanged, a present desktop.log is still captured, and an empty one
still reports "(file empty)" — the app ran and logged nothing is a
different fact from the file being on another host.
This commit is contained in:
brooklyn!
2026-08-04 11:53:32 -06:00
committed by GitHub
parent 2d70f56327
commit 97641a820d
2 changed files with 91 additions and 1 deletions

View File

@@ -372,6 +372,35 @@ def _primary_log_path(log_name: str) -> Optional[Path]:
return (get_hermes_home() / "logs" / filename) if filename else None
# Logs written by a client process rather than by this backend. When the
# desktop app talks to a remote/docker/SSH backend, `hermes debug share` runs
# on the *backend* and can never see them — a bare "(file not found)" then
# reads as "the app logged nothing" and sends triage down a dead end, which is
# exactly the wrong answer when the client is the thing being debugged.
_CLIENT_SIDE_LOGS = {
"desktop": (
"written by Hermes Desktop on the machine running the app, not by this "
"backend. If the desktop connects to a remote/docker/SSH backend, collect "
"it on that client machine"
),
}
def _missing_log_note(log_name: str) -> str:
"""Explain a missing log instead of stating a bare absence.
For a client-side log the absence is expected on a remote backend, so the
note names the writer and the path to collect by hand.
"""
reason = _CLIENT_SIDE_LOGS.get(log_name)
if reason is None:
return "(file not found)"
primary = _primary_log_path(log_name)
where = f" — expected at {primary}" if primary else ""
return f"(not on this host: {reason}{where})"
def _resolve_log_path(log_name: str) -> Optional[Path]:
"""Find the log file for *log_name*, falling back to the .1 rotation.
@@ -432,7 +461,11 @@ def _capture_log_snapshot(
log_path = _resolve_log_path(log_name)
if log_path is None:
primary = _primary_log_path(log_name)
tail = "(file empty)" if primary and primary.exists() else "(file not found)"
tail = (
"(file empty)"
if primary and primary.exists()
else _missing_log_note(log_name)
)
return LogSnapshot(path=None, tail_text=tail, full_text=None)
try:

View File

@@ -146,6 +146,63 @@ class TestCaptureLogSnapshot:
assert len(kept) == 10
class TestMissingLogNote:
"""A missing log explains itself when the writer isn't this backend.
`hermes debug share` runs on the backend, so a desktop connected to a
remote/docker/SSH backend can never contribute desktop.log. Reporting a
bare absence sends triage after a client-side bug it cannot see.
"""
def test_backend_written_log_reports_plain_absence(self, hermes_home):
from hermes_cli.debug import _capture_log_snapshot
(hermes_home / "logs" / "agent.log").unlink()
snap = _capture_log_snapshot("agent", tail_lines=10)
assert snap.full_text is None
assert snap.tail_text == "(file not found)"
def test_client_written_log_names_its_writer_and_path(self, hermes_home):
from hermes_cli.debug import _capture_log_snapshot
(hermes_home / "logs" / "desktop.log").unlink()
snap = _capture_log_snapshot("desktop", tail_lines=10)
assert snap.full_text is None
assert "not on this host" in snap.tail_text
assert "Hermes Desktop" in snap.tail_text
# The reader needs the path to collect by hand on the client machine.
assert str(hermes_home / "logs" / "desktop.log") in snap.tail_text
def test_present_client_log_is_captured_normally(self, hermes_home):
"""A local backend still reads desktop.log — the note is only for a miss."""
from hermes_cli.debug import _capture_log_snapshot
snap = _capture_log_snapshot("desktop", tail_lines=10)
assert "backend spawned" in snap.tail_text
assert "not on this host" not in snap.tail_text
def test_empty_client_log_is_empty_not_absent(self, hermes_home):
"""An empty file means the app ran and logged nothing — a different fact."""
from hermes_cli.debug import _capture_log_snapshot
(hermes_home / "logs" / "desktop.log").write_text("")
snap = _capture_log_snapshot("desktop", tail_lines=10)
assert snap.tail_text == "(file empty)"
def test_report_carries_the_note_for_a_remote_backend(self, hermes_home):
"""The uploaded report — what people paste into support — must explain it."""
from hermes_cli.debug import collect_debug_report
(hermes_home / "logs" / "desktop.log").unlink()
report = collect_debug_report(log_lines=10, dump_text="dump\n")
assert "--- desktop.log" in report
assert "not on this host" in report
# ---------------------------------------------------------------------------