Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions plugins/leaf/skills/leaf/scripts/leaf/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def undo_error(event: dict, events: list, within: dict) -> str | None:
return f"unknown undoes {event['undoes']!r}"
if target["author"] != "user":
return f"{target['kind']} {target['id']} is not the reader's own gesture"
# Asked before the kind's own questions rather than after them, because a gesture
# already taken back is gone from the folds those questions read: `build_threads`
# drops a withdrawn reaction, so the thread walk below found none and raised
# StopIteration out of the append door. That is a 500, which the browser is told to
# retry against a state that will never change again — the outbox wedges on the
# gesture instead of putting it back. Withdrawal is a fact about the target and not
# about its kind, and it is the same no-op whichever kind carries it.
if target["id"] in taken_back(events):
return f"{target['id']} has already been taken back"
if target["kind"] in MESSAGE_KINDS:
if not is_reaction(target):
return (
Expand All @@ -80,8 +89,6 @@ def undo_error(event: dict, events: list, within: dict) -> str | None:
f"{target['kind']} events cannot be taken back (the kinds that can "
f"are {', '.join(sorted(UNDOABLE_KINDS))} and a reaction)"
)
if target["id"] in taken_back(events):
return f"{target['id']} has already been taken back"
return None


Expand Down
13 changes: 13 additions & 0 deletions tests/test_interact_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,19 @@ def test_the_door_admits_a_reaction_only_as_a_token_the_layer_declares(
data=json.dumps({"kind": "undo", "undoes": nod["id"]}).encode(),
)
assert status == 200, body
# And comes off once, however the second press gets here — the racing tab of the
# door's own docstring. A withdrawn reaction is gone from `build_threads`, so the
# kind's thread walk had nothing to find and raised out of the door instead: a 500
# the browser is told to retry, against a state that will never answer differently.
# The no-op costs a toast, which is what a final refusal is.
status, body = fetch(
f"{server}/api/event",
data=json.dumps({"kind": "undo", "undoes": nod["id"]}).encode(),
)
assert status == 400, body
answer = json.loads(body)
assert answer["final"] is True, body
assert "already been taken back" in answer["error"], body
# Answered, the page reaction is a conversation, and the withdrawal would orphan
# the answer; the reader's move is in the thread it opened.
conversation_model.cmd_reply(page_dir, reaction["id"], "Which part is long?", None)
Expand Down
Loading