"""Approval hooks must carry the Hermes session id to observer plugins. Staging defect 2026-08-10: approval marks were emitted under a synthetic "default" relay session because the approval hook payload carried only turn_id/tool_call_id — the observability plugin's ``_session_id()`` fell back to "default", parented the marks to a session scope that never closes, and close-time exporters never shipped them. The audit board's approval tables stayed empty while approvals were demonstrably firing. Contract: when the dispatch layer binds an observability context with a session id, every approval hook payload carries that session id; when no context is bound, the payload omits it (legacy behavior preserved). """ from __future__ import annotations from unittest.mock import patch from tools import approval as approval_mod def _capture_hook(captured): def _invoke(hook_name, **kwargs): captured.append((hook_name, kwargs)) return _invoke class TestApprovalHookSessionId: def test_session_id_forwarded_when_bound(self): captured = [] tokens = approval_mod.set_current_observability_context( turn_id="turn-1", tool_call_id="call-1", session_id="20260810_test_session", ) try: with patch( "hermes_cli.lifecycle.invoke_hook", side_effect=_capture_hook(captured), ): approval_mod._fire_approval_hook( "pre_approval_request", command="rm -rf /etc/hosts", description="dangerous", surface="gateway", ) finally: approval_mod.reset_current_observability_context(tokens) assert captured, "hook must dispatch" _, kwargs = captured[0] assert kwargs.get("session_id") == "20260810_test_session" assert kwargs.get("turn_id") == "turn-1" assert kwargs.get("tool_call_id") == "call-1" def test_explicit_session_id_not_clobbered(self): captured = [] tokens = approval_mod.set_current_observability_context( session_id="context-session", ) try: with patch( "hermes_cli.lifecycle.invoke_hook", side_effect=_capture_hook(captured), ): approval_mod._fire_approval_hook( "post_approval_response", session_id="explicit-session", choice="approved", ) finally: approval_mod.reset_current_observability_context(tokens) _, kwargs = captured[0] assert kwargs.get("session_id") == "explicit-session" def test_absent_when_unbound(self): captured = [] with patch( "hermes_cli.lifecycle.invoke_hook", side_effect=_capture_hook(captured), ): approval_mod._fire_approval_hook( "pre_approval_request", command="x", description="y", ) _, kwargs = captured[0] assert "session_id" not in kwargs, ( "no synthetic session id when none is bound — the observer's " "own fallback owns that decision" )