Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions hermes_cli/kanban_survivor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,23 @@ def _git(repo, *args, env=None, check=True, input=None):
)
if check and result.returncode:
# Git stderr can contain credential-bearing remote URLs, so it is never
# persisted: the raised message stays constant and the detail goes to
# the log, redacted by the same helper that guards an echoed claim.
# persisted. The subcommand and returncode are safe to include in the
# hold; the stderr goes to the log through the existing redaction path.
#
# Without this, every occurrence costs an attribution pass. A failure
# that is purely environmental (t_169d6e46: a concurrent pytest session
# deleting this repo's tmp_path, so git exits 128 "cannot change to
# '<path>': No such file or directory") is indistinguishable from a real
# capture defect once it reaches the caller as a bare
# capture defect when the caller receives only the old bare
# "survivor_unavailable: git inspection failed".
log.warning(
"kanban survivor: git %s failed rc=%s in %s: %s",
args[0] if args else "?", result.returncode, _ext.redact(str(repo)),
_ext.redact(result.stderr.decode("utf-8", "replace").strip()),
)
raise SurvivorUnavailable("survivor_unavailable: git inspection failed")
raise SurvivorUnavailable(
f"survivor_unavailable: git {args[0] if args else '?'} failed (rc={result.returncode})"
)
return result


Expand Down
14 changes: 8 additions & 6 deletions tests/hermes_cli/test_kanban_survivor_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,13 @@ def test_mined_pr_corroborated_by_the_cards_own_metadata_is_accepted(board, remo
# --- t_169d6e46: a swallowed git failure must be diagnosable ----------------

def test_git_failure_logs_returncode_and_stderr_without_persisting_them(tmp_path, caplog):
"""`survivor_unavailable: git inspection failed` discards WHY it failed.
"""The hold names the Git operation and exit code without persisting stderr.

That message is identical for a real capture defect and for a purely
The old message was identical for a real capture defect and for a purely
environmental fault (t_169d6e46: a concurrent pytest session deleting this
repo's tmp_path, so git exits 128 `cannot change to '<path>'`). Telling them
apart cost a full attribution pass per occurrence. The returncode and stderr
must reach the LOG; the raised message must stay constant and credential-free,
must reach the LOG; the raised message must stay credential-free,
because it is persisted to held_reason, the event log and stderr.
"""
import hermes_cli.kanban_survivor as survivor
Expand All @@ -226,8 +226,8 @@ def test_git_failure_logs_returncode_and_stderr_without_persisting_them(tmp_path
with pytest.raises(survivor.SurvivorUnavailable) as excinfo:
survivor._git(missing, "status", "--porcelain")

# The persisted surface is unchanged — 5 open PRs key on this string.
assert str(excinfo.value) == "survivor_unavailable: git inspection failed"
assert str(excinfo.value) == "survivor_unavailable: git status failed (rc=128)"
assert "No such file or directory" not in str(excinfo.value)
# ...and the diagnostic that distinguishes environment from defect is logged.
assert "rc=128" in caplog.text
assert "status" in caplog.text
Expand All @@ -250,9 +250,11 @@ def test_git_failure_log_redacts_credentials_from_stderr(tmp_path, caplog, monke
lambda *a, **k: sp.CompletedProcess(a[0], 128, b"", leaky.encode()),
)
with caplog.at_level("WARNING"):
with pytest.raises(survivor.SurvivorUnavailable):
with pytest.raises(survivor.SurvivorUnavailable) as excinfo:
survivor._git(tmp_path, "fetch")

assert SECRET not in caplog.text
assert SECRET not in str(excinfo.value)
assert str(excinfo.value) == "survivor_unavailable: git fetch failed (rc=128)"
assert SECRET not in str(caplog.records[-1].getMessage())
assert "rc=128" in caplog.text # still diagnosable
Loading