From 0cb00e60956d1de6f13a33e52e50ab8c5001a604 Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Sat, 3 Oct 2026 09:04:11 -0400 Subject: [PATCH] test: qualify keyed voice cancellation receipts --- tests/unit/test_conversation_llm.py | 48 +++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_conversation_llm.py b/tests/unit/test_conversation_llm.py index a51be37..843ca00 100644 --- a/tests/unit/test_conversation_llm.py +++ b/tests/unit/test_conversation_llm.py @@ -116,7 +116,9 @@ class ConversationLLMTests(unittest.IsolatedAsyncioTestCase): await asyncio.gather(consumer, return_exceptions=True) self.assertEqual(len(payloads), 2) self.assertEqual(payloads[0], payloads[1]) - self.assertEqual(cancelled_paths, ['/internal/chat/rooms/bound-room/runs/offline-recovered-id/cancel']) + self.assertEqual(cancelled_paths, [ + '/internal/chat/rooms/bound-room/operations/' + context.items[-1].id + '/cancel', + '/internal/chat/rooms/bound-room/runs/offline-recovered-id/cancel']) finally: release.set() if not consumer.done(): consumer.cancel() @@ -159,7 +161,9 @@ class ConversationLLMTests(unittest.IsolatedAsyncioTestCase): await asyncio.wait_for(asyncio.gather(closer, second_closer), 2) await asyncio.gather(consumer, return_exceptions=True) await asyncio.wait_for(cancelled.wait(), .3) - self.assertEqual(cancellations, ['/internal/chat/rooms/room/runs/offline-admitted-run/cancel']) + self.assertEqual(cancellations, [ + '/internal/chat/rooms/room/operations/' + context.items[-1].id + '/cancel', + '/internal/chat/rooms/room/runs/offline-admitted-run/cancel']) finally: release_admission.set() if closer and not closer.done(): @@ -170,4 +174,44 @@ class ConversationLLMTests(unittest.IsolatedAsyncioTestCase): await asyncio.gather(consumer, return_exceptions=True) await bridge.aclose() + async def test_key_cancel_receipt_prevents_late_rejected_admission_action(self): + entered, release, tombstoned = asyncio.Event(), asyncio.Event(), asyncio.Event() + requests, progress_events = [], [] + async def handler(request): + requests.append(request.url.path) + if request.url.path.endswith('/messages'): + entered.set() + await release.wait() + self.assertTrue(tombstoned.is_set(), 'Persist operation cancellation before admission settles') + return httpx.Response(409, json={'detail': 'Operation cancelled'}) + if '/operations/' in request.url.path and request.url.path.endswith('/cancel'): + tombstoned.set() + return httpx.Response(200, json={'ok': True}) + self.fail('A rejected cancelled operation must never stream or cancel an invented run') + async def progress(value): progress_events.append(value) + bridge = ConversationLLM(control_url='https://offline.test', secret='offline', room_id='bound-room', on_progress=progress) + await bridge.client.aclose() + bridge.client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + context = llm.ChatContext([llm.ChatMessage(role='user', content=['Cancel before admission'])]) + stream = bridge.chat(chat_ctx=context) + async def consume(): + async with stream: + return [chunk async for chunk in stream] + consumer = asyncio.create_task(consume()) + try: + await asyncio.wait_for(entered.wait(), 2) + closer = asyncio.create_task(stream.aclose()) + await asyncio.wait_for(tombstoned.wait(), 2) + release.set() + await asyncio.wait_for(closer, 2) + await asyncio.gather(consumer, return_exceptions=True) + self.assertEqual(requests, ['/internal/chat/rooms/bound-room/messages', + '/internal/chat/rooms/bound-room/operations/' + context.items[-1].id + '/cancel']) + self.assertEqual(progress_events[-1]['status'], 'failed') + finally: + release.set() + if not consumer.done(): consumer.cancel() + await asyncio.gather(consumer, return_exceptions=True) + await bridge.aclose() + if __name__=='__main__': unittest.main()