fix(gateway): only coerce int route ids; warn when a float/bool id can never match (#86470)

Folds the #86815 nuance into the #70815 helper: `bool` is an int subclass
and floats stringify to "123.0", both of which silently recreate the
no-match #70815 fixes — pass them through with a load-time warning.
Trims the route-id tests to one positive (int/negative-int/omitted) and
one negative (float/bool warn).

Co-authored-by: pittosporum-seu <117899760+pittosporum-seu@users.noreply.github.com>
Co-authored-by: fangliquanflq <280272527+fangliquanflq@users.noreply.github.com>
This commit is contained in:
Teknium
2026-09-02 04:01:13 -07:00
parent ca42d7a034
commit bedebf5f7d
2 changed files with 44 additions and 45 deletions

View File

@@ -163,9 +163,22 @@ def _coerce_route_id(value: Any) -> Optional[str]:
PyYAML loads unquoted numeric IDs (Discord snowflakes, Telegram negative
chat ids) as ``int``. Inbound ``SessionSource`` fields are always ``str``
via ``build_source``, so leaving ints here makes ``matches()`` fail silently.
Only ``int`` is coerced (the legitimate YAML-numeric case). ``bool`` is an
``int`` subclass but never a valid id; floats and other types stringify to
something (``"123.0"``) that can never equal an inbound id — recreating the
silent no-match this exists to fix — so they are passed through with a
load-time warning instead of being silently "fixed" (#86470).
"""
if value is None:
return None
if value is None or isinstance(value, str):
return value
if isinstance(value, int) and not isinstance(value, bool):
return str(value)
logger.warning(
"Profile route discriminator %r (type %s) can never match an inbound "
"id — quote it in config.yaml (e.g. chat_id: \"%s\").",
value, type(value).__name__, value,
)
return str(value)

View File

@@ -53,52 +53,38 @@ class TestParseProfileRoutes:
assert parse_profile_routes([]) == []
def test_coerces_yaml_native_int_ids_to_str(self):
# PyYAML loads unquoted snowflakes as int; inbound SessionSource IDs are str.
raw = [
{
"name": "server",
"platform": "discord",
"profile": "p",
"guild_id": 111,
"chat_id": 222,
"thread_id": 333,
},
]
routes = parse_profile_routes(raw)
assert routes[0].guild_id == "111"
assert routes[0].chat_id == "222"
assert routes[0].thread_id == "333"
assert all(isinstance(v, str) for v in (
routes[0].guild_id, routes[0].chat_id, routes[0].thread_id,
))
matched = match_profile_route(
routes, "discord", guild_id="111", chat_id="222", thread_id="333",
)
assert matched is not None
assert matched.name == "server"
def test_coerces_negative_telegram_chat_id(self):
raw = [
{
"name": "tg",
"platform": "telegram",
"profile": "p",
"chat_id": -1001234567890,
},
]
routes = parse_profile_routes(raw)
assert routes[0].chat_id == "-1001234567890"
matched = match_profile_route(routes, "telegram", chat_id="-1001234567890")
assert matched is not None
assert matched.name == "tg"
def test_omitted_ids_remain_none(self):
# PyYAML loads unquoted snowflakes / negative Telegram ids as int;
# inbound SessionSource ids are str, so un-coerced routes never match.
routes = parse_profile_routes([
{"name": "server", "platform": "discord", "profile": "p",
"guild_id": 111, "chat_id": 222, "thread_id": 333},
{"name": "tg", "platform": "telegram", "profile": "p",
"chat_id": -1001234567890},
{"name": "platform-only", "platform": "discord", "profile": "p"},
])
assert routes[0].guild_id is None
assert routes[0].chat_id is None
assert routes[0].thread_id is None
by_name = {r.name: r for r in routes}
assert (by_name["server"].guild_id, by_name["server"].chat_id,
by_name["server"].thread_id) == ("111", "222", "333")
assert match_profile_route(
routes, "discord", guild_id="111", chat_id="222", thread_id="333",
).name == "server"
assert match_profile_route(
routes, "telegram", chat_id="-1001234567890",
).name == "tg"
assert (by_name["platform-only"].guild_id, by_name["platform-only"].chat_id,
by_name["platform-only"].thread_id) == (None, None, None)
def test_non_int_numeric_ids_warn_instead_of_silently_coercing(self, caplog):
# #86470 nuance: float/bool stringify to values that can never match
# an inbound id, so surface the misconfiguration at load time.
with caplog.at_level("WARNING", logger="gateway.profile_routing"):
routes = parse_profile_routes([
{"name": "f", "platform": "discord", "profile": "p", "chat_id": 123.0},
{"name": "b", "platform": "discord", "profile": "p", "guild_id": True},
])
assert {r.name for r in routes} == {"f", "b"}
assert match_profile_route(routes, "discord", chat_id="123") is None
assert sum("can never match" in rec.message for rec in caplog.records) == 2
class TestMatchProfileRoute: