`hermes -z` judged the run by whether stdout was non-empty: a turn that hit the iteration budget, ended `partial`, failed on the provider, or was interrupted still exited 0 as long as it printed an explanation, so scripts treated a half-done job (or a 401 summary) as success. The `-q`/`-Q` paths already got one result-derived contract in 23863ccbafe0d; this is the same class on the scripted one-shot entry point (#111770). - `hermes_cli/oneshot.py::_oneshot_exit_code`: 0 only when the turn completed; 130 interrupted; 2 failed / partial / completed:false; 1 a completed turn with no text (unchanged message). - `--usage-file` now also reports `partial`, `interrupted` and `turn_exit_reason`, so a pipeline can tell a budget stop from a Ctrl-C. - Docs: `hermes -z` exit-code table + usage-file fields in cli-commands.md. Live probe (run_oneshot with a stubbed _run_agent): partial-with-text 0 -> 2, budget-with-text 0 -> 2, interrupted-with-text 0 -> 130, failed-with-text 0 -> 2; completed 0 -> 0 (control), partial-no-text 2 -> 2 (unchanged).
42 lines
1.8 KiB
Python
42 lines
1.8 KiB
Python
"""``hermes -z`` exit-code contract: the outcome is judged from the turn result, not from
|
|
whether any text was printed (#111770 — an incomplete or failed run that left an explanation on
|
|
stdout used to exit 0, so scripts treated a half-done job as success)."""
|
|
|
|
from unittest import mock
|
|
|
|
import hermes_cli.oneshot as oneshot
|
|
|
|
|
|
def _run(monkeypatch, tmp_path, response, result):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
usage = tmp_path / "usage.json"
|
|
with mock.patch.object(oneshot, "_run_agent", return_value=(response, dict(result))):
|
|
code = oneshot.run_oneshot("q", usage_file=str(usage))
|
|
return code, usage
|
|
|
|
|
|
def test_incomplete_run_with_text_exits_nonzero_and_reports_why(monkeypatch, tmp_path):
|
|
import json
|
|
|
|
partial = {"final_response": "Got as far as step 2.", "completed": False, "partial": True,
|
|
"turn_exit_reason": "iteration_limit"}
|
|
code, usage = _run(monkeypatch, tmp_path, "Got as far as step 2.", partial)
|
|
assert code == 2
|
|
report = json.loads(usage.read_text(encoding="utf-8"))
|
|
assert report["completed"] is False and report["partial"] is True
|
|
assert report["turn_exit_reason"] == "iteration_limit"
|
|
|
|
interrupted = {"final_response": "Stopping.", "completed": False, "interrupted": True}
|
|
code, usage = _run(monkeypatch, tmp_path, "Stopping.", interrupted)
|
|
assert code == 130
|
|
assert json.loads(usage.read_text(encoding="utf-8"))["interrupted"] is True
|
|
|
|
failed = {"final_response": "Provider returned 401.", "completed": False, "failed": True}
|
|
code, _ = _run(monkeypatch, tmp_path, "Provider returned 401.", failed)
|
|
assert code == 2
|
|
|
|
|
|
def test_completed_run_still_exits_zero(monkeypatch, tmp_path):
|
|
code, _ = _run(monkeypatch, tmp_path, "Paris.", {"final_response": "Paris.", "completed": True, "failed": False})
|
|
assert code == 0
|