Skip to content

Commit 22197ed

Browse files
authored
fix(lark): admit topic events with absent optional ancestry (#6011)
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
1 parent e8085c5 commit 22197ed

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎loopx/extensions/lark/private_conversations.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,17 @@ def _source_message(self, record: dict[str, Any], *, selected: dict[str, Any] |
112112
if selected["binding"].get("audience") == "group":
113113
# Provider readback, never event text, fixes the topic root.
114114
root = str(message.get("root_id") or message["message_id"])
115+
# The compact event projection emits null for absent
116+
# ancestry. mget can enrich the thread id later; that is
117+
# not a conflict unless the event supplied another id.
118+
# Parent/root ancestry must still agree exactly.
119+
parent = str(message.get("parent_id") or "")
120+
event_parent = str(event.get("parent_id") or "")
121+
event_thread = str(event.get("thread_id") or "")
115122
if (root != str(event.get("root_id") or event["message_id"])
116-
or (message.get("parent_id") and not message.get("root_id"))
117-
or any(message.get(key, "") != event.get(key, "") for key in ["parent_id", "thread_id"])):
123+
or (parent and not message.get("root_id"))
124+
or parent != event_parent
125+
or (event_thread and event_thread != str(message.get("thread_id") or ""))):
118126
return None
119127
if root == event["message_id"]:
120128
original: Mapping[str, Any] | None = message

‎tests/test_lark_private_conversations.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,42 @@ def test_group_provider_mention_survives_a_bot_rename_and_topic_followup(ordinar
328328
runtime.close()
329329

330330

331+
@pytest.mark.parametrize("is_reply", [False, True])
332+
def test_group_readback_accepts_null_event_fields_and_provider_enriched_thread(ordinary, is_reply): # noqa: F811
333+
_, runtime, provider, transport = connect_group(ordinary)
334+
try:
335+
root = provider.topic("compact-topic", "Explain this project")
336+
# The CLI event projection emits null for absent fields; mget can
337+
# supply the topic's thread id even when the event did not carry it.
338+
root.update(root_id=None, parent_id=None, thread_id=None)
339+
provider.messages[root["message_id"]]["thread_id"] = "omt_compact_topic"
340+
provider.messages[root["message_id"]]["mentions"] = [{"id": "cli_notes_app", "name": "notes-app"}]
341+
event = root
342+
if is_reply:
343+
event = provider.topic("compact-reply", "Compare its configuration", root=root["message_id"])
344+
event["thread_id"] = None
345+
provider.messages[event["message_id"]]["thread_id"] = "omt_compact_topic"
346+
assert transport.admit("notes-app", event)["status"] == "durably_accepted"
347+
finish_group_turn(runtime, transport, message=event["content"])
348+
assert all(topic == root["message_id"] for _, topic, _ in provider.topic_writes)
349+
finally:
350+
runtime.close()
351+
352+
353+
@pytest.mark.parametrize("field,value", [("parent_id", "om_wrong_parent"), ("thread_id", "omt_wrong_thread")])
354+
def test_group_readback_rejects_supplied_conflicting_ancestry(ordinary, field, value): # noqa: F811
355+
store, runtime, provider, transport = connect_group(ordinary)
356+
try:
357+
root = provider.topic("conflicting-topic", "Explain this project")
358+
event = provider.topic("conflicting-reply", "Continue", root=root["message_id"])
359+
event[field] = value
360+
assert transport.admit("notes-app", event)["status"] == "source_verification_failed"
361+
assert not store.list_sessions() and not list(transport.root.glob("*.json"))
362+
assert provider.writes == []
363+
finally:
364+
runtime.close()
365+
366+
331367
def test_group_status_and_recipient_commands_cannot_expose_private_sessions(ordinary): # noqa: F811
332368
store, runtime, provider, transport = connect_group(ordinary)
333369
try:

0 commit comments

Comments
 (0)