from __future__ import annotations import asyncio import pytest from deeptutor.services.session.sqlite_store import SQLiteSessionStore from deeptutor.services.session.turn_runtime import TurnRuntimeManager, _TurnExecution @pytest.mark.asyncio async def test_subscribe_turn_does_not_synthesize_done_for_running_turn(tmp_path) -> None: """A paused/replaced subscription must not make the UI think the turn ended.""" store = SQLiteSessionStore(tmp_path / "chat_history.db") runtime = TurnRuntimeManager(store) session = await store.ensure_session(None) turn = await store.create_turn(session["id"], capability="chat") execution = _TurnExecution( turn_id=turn["id"], session_id=session["id"], capability="chat", payload={}, ) runtime._executions[turn["id"]] = execution events: list[dict] = [] async def _collect() -> None: async for event in runtime.subscribe_turn(turn["id"], after_seq=0): events.append(event) task = asyncio.create_task(_collect()) for _ in range(200): if execution.subscribers: break await asyncio.sleep(0.01) assert execution.subscribers await execution.subscribers[0].queue.put(None) await asyncio.wait_for(task, timeout=1) assert events == [] persisted = await store.get_turn(turn["id"]) assert persisted is not None assert persisted["status"] == "running" @pytest.mark.asyncio async def test_subscribe_turn_marks_orphan_running_turn_failed(tmp_path) -> None: """A DB-running turn with no in-process execution is stale after restart.""" store = SQLiteSessionStore(tmp_path / "chat_history.db") runtime = TurnRuntimeManager(store) session = await store.ensure_session(None) turn = await store.create_turn(session["id"], capability="chat") events: list[dict] = [] async for event in runtime.subscribe_turn(turn["id"], after_seq=0): events.append(event) persisted = await store.get_turn(turn["id"]) assert persisted is not None assert persisted["status"] == "failed" assert "restart" in persisted["error"].lower() assert [event["type"] for event in events] == ["error", "done"] assert events[-1]["metadata"]["status"] == "failed" @pytest.mark.asyncio async def test_start_turn_clears_orphan_running_turn_before_create( monkeypatch: pytest.MonkeyPatch, tmp_path ) -> None: """A stale active turn should not block the next user message after restart.""" store = SQLiteSessionStore(tmp_path / "chat_history.db") runtime = TurnRuntimeManager(store) session = await store.ensure_session(None) stale = await store.create_turn(session["id"], capability="chat") async def _noop_run_turn(_execution): return None monkeypatch.setattr(runtime, "_run_turn", _noop_run_turn) _, new_turn = await runtime.start_turn( { "type": "start_turn", "session_id": session["id"], "capability": "chat", "content": "hello", "tools": [], "knowledge_bases": [], "attachments": [], "language": "en", "config": {}, } ) assert new_turn["id"] != stale["id"] persisted = await store.get_turn(stale["id"]) assert persisted is not None assert persisted["status"] == "failed"