Never match a date separator as a summary anchor

Date separators are local MessageObjects that live in the same
ChatActivity.messages list as real messages, and ChatActivity builds them
with messageOwner.id left at 0 (their stable ids come from a separate
dateObjectsStableIds space, so they never collide with injected cards).

find_index_for_anchor scanned for an exact id match without requiring a
positive id, so looking up anchor 0 returned the index of the first
separator and would have placed a card against a divider rather than a
message. loaded_message_ids had the matching flaw: it recorded the
separators' 0, which made a summary with anchor id 0 look restorable.

Reject non-positive anchors outright and keep separators out of the loaded
id set. Every real chat interleaves separators, so the harness now models
them: they must not be picked as the insertion point, must not satisfy an
anchor-loaded check, and summaries anchored on real messages must still
restore correctly with separators in between.
This commit is contained in:
lyuksovannyy committed 2026-09-19 16:54:25 +02:00
1 parent 0619c5fe68
commit b05d1a274c
2 files changed
+57 -2

No files matched your search

+13 -2
View File
@@ -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. again once the anchor is paged in.
""" """
anchor_msg_id = int(anchor_msg_id) 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: try:
size = messages_list.size() size = messages_list.size()
for i in range(size): for i in range(size):
@@ -558,8 +564,13 @@ def loaded_message_ids(messages_list: Any) -> set:
try: try:
for i in range(messages_list.size()): for i in range(messages_list.size()):
owner = getattr(messages_list.get(i), "messageOwner", None) owner = getattr(messages_list.get(i), "messageOwner", None)
if owner is not None: if owner is None:
ids.add(int(getattr(owner, "id", 0) or 0)) 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: except Exception as exc:
diagnostics.record_error("injector.loaded_message_ids", exc) diagnostics.record_error("injector.loaded_message_ids", exc)
return ids return ids
+44
View File
@@ -1927,6 +1927,50 @@ def main(argv=None) -> int:
f"index={injector.find_index_for_anchor(absent.messages, 123, require_anchor=False)}", 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( check(
"sys.modules gained no entry for bundled plugin module names", "sys.modules gained no entry for bundled plugin module names",
not ({"main", "config", "services", "providers", "hooks", "ui.summarize_dialog"} & new_modules), not ({"main", "config", "services", "providers", "hooks", "ui.summarize_dialog"} & new_modules),