From f2b34b9d0aa90a1b8b9eb6f4c468fe4a919748d5 Mon Sep 17 00:00:00 2001 From: Lafnaps <177272356+Lafnaps@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:17:58 +0300 Subject: [PATCH] fix(mcp): migrate the last-read fallback on identity rename, purge, and channel rename/delete --- mcp_bridge.py | 20 ++++++++++++-- tests/test_channel_fallback.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/mcp_bridge.py b/mcp_bridge.py index d6974dba..70ad6ba0 100644 --- a/mcp_bridge.py +++ b/mcp_bridge.py @@ -510,6 +510,11 @@ def migrate_identity(old_name: str, new_name: str): with _cursors_lock: if old_name in _cursors: _cursors[new_name] = _cursors.pop(old_name) + with _last_read_lock: + if old_name in _last_read_channel: + _last_read_channel[new_name] = _last_read_channel.pop(old_name) + if old_name in _last_read_job_id: + _last_read_job_id[new_name] = _last_read_job_id.pop(old_name) if old_name in _roles: _roles[new_name] = _roles.pop(old_name) _save_roles() @@ -524,6 +529,9 @@ def purge_identity(name: str): _activity_ts.pop(name, None) with _cursors_lock: _cursors.pop(name, None) + with _last_read_lock: + _last_read_channel.pop(name, None) + _last_read_job_id.pop(name, None) if name in _roles: del _roles[name] _save_roles() @@ -531,19 +539,27 @@ def purge_identity(name: str): def migrate_cursors_rename(old_name: str, new_name: str): - """Move cursor entries from old channel name to new channel name.""" + """Move cursor and fallback entries from old channel to new channel.""" with _cursors_lock: for agent_cursors in _cursors.values(): if old_name in agent_cursors: agent_cursors[new_name] = agent_cursors.pop(old_name) + with _last_read_lock: + for sender, fallback in list(_last_read_channel.items()): + if fallback == old_name: + _last_read_channel[sender] = new_name _save_cursors() def migrate_cursors_delete(channel: str): - """Remove cursor entries for a deleted channel.""" + """Remove cursors and reset every stale fallback to #general.""" with _cursors_lock: for agent_cursors in _cursors.values(): agent_cursors.pop(channel, None) + with _last_read_lock: + for sender, fallback in list(_last_read_channel.items()): + if fallback == channel: + _last_read_channel[sender] = "general" _save_cursors() diff --git a/tests/test_channel_fallback.py b/tests/test_channel_fallback.py index 166ad31c..2c59e4b8 100644 --- a/tests/test_channel_fallback.py +++ b/tests/test_channel_fallback.py @@ -144,5 +144,54 @@ def test_explicit_channel_is_never_overridden(self): self.assertEqual(channel, "portfolio") +class ChannelFallbackMigrationTests(unittest.TestCase): + """Tests that identity and channel lifecycle hooks keep the fallback + maps in sync (rename/purge of agents, rename/delete of channels).""" + + def setUp(self): + self._saved_ch = dict(mcp_bridge._last_read_channel) + self._saved_job = dict(mcp_bridge._last_read_job_id) + mcp_bridge._last_read_channel.clear() + mcp_bridge._last_read_job_id.clear() + + def tearDown(self): + mcp_bridge._last_read_channel.clear() + mcp_bridge._last_read_channel.update(self._saved_ch) + mcp_bridge._last_read_job_id.clear() + mcp_bridge._last_read_job_id.update(self._saved_job) + + def test_identity_rename_carries_fallback_state(self): + mcp_bridge._last_read_channel["alice"] = "bugfixing" + mcp_bridge._last_read_job_id["alice"] = 7 + mcp_bridge.migrate_identity("alice", "alice-2") + self.assertEqual(mcp_bridge._last_read_channel["alice-2"], "bugfixing") + self.assertEqual(mcp_bridge._last_read_job_id["alice-2"], 7) + self.assertNotIn("alice", mcp_bridge._last_read_channel) + self.assertNotIn("alice", mcp_bridge._last_read_job_id) + + def test_purge_identity_drops_fallback_state(self): + mcp_bridge._last_read_channel["alice"] = "bugfixing" + mcp_bridge._last_read_job_id["alice"] = 7 + mcp_bridge.purge_identity("alice") + self.assertNotIn("alice", mcp_bridge._last_read_channel) + self.assertNotIn("alice", mcp_bridge._last_read_job_id) + + def test_channel_rename_rewrites_matching_fallbacks(self): + mcp_bridge._last_read_channel["alice"] = "bugfixing" + mcp_bridge._last_read_channel["bob"] = "portfolio" + mcp_bridge.migrate_cursors_rename("bugfixing", "hotfixes") + self.assertEqual(mcp_bridge._last_read_channel["alice"], "hotfixes") + # Other agents' fallbacks are untouched + self.assertEqual(mcp_bridge._last_read_channel["bob"], "portfolio") + + def test_channel_delete_resets_fallbacks_to_general(self): + mcp_bridge._last_read_channel["alice"] = "bugfixing" + mcp_bridge._last_read_channel["bob"] = "portfolio" + mcp_bridge.migrate_cursors_delete("bugfixing") + self.assertEqual(mcp_bridge._last_read_channel["alice"], "general") + # Other agents' fallbacks are untouched + self.assertEqual(mcp_bridge._last_read_channel["bob"], "portfolio") + + if __name__ == "__main__": unittest.main()