"""Real loopback HTTP cancellation, without a provider or business run.""" import asyncio import pytest from langchain_core.messages import HumanMessage from langgraph.graph import END, START, StateGraph from EvoScientist.llm.gateway_proxy import GatewayProxyChatModel @pytest.mark.anyio async def test_model_cancel_closes_http_sse_reader(monkeypatch): monkeypatch.setenv("AI4SCI_EVO_RUNTIME_GRANT_SECRET", "test-only-secret") connected = asyncio.Event() disconnected = asyncio.Event() async def serve(reader, writer): try: headers = await reader.readuntil(b"\r\n\r\n") length = next(int(line.split(b":", 1)[1]) for line in headers.splitlines() if line.lower().startswith(b"content-length:")) await reader.readexactly(length) writer.write(b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n" b"Transfer-Encoding: chunked\r\n\r\n") await writer.drain() connected.set() assert await reader.read() == b"" disconnected.set() finally: writer.close() await writer.wait_closed() server = await asyncio.start_server(serve, "127.0.0.1", 0) port = server.sockets[0].getsockname()[1] model = GatewayProxyChatModel(gateway_url=f"http://127.0.0.1:{port}", run_id="test", envelope_signature="test") async def consume(_state): async for _ in model.astream([HumanMessage(content="test")]): pytest.fail("silent test stream should produce no model output") return {} builder = StateGraph(dict) builder.add_node("model", consume) builder.add_edge(START, "model") builder.add_edge("model", END) task = asyncio.create_task(builder.compile().ainvoke({})) try: await asyncio.wait_for(connected.wait(), 3) task.cancel() with pytest.raises(asyncio.CancelledError): await task await asyncio.wait_for(disconnected.wait(), 3) finally: task.cancel() await asyncio.gather(task, return_exceptions=True) server.close() await server.wait_closed()