fix(kanban): route active named profile through the active adapter map

A gateway running under a named active profile (e.g. `hermes -p main gateway`)
stamps kanban auto-subscriptions with notifier_profile=main, but
_authorization_adapter() treated any name other than the literal "default"
as a multiplex secondary and consulted only _profile_adapters — empty on
standalone gateway-per-profile deployments. The helper failed closed, the
notifier rewound the claim, and the notification was silently retried
forever (#71340).

Recognize the gateway's own active profile name as primary so its stamped
subscriptions resolve via self.adapters; genuinely secondary profiles keep
the fail-closed lookup.

Salvaged from PR #62380 (the unrelated blocked-reason truncation change is
intentionally not taken).
This commit is contained in:
Salim 2026-07-26 13:38:41 -07:00 committed by Teknium
parent dda6f0f63e
commit c93ed07459
2 changed files with 47 additions and 0 deletions

View file

@ -174,6 +174,44 @@ def test_kanban_notifier_rewinds_claim_if_adapter_disconnects(tmp_path, monkeypa
assert [ev.kind for ev in _unseen_terminal_events(tid)] == ["completed"]
def test_active_named_profile_subscription_is_delivered(tmp_path, monkeypatch):
"""A sub stamped with the gateway's own named profile uses self.adapters.
Regression for #71340: on a standalone (non-multiplex) gateway running a
named profile, _authorization_adapter() used to treat the active name as a
multiplex secondary, find no _profile_adapters entry, fail closed, and
rewind the claim forever silent zero-delivery.
"""
db_path = tmp_path / "actionable-block.db"
monkeypatch.setenv("HERMES_KANBAN_DB", str(db_path))
kb.init_db()
reason = "AGE-39 — https://linear.example/AGE-39 — publishing verified."
conn = kb.connect()
try:
tid = kb.create_task(conn, title="approval", assignee="publisher")
kb.add_notify_sub(
conn,
task_id=tid,
platform="telegram",
chat_id="chat-1",
notifier_profile="main",
)
kb.block_task(conn, tid, reason=reason, kind="needs_input")
finally:
conn.close()
adapter = RecordingAdapter()
runner = _make_runner(adapter)
runner._active_profile_name = lambda: "main"
asyncio.run(_run_one_notifier_tick(monkeypatch, runner))
assert len(adapter.sent) == 1
message = adapter.sent[0]["text"]
assert tid in message
assert "blocked" in message
def test_kanban_db_path_is_test_isolated_from_real_home():
hermes_home = Path(kb.kanban_home())
production_db = Path.home() / ".hermes" / "kanban.db"

View file

@ -209,6 +209,15 @@ def test_adapter_for_direct_source_keeps_native_platform_adapter(monkeypatch):
assert runner._adapter_for_source(source) is slack_adapter
def test_explicit_active_profile_stamp_uses_default_adapter_map(monkeypatch):
"""A named active profile is not misclassified as multiplex secondary."""
runner, default_adapter, _secondary_adapter = _make_multiplex_runner(monkeypatch)
runner._active_profile_name = lambda: "main"
assert runner._authorization_adapter(Platform.WECOM, profile="main") is default_adapter
def test_secondary_allowlist_dm_behavior_ignores_unauthorized(monkeypatch):
"""Unauthorized-DM behavior must read the secondary adapter's dm_policy."""
runner, _default_adapter, secondary_adapter = _make_multiplex_runner(monkeypatch)