Skip to content
Closed
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
8 changes: 7 additions & 1 deletion gateway/authz_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ def _authorization_adapter(
if not platform:
return None
profile_name = (profile or "").strip() or None
if profile_name and profile_name != "default":
active_profile = None
if profile_name:
try:
active_profile = getattr(self, "_active_profile_name")()
except Exception:
active_profile = None
if profile_name and profile_name not in {"default", active_profile}:
profile_adapters = getattr(self, "_profile_adapters", None) or {}
if profile_name in profile_adapters:
return profile_adapters[profile_name].get(platform)
Expand Down
6 changes: 5 additions & 1 deletion gateway/kanban_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,11 @@ def _collect():
elif kind == "blocked":
reason = ""
if ev.payload and ev.payload.get("reason"):
reason = f": {str(ev.payload['reason'])[:160]}"
# Block reasons often contain the human approval
# contract: ticket URL plus exact response choices.
# A 160-character preview can cut those choices in
# half and turn the notification into a dead end.
reason = f": {str(ev.payload['reason'])[:3000]}"
msg = f"⏸ {board_tag}{tag}Kanban {sub['task_id']} blocked{reason}"
elif kind == "gave_up":
err = ""
Expand Down
38 changes: 38 additions & 0 deletions tests/gateway/test_kanban_notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,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_gets_full_actionable_block_notification(tmp_path, monkeypatch):
"""The active profile uses self.adapters and keeps approval instructions."""
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. "
+ "context " * 20
+ "Respond `Approve` or `Request changes: …`."
)
assert len(reason) > 160
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 "https://linear.example/AGE-39" in message
assert "`Approve`" in message
assert "`Request changes: …`" 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"
Expand Down
8 changes: 8 additions & 0 deletions tests/gateway/test_multiplex_profile_authz.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ def test_adapter_for_source_resolves_secondary_profile_adapter(monkeypatch):
) is default_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)
Expand Down