diff --git a/src/services/rich_injector.py b/src/services/rich_injector.py index 4211f15..c1a7898 100644 --- a/src/services/rich_injector.py +++ b/src/services/rich_injector.py @@ -195,6 +195,12 @@ def find_index_for_anchor(messages_list: Any, anchor_msg_id: int, require_anchor again once the anchor is paged in. """ anchor_msg_id = int(anchor_msg_id) + # Anchors are real, positive message ids. A non-positive anchor must never + # match: date separators are local MessageObjects whose messageOwner.id is + # 0, so scanning for 0 would "find" the first separator and place the card + # against a divider instead of a message. + if anchor_msg_id <= 0: + return -1 try: size = messages_list.size() for i in range(size): @@ -558,8 +564,13 @@ def loaded_message_ids(messages_list: Any) -> set: try: for i in range(messages_list.size()): owner = getattr(messages_list.get(i), "messageOwner", None) - if owner is not None: - ids.add(int(getattr(owner, "id", 0) or 0)) + if owner is None: + continue + msg_id = int(getattr(owner, "id", 0) or 0) + # Skip date separators (id 0). Keeping them would make a summary + # whose anchor id is 0 look restorable. + if msg_id: + ids.add(msg_id) except Exception as exc: diagnostics.record_error("injector.loaded_message_ids", exc) return ids diff --git a/tools/verify_bundle.py b/tools/verify_bundle.py index e0e8dfd..770272a 100755 --- a/tools/verify_bundle.py +++ b/tools/verify_bundle.py @@ -1927,6 +1927,50 @@ def main(argv=None) -> int: f"index={injector.find_index_for_anchor(absent.messages, 123, require_anchor=False)}", ) + # Every real chat interleaves date separators, which are local + # MessageObjects carrying messageOwner.id == 0 (ChatActivity builds them + # with `iput v12, ...Message->id` where v12 is 0, and gives them ids from + # a separate dateObjectsStableIds space). They must never be treated as + # the insertion point or counted as a loaded anchor. + dated = _FakeChatActivity([700, 690]) + separator_owner = _TLObj("TL_messageService") + separator_owner.id = 0 + dated.messages.add(1, _FakeMessageObject(0, separator_owner)) + dated_index = injector.find_index_for_anchor(dated.messages, 690) + dated_ids = [m.messageOwner.id for m in dated.messages] + check( + "a date separator is not mistaken for the anchor", + dated_ids[dated_index] == 690, + f"index={dated_index} ids={dated_ids}", + ) + # A separator's id 0 must not make "anchor 0" look loaded, and must not + # be offered as a position; anchor ids are always positive. + check( + "a date separator does not satisfy an anchor-loaded check", + injector.find_index_for_anchor(dated.messages, 0) == -1, + f"index={injector.find_index_for_anchor(dated.messages, 0)}", + ) + # A summary anchored on a real message must still restore correctly when + # separators sit between the loaded messages. + sep_restore = _FakeChatActivity([800, 790]) + sep_owner = _TLObj("TL_messageService") + sep_owner.id = 0 + sep_restore.messages.add(1, _FakeMessageObject(0, sep_owner)) + sep_n = injector.restore_saved_summaries( + sep_restore, + 0, + -4242, + 0, + [summary_db_mod.StoredSummary(41, -4242, 0, 790, "text", "", 5, 0)], + ) + sep_ids = [m.messageOwner.id for m in sep_restore.messages] + sep_card = injector.virtual_id_for(41, 790) + check( + "summaries restore correctly around date separators", + sep_n == 1 and sep_ids.index(sep_card) == sep_ids.index(790) - 1, + f"dispatched={sep_n} ids={sep_ids}", + ) + check( "sys.modules gained no entry for bundled plugin module names", not ({"main", "config", "services", "providers", "hooks", "ui.summarize_dialog"} & new_modules),