fix(gateway): normalize legacy bool reply_to_mode so 'false' truly means 'off' (#29623)#29639
Open
xxxigm wants to merge 2 commits into
Open
fix(gateway): normalize legacy bool reply_to_mode so 'false' truly means 'off' (#29623)#29639xxxigm wants to merge 2 commits into
xxxigm wants to merge 2 commits into
Conversation
…ans 'off' (NousResearch#29623) Older Discord/Telegram configs (or migrated configs that came in via the YAML 1.1 ``off``-as-bool trap) can still contain ``reply_to_mode: false`` under ``display.platforms.discord:`` / ``platforms.discord:`` blocks that the YAML→env bridge doesn't pre-process. ``PlatformConfig.from_dict`` then stored the raw ``False``, and the adapter's ``getattr(config, 'reply_to_mode', 'first') or 'first'`` mapped that falsy value back to ``"first"`` — exactly the opposite of what the user asked for. Add ``gateway.config.normalize_reply_to_mode(value)`` as the single source of truth for the contract (``False`` → ``"off"``, ``True`` → default, ``None``/garbage → default, case+whitespace-insensitive ``off``/``first``/``all``), wire it into ``PlatformConfig.__post_init__`` so every construction path (``from_dict``, env-var bridge, direct ``PlatformConfig(reply_to_mode=…)``) lands on a canonical string, and also apply it defensively in the Discord and Telegram adapter constructors for the rare stub-config path that bypasses the dataclass. Two existing ``test_invalid_mode_stored_as_is`` assertions pinned the pre-fix "store garbage as-is" behaviour; both are flipped to the improved contract here.
…ousResearch#29623) Adds tests/gateway/test_reply_to_mode_normalization.py — 64 cases across six classes: - ``TestNormalizeReplyToModeHelper`` (parametrised + 3 standalone): pure contract of the new helper — every bool / str / numeric / None / garbage input lands on one of ``REPLY_TO_MODES``, the ``default=`` override is honoured for missing/garbage but NOT for the explicit boolean ``False`` intent, and ``DEFAULT_REPLY_TO_MODE`` itself is always inside the public tuple. - ``TestReporterRepro29623`` (4 cases): the exact transcript from the issue body — ``PlatformConfig.from_dict({'reply_to_mode': False})`` now resolves to ``"off"`` (was ``False``), ``"off"`` round-trips, missing keys still default to ``"first"``, plus a single-shot test that asserts all three rows of the reporter's table at once. - ``TestPlatformConfigPostInit`` (parametrised + 3): every direct ``PlatformConfig(reply_to_mode=…)`` construction style is normalised, ``to_dict`` no longer re-emits the legacy bool, and a guardrail proves ``__post_init__`` actually runs (a future ``__init__`` that silently swallows it would trip here). - ``TestEnvVarBridgeNormalises`` (2): ``DISCORD_REPLY_TO_MODE`` lands via ``load_gateway_config`` into a ``PlatformConfig`` that still goes through the normaliser. - ``TestAdapterDefensiveNormalisation`` (4): even stub configs that bypass the dataclass entirely (``_StubConfig`` duck-typed) are handled by the adapter-side defensive call, so the bug surface is closed regardless of how the config object was built. - ``TestLegacyBoolRoundTrip`` (parametrised): every legacy ``off``-ish input (``False`` / ``"off"`` / ``"false"`` / ``"no"`` / ``"0"`` / ``0``) survives a from_dict→to_dict→from_dict cycle as ``"off"``, so configs written by older Hermes versions can't reanimate the bug on the next save.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes #29623 — Discord (and Telegram) configs that carry the legacy boolean
reply_to_mode: falsewere silently regressing to"first"on the current runtime, so the bot kept replying to the triggering message instead of sending it standalone.Root cause.
PlatformConfig.from_dictdid:— no normalisation, so
Falsewas stored verbatim. The Discord and Telegram adapter constructors then did:False or 'first'evaluates to'first', which is exactly the opposite of what the user asked for. The YAML→env bridge inload_gateway_configalready coercedFalse → "off"for top-leveldiscord:/telegram:sections, but thefrom_dictpath (used bydisplay.platforms.discord:andplatforms.discord:blocks) bypassed that bridge entirely.Fix.
gateway.config.normalize_reply_to_mode(value, *, default=…)is the single source of truth:False/"false"/"no"/"0"/0→"off",True/"true"/"yes"/"1"/1→ default,None/missing/garbage → default, case- and whitespace-insensitiveoff/first/allround-trip canonically.PlatformConfig.__post_init__runs the normaliser, so every construction path (from_dict, env-var bridge, directPlatformConfig(reply_to_mode=…)in tests and embedders) lands on a canonical string.DiscordAdapter.__init__andTelegramAdapter.__init__apply the same normaliser defensively, so the rare stub-config path that bypasses the dataclass (downstream embedders that hand-roll a duck-typed config object) still produces a canonical mode.The reporter's exact repro now prints
False -> offinstead ofFalse -> False.Related Issue
Closes #29623 — Discord
reply_to_mode: falseregresses tofirston current runtime.Type of Change
or 'first'/_rtm_str = "off" if … is False else …logic into one helperChanges Made
gateway/config.pyREPLY_TO_MODES = ("off", "first", "all")andDEFAULT_REPLY_TO_MODE = "first"— both used as the public contract surface for tests and adapter code.normalize_reply_to_mode(value, *, default=DEFAULT_REPLY_TO_MODE) -> strhelper with the full rule table (see fix description above).PlatformConfig.reply_to_modedefault switched toDEFAULT_REPLY_TO_MODE(same string, semantically labelled).PlatformConfig.__post_init__that runs every constructed config through the normaliser — the single point of truth.PlatformConfig.from_dictnow calls the normaliser explicitly at the boundary (redundant with__post_init__but documents the contract).gateway/platforms/discord.py—DiscordAdapter.__init__swaps the brittlegetattr(config, 'reply_to_mode', 'first') or 'first'for the new normaliser, with a comment that points atgateway.config.normalize_reply_to_modeand Bug: Discordreply_to_mode: falseregresses tofirston current runtime #29623.gateway/platforms/telegram.py— same defensive normalisation pattern inTelegramAdapter.__init__.tests/gateway/test_discord_reply_mode.pyandtests/gateway/test_telegram_reply_mode.py— flip the twotest_invalid_mode_stored_as_isassertions totest_invalid_mode_normalised_to_defaultso they pin the improved contract instead of the bug surface they used to lock in.tests/gateway/test_reply_to_mode_normalization.py(new, +329 lines, 64 cases across six classes):TestNormalizeReplyToModeHelper— pure-helper contract (parametrised over ~30 inputs, plustest_custom_default_is_honoured,test_output_is_always_in_REPLY_TO_MODES,test_default_constant_is_in_modes).TestReporterRepro29623— the exact transcript pasted in the issue body, asserted as four separate tests.TestPlatformConfigPostInit— every directPlatformConfig(reply_to_mode=…)is normalised,to_dictno longer re-emits the legacy bool, and a guardrail catches a future__init__that silently drops__post_init__.TestEnvVarBridgeNormalises— the env-var path throughload_gateway_configalso lands on canonical values.TestAdapterDefensiveNormalisation— duck-typed stub configs (downstream embedders) still get a canonical mode out ofDiscordAdapter/ via thenormalize_reply_to_mode(getattr(stub, 'reply_to_mode', None))helper used inTelegramAdapter.TestLegacyBoolRoundTrip— every legacyoff-ish input survivesfrom_dict → to_dict → from_dictas"off", so a config written by an older Hermes can't reanimate the bug on the next save.How to Test
off(before this PR:False).Checklist
Code
fix(gateway): …andtest(gateway): …)scripts/run_tests.sh tests/gateway/test_reply_to_mode_normalization.py tests/gateway/test_discord_reply_mode.py tests/gateway/test_telegram_reply_mode.pyand all 137 tests passDocumentation & Housekeeping
normalize_reply_to_modedocuments the full rule table; no user-facing docs change is needed (the YAML key + accepted string values are unchanged — legacy bool values just now resolve correctly)cli-config.yaml.exampleif I added/changed config keys — N/A (no new config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs