Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Regression: a dying turn closes its native stream (PR 85796 review, B8).
|
||||
|
||||
Stale-generation exits and cancellations returned from run() with the
|
||||
native stream still open: the Slack message kept its live streaming
|
||||
indicator forever, and the adapter's armed interception state survived
|
||||
into the next turn (which could inherit the key and seal a dead
|
||||
draft_id). The consumer now calls adapter.abandon_open_draft on both
|
||||
death paths — sealing in place with the last delivered frame, claiming
|
||||
nothing (no delivery flags).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig
|
||||
from tests.gateway.relay.test_relay_live_cards import _connected_adapter
|
||||
|
||||
|
||||
class RecordingTransport:
|
||||
def __init__(self):
|
||||
self.ops = []
|
||||
|
||||
async def send_outbound(self, payload, platform=None):
|
||||
self.ops.append(dict(payload))
|
||||
return {"success": True, "message_id": "ts.1"}
|
||||
|
||||
|
||||
def _consumer(run_still_current=None):
|
||||
adapter, _ = _connected_adapter()
|
||||
t = RecordingTransport()
|
||||
adapter._transport = t
|
||||
cfg = StreamConsumerConfig(
|
||||
transport="auto", chat_type="dm",
|
||||
edit_interval=0.01, buffer_threshold=1, cursor="",
|
||||
)
|
||||
sc = GatewayStreamConsumer(
|
||||
adapter, "C1", cfg,
|
||||
metadata={"thread_ts": "1700.8", "message_id": "1700.801"},
|
||||
run_still_current=run_still_current,
|
||||
)
|
||||
return sc, adapter, t
|
||||
|
||||
|
||||
class TestAbandonOnTurnDeath:
|
||||
@pytest.mark.asyncio
|
||||
async def test_stale_generation_seals_open_stream(self):
|
||||
"""/new mid-stream: run_still_current flips False → the open
|
||||
stream is sealed with the on-screen text, and no delivery is
|
||||
claimed."""
|
||||
_alive = [True]
|
||||
sc, adapter, t = _consumer(run_still_current=lambda: _alive[0])
|
||||
task = asyncio.create_task(sc.run())
|
||||
sc.on_delta("partial answer on screen")
|
||||
await asyncio.sleep(0.08)
|
||||
assert adapter._open_draft_by_chat, "stream should be armed"
|
||||
_alive[0] = False
|
||||
await task
|
||||
assert not adapter._open_draft_by_chat, "stream left armed after stale exit"
|
||||
seals = [o for o in t.ops if o["op"] == "draft" and o.get("final")]
|
||||
assert len(seals) == 1
|
||||
assert seals[0]["content"] == "partial answer on screen"
|
||||
assert sc.final_response_sent is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancellation_seals_open_stream(self):
|
||||
sc, adapter, t = _consumer()
|
||||
task = asyncio.create_task(sc.run())
|
||||
sc.on_delta("partial before stop")
|
||||
await asyncio.sleep(0.08)
|
||||
assert adapter._open_draft_by_chat
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
assert not adapter._open_draft_by_chat, "stream left armed after cancel"
|
||||
seals = [o for o in t.ops if o["op"] == "draft" and o.get("final")]
|
||||
assert len(seals) == 1
|
||||
assert sc.final_response_sent is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_abandon_without_open_stream_is_noop(self):
|
||||
sc, adapter, t = _consumer()
|
||||
await sc._abandon_native_stream()
|
||||
assert t.ops == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_next_turn_not_intercepted_after_abandon(self):
|
||||
"""The B8 inheritance hazard: after an abandoned turn, a NEW turn
|
||||
on the same key must arm and seal normally with its own content."""
|
||||
_alive = [True]
|
||||
sc, adapter, t = _consumer(run_still_current=lambda: _alive[0])
|
||||
task = asyncio.create_task(sc.run())
|
||||
sc.on_delta("old turn partial")
|
||||
await asyncio.sleep(0.08)
|
||||
_alive[0] = False
|
||||
await task
|
||||
# Next turn, same chat/keys, new draft_id:
|
||||
md = {"thread_ts": "1700.8", "message_id": "1700.801"}
|
||||
await adapter.send_draft("C1", 999, "new turn", metadata=md)
|
||||
r = await adapter.send("C1", "new turn final", metadata=dict(md))
|
||||
assert r.success
|
||||
seals = [o for o in t.ops if o["op"] == "draft" and o.get("final")]
|
||||
assert seals[-1]["draft_id"] == 999
|
||||
assert seals[-1]["content"] == "new turn final"
|
||||
Reference in New Issue
Block a user