Commit graph

2 commits

Author SHA1 Message Date
Brian D. Evans
0344959d46 test+types: address Copilot nits on #15318
Three minor cleanups from Copilot's review on #15318:

1. ``_parse_launchd_list_output`` return type: ``set`` → ``set[int]``
   (matches the ``seen: set[int] = set()`` style elsewhere in the
   module).
2. ``_get_service_pids`` return type + local: same parameterization;
   was bare ``set`` before this branch existed, but Copilot flagged
   the pre-existing annotation while reviewing the diff so worth
   tightening as a drive-by.
3. ``unittest.mock.patch`` was imported in
   ``test_gateway_pid_detection_macos.py`` but never called — the
   tests use ``monkeypatch`` exclusively.  Removed the unused import.

No behaviour change.  39/39 tests still pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:26:18 -07:00
Brian D. Evans
5751372877 fix(gateway): correct macOS gateway-pid detection (#15225)
``hermes cron list`` falsely reports "Gateway is not running" on macOS
even when launchd has the service loaded and cron jobs fire correctly.
Two independent bugs in ``find_gateway_pids()`` each drop macOS into
an empty-result path; fixing either alone would clear the warning, but
both are real and both are worth fixing.

### Bug 1 — ``_get_service_pids()`` mis-parses ``launchctl list <label>``

``launchctl list`` returns two different formats depending on whether
a label is passed:

* No label → tab-separated table ``PID\\tStatus\\tLabel``
* With label → plist-dict dump, e.g. ``"PID" = 855;``

The old code always called ``launchctl list <label>`` (the plist-dict
path) but parsed with ``string.split()`` expecting the tab-separated
format.  ``parts[2]`` on ``"Label" = "ai.hermes.gateway";`` is
``'"ai.hermes.gateway";'`` — quoted, semicolon'd — so the ``== label``
comparison never matched and no PID was ever extracted.

Fix: extracted a ``_parse_launchd_list_output(stdout, label)`` helper
that tries the plist-dict ``"PID" = N;`` regex first and falls back to
the tab-separated path when no plist matches were found.  Handles both
formats so a future change to the caller (passing or not passing the
label) can't re-break detection.  Regex anchored to the ``"PID"`` key
so sibling fields like ``LastExitStatus`` can't match; PID 0 rejected
so downstream ``os.kill(0, ...)`` never sees it.

### Bug 2 — ``_scan_gateway_pids()`` passes ``eww`` to ``ps``

The old invocation ``ps -A eww -o pid=,command=`` has two problems:

* **Darwin** rejects ``eww`` as "illegal argument" and exits 1 —
  ``stdout`` is empty, the parse loop iterates nothing, and no PID is
  extracted (#15225).
* **FreeBSD** accepts ``eww`` but the ``e`` attaches environment
  variables to the command column, so ``split(None, 1)`` picks up the
  first env var as the command (#9069).  The env vars can include API
  keys and tokens, which also leaked them into any log line that
  echoed the command.

Fix: replaced with ``ps -A -ww -o pid=,command=`` — portable across
Linux (procps), Darwin, FreeBSD, and busybox.  Drops env-var leakage
as a side benefit.

### Tests (19 new + 1 updated, all passing)

``tests/hermes_cli/test_gateway_pid_detection_macos.py``:

* **``TestParseLaunchdListOutput``** (8 cases) — exercises the new
  helper directly with the exact plist-dict output from the #15225
  repro, the tab-separated format, the dash-PID unloaded case,
  mixed-shape defensive input, PID=0 rejection, and 4
  whitespace-variant tolerance cases for the PID regex (``"PID" =``,
  ``"PID"=``, ``"PID"  =  ``, leading tabs).

* **``TestGetServicePidsMacOS``** (3 cases) — end-to-end macOS branch
  with ``subprocess.run`` patched to return the plist-dict payload;
  asserts the single-PID set is returned, a non-zero launchctl exit
  returns empty, and a missing ``launchctl`` binary (FileNotFoundError)
  falls through cleanly.

* **``TestPsInvocationPortability``** (4 cases) — captures the exact
  argv the production code passes to ``ps``, asserts ``"eww"`` is
  never on the command line, pins the ``["ps", "-A", "-ww", "-o",
  "pid=,command="]`` shape, parses a realistic Darwin-style ps sample
  end-to-end, and exercises the nonzero-returncode fallback.

Also updated ``test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails``
in ``tests/hermes_cli/test_gateway.py`` to match the new portable
``-ww`` invocation; added an inline comment pointing at #15225 history.

**Verified tests are real regression guards**: temporarily reverted
Bug 1 and Bug 2 independently; the relevant test classes correctly
failed with clear messages pointing at the regressed invariant.

Closes #15225

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:59:40 -07:00