Skip to content

Commit b799415

Browse files
committed
fix: make stdio smoke test deterministic
1 parent f819d54 commit b799415

1 file changed

Lines changed: 78 additions & 23 deletions

File tree

scripts/smoke_stdio.py

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,107 @@
55

66
import json
77
import os
8+
import selectors
89
import subprocess
910
import sys
11+
import time
1012
from pathlib import Path
13+
from typing import TextIO
1114

1215
EXPECTED_TOOLS = {"get_gateway_status", "list_models", "transcribe_file"}
1316

1417

18+
def send_message(stream: TextIO, message: dict) -> None:
19+
stream.write(json.dumps(message) + "\n")
20+
stream.flush()
21+
22+
23+
def read_response(stream: TextIO, request_id: int, *, timeout: float = 10.0) -> dict:
24+
"""Wait for one JSON-RPC response without closing stdin and racing server shutdown."""
25+
selector = selectors.DefaultSelector()
26+
selector.register(stream, selectors.EVENT_READ)
27+
deadline = time.monotonic() + timeout
28+
try:
29+
while True:
30+
remaining = deadline - time.monotonic()
31+
if remaining <= 0:
32+
break
33+
if not selector.select(remaining):
34+
break
35+
line = stream.readline()
36+
if not line:
37+
break
38+
response = json.loads(line)
39+
if response.get("id") == request_id:
40+
return response
41+
finally:
42+
selector.close()
43+
raise TimeoutError(f"MCP server did not return JSON-RPC response {request_id}")
44+
45+
1546
def main() -> None:
1647
if len(sys.argv) != 2:
1748
raise SystemExit("usage: smoke_stdio.py /path/to/vocagateway-mcp")
1849

1950
executable = Path(sys.argv[1]).resolve(strict=True)
20-
messages = [
21-
{
22-
"jsonrpc": "2.0",
23-
"id": 1,
24-
"method": "initialize",
25-
"params": {
26-
"protocolVersion": "2024-11-05",
27-
"capabilities": {},
28-
"clientInfo": {"name": "ci-smoke", "version": "1"},
29-
},
30-
},
31-
{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
32-
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
33-
]
34-
payload = "\n".join(json.dumps(message) for message in messages) + "\n"
3551
environment = os.environ.copy()
3652
environment.update(
3753
{
3854
"VOCAGATEWAY_URL": "https://gateway.example.test",
3955
"VOCAGATEWAY_TOKEN": "ci-token-with-at-least-thirty-two-characters",
4056
}
4157
)
42-
completed = subprocess.run(
58+
process = subprocess.Popen(
4359
[str(executable)],
44-
input=payload,
60+
stdin=subprocess.PIPE,
61+
stdout=subprocess.PIPE,
62+
stderr=subprocess.PIPE,
4563
text=True,
46-
capture_output=True,
47-
check=True,
48-
timeout=10,
64+
bufsize=1,
4965
env=environment,
5066
)
51-
responses = [json.loads(line) for line in completed.stdout.splitlines() if line.strip()]
52-
initialize = next(response for response in responses if response.get("id") == 1)
53-
listed = next(response for response in responses if response.get("id") == 2)
67+
assert process.stdin is not None
68+
assert process.stdout is not None
69+
assert process.stderr is not None
70+
try:
71+
send_message(
72+
process.stdin,
73+
{
74+
"jsonrpc": "2.0",
75+
"id": 1,
76+
"method": "initialize",
77+
"params": {
78+
"protocolVersion": "2024-11-05",
79+
"capabilities": {},
80+
"clientInfo": {"name": "ci-smoke", "version": "1"},
81+
},
82+
},
83+
)
84+
initialize = read_response(process.stdout, 1)
85+
86+
send_message(
87+
process.stdin,
88+
{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
89+
)
90+
send_message(
91+
process.stdin,
92+
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
93+
)
94+
listed = read_response(process.stdout, 2)
95+
finally:
96+
process.stdin.close()
97+
98+
try:
99+
return_code = process.wait(timeout=10)
100+
except subprocess.TimeoutExpired:
101+
process.kill()
102+
raise
103+
if return_code:
104+
raise subprocess.CalledProcessError(
105+
return_code,
106+
process.args,
107+
stderr=process.stderr.read(),
108+
)
54109

55110
assert initialize["result"]["serverInfo"]["name"] == "VocaGateway"
56111
names = {tool["name"] for tool in listed["result"]["tools"]}

0 commit comments

Comments
 (0)