Skip to content
Open
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
5 changes: 5 additions & 0 deletions raven/tui_rpc/methods/turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ async def turn_cancel(
# handle.result() returns None on cancellation (does not raise).
await handle.result()

# Defensive clear: the sink drops the slot via on_turn_end at turn exit,
# but a cancelled turn can resolve before that callback runs. Clear here
# so the next turn.send does not race into a phantom -32003.
clear_active(parsed.session_key)

return {"cancelled": True}


Expand Down
38 changes: 38 additions & 0 deletions tests/test_turn_cancel_clears_active.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""turn.cancel must drop the active-turn slot even if the sink has not yet (issue #115)."""

from __future__ import annotations

from raven.tui_rpc.methods import turn as turn_mod


class _FakeHandle:
"""Stand-in for a TurnHandle: cancel marks it, result resolves to None."""

def __init__(self) -> None:
self.cancelled = False

def cancel(self) -> None:
self.cancelled = True

async def result(self) -> None:
return None


async def test_turn_cancel_clears_active_slot() -> None:
session_key = "sess-cancel-test"
turn_mod._active_turns[session_key] = _FakeHandle()

result = await turn_mod.turn_cancel({"session_key": session_key}, emitter=None)

assert result == {"cancelled": True}
assert turn_mod.is_turn_active(session_key) is False


async def test_turn_cancel_without_active_turn_is_noop() -> None:
session_key = "sess-idle"
turn_mod._active_turns.pop(session_key, None)

result = await turn_mod.turn_cancel({"session_key": session_key}, emitter=None)

assert result == {"cancelled": False}
assert turn_mod.is_turn_active(session_key) is False