22import anyio
33
44from lf_toolkit .io .stream_io import StreamIO , PrefixStreamIO , StreamServer
5+ from lf_toolkit .io .stdio_server import StdioServer
56
67
78@pytest .fixture
89def anyio_backend ():
910 return "asyncio"
1011
1112
13+ # ---------------------------------------------------------------------------
14+ # Helpers
15+ # ---------------------------------------------------------------------------
1216
1317def make_framed_message (payload : str ) -> bytes :
1418 """Wrap a JSON string in Content-Length framing."""
@@ -45,80 +49,29 @@ async def close(self):
4549 self .close_count += 1
4650
4751
48- class EchoServer (StreamServer ):
49- """
50- Concrete StreamServer for testing.
51- - run() is required by BaseServer (abstract) but not used in tests
52- since we call _handle_client directly.
53- - dispatch() is overridden to echo the raw request back, bypassing
54- the real JsonRpcHandler so tests stay self-contained.
55- """
56-
57- async def run (self ):
58- pass
59-
60- async def dispatch (self , data : str ) -> str :
61- return data
62-
63-
64- class BuggyStreamServer (StreamServer ):
65- """
66- Reproduces the original bug by overriding _handle_client with
67- close() inside the finally block.
68- """
69-
70- async def run (self ):
71- pass
72-
73- async def dispatch (self , data : str ) -> str :
74- return data
75-
76- async def _handle_client (self , client : StreamIO ):
77- io = self .wrap_io (client )
78- while True :
79- try :
80- data = await io .read (4096 )
81- if not data :
82- break
83- response = await self .dispatch (data .decode ("utf-8" ))
84- await io .write (response .encode ("utf-8" ))
85- except anyio .EndOfStream :
86- break
87- except anyio .ClosedResourceError :
88- break
89- except Exception as e :
90- print (f"Exception: { e } " )
91- finally :
92- await client .close () # BUG: closes after every message
93-
94-
9552# ---------------------------------------------------------------------------
9653# Tests
9754# ---------------------------------------------------------------------------
9855
99- class TestStreamServer :
56+ class TestStdioServer :
10057
10158 @pytest .fixture
10259 def stream (self ):
10360 return FakeStreamIO ()
10461
10562 @pytest .fixture
10663 def server (self ):
107- return EchoServer ()
108-
109- @pytest .fixture
110- def buggy_server (self ):
111- return BuggyStreamServer ()
64+ return StdioServer ()
11265
11366 @pytest .mark .anyio
11467 async def test_handles_multiple_messages (self , stream , server ):
11568 """
11669 Core fix test: the server must process multiple messages in a single
11770 session without closing the connection between them.
11871 """
119- stream .feed (make_framed_message ('{"command": " eval", " id": 1}' ))
120- stream .feed (make_framed_message ('{"command": " eval", " id": 2}' ))
121- stream .feed (make_framed_message ('{"command": " eval", " id": 3}' ))
72+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":" eval","params":{}," id":1}' ))
73+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":" eval","params":{}," id":2}' ))
74+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":" eval","params":{}," id":3}' ))
12275
12376 await server ._handle_client (stream )
12477
@@ -133,8 +86,8 @@ async def test_closes_only_once(self, stream, server):
13386 The client connection should be closed exactly once — after the loop
13487 exits — not once per message.
13588 """
136- stream .feed (make_framed_message ('{"id": 1}' ))
137- stream .feed (make_framed_message ('{"id": 2}' ))
89+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":"eval","params":{}," id":1}' ))
90+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":"eval","params":{}," id":2}' ))
13891
13992 await server ._handle_client (stream )
14093
@@ -143,32 +96,17 @@ async def test_closes_only_once(self, stream, server):
14396 f"{ stream .close_count } times. This is the original bug."
14497 )
14598
146- @pytest .mark .anyio
147- async def test_buggy_server_closes_after_each_message (self , stream , buggy_server ):
148- """
149- Demonstrates the original bug: close() in the finally block causes
150- the stream to be closed after every message, not just at the end.
151- """
152- stream .feed (make_framed_message ('{"id": 1}' ))
153- stream .feed (make_framed_message ('{"id": 2}' ))
154-
155- await buggy_server ._handle_client (stream )
156-
157- assert stream .close_count > 1 , (
158- "Expected buggy server to call close() more than once, "
159- "confirming the bug exists in the original code."
160- )
161-
16299 @pytest .mark .anyio
163100 async def test_single_message (self , stream , server ):
164101 """A single message round-trip should work correctly."""
165- payload = '{"command": "eval", "response": "test"}'
166- stream .feed (make_framed_message (payload ))
102+ stream .feed (make_framed_message ('{"jsonrpc":"2.0","method":"eval","params":{},"id":1}' ))
167103
168104 await server ._handle_client (stream )
169105
170106 assert len (stream .responses ) == 1
171- assert payload .encode () in stream .responses [0 ]
107+ # Response is a framed JSON-RPC envelope
108+ assert b"Content-Length:" in stream .responses [0 ]
109+ assert b"jsonrpc" in stream .responses [0 ]
172110
173111 @pytest .mark .anyio
174112 async def test_closes_on_empty_stream (self , stream , server ):
@@ -179,10 +117,10 @@ async def test_closes_on_empty_stream(self, stream, server):
179117
180118 @pytest .mark .anyio
181119 async def test_response_content (self , stream , server ):
182- """Verify the actual response content is correct across messages ."""
120+ """Verify a response is returned for each message sent ."""
183121 messages = [
184- '{"id": 1, "command": "eval"}' ,
185- '{"id": 2, "command": "preview"}' ,
122+ '{"jsonrpc":"2.0","method": "eval","params":{},"id":1 }' ,
123+ '{"jsonrpc":"2.0","method": "preview","params":{},"id":2 }' ,
186124 ]
187125
188126 for msg in messages :
@@ -191,8 +129,9 @@ async def test_response_content(self, stream, server):
191129 await server ._handle_client (stream )
192130
193131 assert len (stream .responses ) == 2
194- for i , msg in enumerate (messages ):
195- assert msg .encode () in stream .responses [i ]
132+ for response in stream .responses :
133+ assert b"Content-Length:" in response
134+ assert b"jsonrpc" in response
196135
197136
198137class TestPrefixStreamIO :
0 commit comments