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>
``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>