fix(kanban): warn once on credential-gated subs instead of silent rewind
Kanban subscriptions fail-closed by the multiplex credential gate rewinded their claim every tick with only a DEBUG line: a profile_routes-pinned profile that runs other-platform adapters but none for the subscription's platform, and a sub stamped with a profile other than the route's, were permanent invisible dead-ends. Mirror the #110919/#111079 anchorless-thread warning: say so ONCE per row at WARNING, naming the dead-end and the re-subscribe escape hatch (#115460).
This commit is contained in:
@@ -128,6 +128,21 @@ def _warn_anchorless_thread_sub_once(sub: dict, platform: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
_UNROUTABLE_WARNED: set[tuple] = set()
|
||||
|
||||
|
||||
def _warn_unroutable_sub_once(sub: dict, platform: Any, message: str, *extra_args: Any) -> None:
|
||||
"""A routed subscription the credential gate fail-closes is a permanent dead-end: delivery
|
||||
rewinds every tick with only a DEBUG line. Say so ONCE per row at WARNING, mirroring
|
||||
``_warn_anchorless_thread_sub_once`` (#115460)."""
|
||||
key = (sub.get("task_id"), platform, sub.get("chat_id"), sub.get("thread_id") or "")
|
||||
if key in _UNROUTABLE_WARNED:
|
||||
return
|
||||
_UNROUTABLE_WARNED.add(key)
|
||||
logger.warning(message, sub.get("task_id"), getattr(platform, "value", platform),
|
||||
sub.get("chat_id"), *extra_args)
|
||||
|
||||
|
||||
def _platform_names(mapping: Any) -> set[str]:
|
||||
"""Lower-cased platform names of an adapters mapping (Platform enums or strings)."""
|
||||
return {getattr(platform, "value", str(platform)).lower() for platform in mapping}
|
||||
@@ -148,6 +163,13 @@ def _adapter_for_subscription(runner: Any, platform: Any, sub: dict, owner_profi
|
||||
# Empty maps are startup placeholders for route-only profiles; a connected
|
||||
# secondary on ANY platform establishes an independent credential boundary.
|
||||
if (getattr(runner, "_profile_adapters", {}) or {}).get(profile):
|
||||
_warn_unroutable_sub_once(
|
||||
sub, platform,
|
||||
"kanban notifier: subscription for %s on %s chat %s is pinned to profile %s, which runs "
|
||||
"other-platform adapters but none for %s; it will not be delivered. Give that profile a %s "
|
||||
"adapter or make it route-only, then re-subscribe with `hermes kanban notify-subscribe ... "
|
||||
"--notifier-profile <a profile that holds a %s credential>`.",
|
||||
profile, platform.value, platform.value, platform.value)
|
||||
return None
|
||||
metadata = sub.get("delivery_metadata") or {}
|
||||
guild = metadata.get("scope_id") or metadata.get("guild_id")
|
||||
@@ -165,6 +187,12 @@ def _adapter_for_subscription(runner: Any, platform: Any, sub: dict, owner_profi
|
||||
if route.matches(platform.value, guild_id=guild, chat_id=chat,
|
||||
thread_id=thread, parent_chat_id=parent, user_id=user_id):
|
||||
if route.profile != profile:
|
||||
_warn_unroutable_sub_once(
|
||||
sub, platform,
|
||||
"kanban notifier: subscription for %s on %s chat %s is stamped with profile %s but a "
|
||||
"profile_routes entry pins that chat to profile %s; it will not be delivered. "
|
||||
"Re-subscribe with `hermes kanban notify-subscribe ... --notifier-profile %s`.",
|
||||
profile, route.profile, route.profile)
|
||||
return None
|
||||
from gateway.run import _multiplex_profile_homes
|
||||
served = {name for name, _home in _multiplex_profile_homes(config)}
|
||||
|
||||
@@ -234,3 +234,41 @@ def test_anchorless_thread_subscription_warns_once_instead_of_silent_skip(tmp_pa
|
||||
assert len(warnings) == 1 and warnings[0].levelno == logging.WARNING
|
||||
assert "--parent-chat-id" in warnings[0].getMessage()
|
||||
assert unseen(task)
|
||||
|
||||
|
||||
def test_credential_gate_denials_warn_once_instead_of_silent_rewind(tmp_path, monkeypatch, caplog):
|
||||
"""A pinned profile that runs other-platform adapters but none for the subscription's
|
||||
platform, and a sub stamped with a profile other than the route's, are permanent dead-ends:
|
||||
the notifier rewinds the claim every tick at DEBUG only. Both skips must surface ONCE per
|
||||
row at WARNING with the re-subscribe escape hatch (#115460)."""
|
||||
import logging
|
||||
from gateway import kanban_watchers_notifier as notifier
|
||||
|
||||
runner = setup_runner(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(notifier, "_UNROUTABLE_WARNED", set())
|
||||
# The pinned profile holds a credential on another platform, so it is an independent
|
||||
# credential boundary without a Discord adapter of its own.
|
||||
runner._profile_adapters["yuki"] = {Platform.TELEGRAM: RecordingAdapter()}
|
||||
task = completion()
|
||||
with caplog.at_level(logging.WARNING, logger=notifier.logger.name):
|
||||
assert not collect(runner)
|
||||
assert not collect(runner)
|
||||
warnings = [r for r in caplog.records if "none for discord" in r.getMessage() and task in r.getMessage()]
|
||||
assert len(warnings) == 1 and warnings[0].levelno == logging.WARNING
|
||||
assert "--notifier-profile" in warnings[0].getMessage()
|
||||
assert unseen(task)
|
||||
|
||||
# An owner stamped with the invoking shell's profile (#76483) instead of the route's
|
||||
# is the same silent dead-end. (Fresh DB: resetting the credential boundary above
|
||||
# re-enables the first sub, which is the documented workaround, and its backlog must
|
||||
# not pollute this claim.)
|
||||
runner._profile_adapters["yuki"] = {}
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "stamped-owner.db"))
|
||||
stamped = completion(profile="default")
|
||||
with caplog.at_level(logging.WARNING, logger=notifier.logger.name):
|
||||
assert not collect(runner)
|
||||
warnings = [r for r in caplog.records if "pins that chat to profile yuki" in r.getMessage()
|
||||
and stamped in r.getMessage()]
|
||||
assert len(warnings) == 1 and warnings[0].levelno == logging.WARNING
|
||||
assert "--notifier-profile yuki" in warnings[0].getMessage()
|
||||
assert unseen(stamped)
|
||||
|
||||
Reference in New Issue
Block a user