from fastapi.testclient import TestClient import json from duck_core.model_client import ModelResponse from duck_core.api import create_app def test_stream_chat_endpoint_emits_sse_reasoning_and_content(tmp_path, monkeypatch): monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) async def fake_chat(self, role, messages): return ModelResponse( role=role, model="local-main", content=json.dumps( { "kind": "action_directive", "intent": "answer directly", "risk_level": "none", "actions": [], } ), reasoning_content=None, raw={}, latency_ms=1.0, ) async def fake_stream_chat(self, role, messages): yield {"type": "reasoning_delta", "delta": "thinking"} yield {"type": "content_delta", "delta": "answer"} monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat) monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat) app = create_app() client = TestClient(app) with client.stream( "POST", "/v1/chat/stream", json={"message": "hello", "workspace": "./workspace", "debug": True}, ) as response: body = "".join(response.iter_text()) assert response.status_code == 200 assert "event: reasoning_delta" in body assert "event: content_delta" in body assert "event: done" in body assert "thinking" in body assert "answer" in body def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, monkeypatch): monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) (tmp_path / "note.txt").write_text("stream tool content") async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None): assert role == "action" return ModelResponse( role=role, model="local-main", content=json.dumps( { "kind": "action_directive", "intent": "read requested file", "risk_level": "low", "actions": [ { "tool": "file_read", "args": {"path": "note.txt"}, "reason": "User asked for file contents", } ], } ), reasoning_content=None, raw={}, latency_ms=1.0, ) async def fake_stream_chat(self, role, messages): assert role == "thinker" assert any("tool_observations" in message["content"] for message in messages) yield {"type": "content_delta", "delta": "answer from tool"} monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat) monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat) client = TestClient(create_app()) with client.stream( "POST", "/v1/chat/stream", json={"message": "read note.txt", "workspace": str(tmp_path), "debug": True}, ) as response: body = "".join(response.iter_text()) assert response.status_code == 200 assert "event: tool_call_started" in body assert "event: tool_call_finished" in body assert "stream tool content" in body assert "event: content_delta" in body assert "answer from tool" in body assert "event: done" in body