test_client.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. from __future__ import annotations
  2. import json
  3. import inspect
  4. import sys
  5. import threading
  6. import time
  7. from pathlib import Path
  8. import pytest
  9. from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
  10. def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
  11. script = tmp_path / "fake_runtime.py"
  12. env_dump = tmp_path / "env.json"
  13. script.write_text(
  14. """
  15. import json
  16. import os
  17. import sys
  18. env_dump = os.environ["ENV_DUMP"]
  19. json.dump({
  20. "DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
  21. "DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
  22. "DSH_CWD": os.environ.get("DSH_CWD"),
  23. "DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
  24. "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
  25. }, open(env_dump, "w"))
  26. for line in sys.stdin:
  27. msg = json.loads(line)
  28. method = msg.get("method")
  29. if method == "initialize":
  30. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  31. elif method == "session/prompt":
  32. params = msg.get("params") or {}
  33. print(json.dumps({
  34. "jsonrpc": "2.0",
  35. "method": "session.event",
  36. "params": {
  37. "sessionId": params["sessionId"],
  38. "event": {
  39. "type": "assistant/message",
  40. "data": {"content": [{"type": "text", "text": "hello from runtime"}]},
  41. },
  42. },
  43. }), flush=True)
  44. print(json.dumps({
  45. "jsonrpc": "2.0",
  46. "method": "session.finished",
  47. "params": {"sessionId": params["sessionId"], "status": "ok"},
  48. }), flush=True)
  49. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  50. elif method == "shutdown":
  51. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  52. break
  53. """.strip()
  54. )
  55. with DeepSeekHarness(
  56. model="deepseek-v4-flash",
  57. cwd=str(tmp_path),
  58. cordis=str(tmp_path / "cordis.yml"),
  59. session_root=str(tmp_path / "sessions"),
  60. launch_args_override=(sys.executable, str(script)),
  61. env={
  62. "ENV_DUMP": str(env_dump),
  63. "DEEPSEEK_API_KEY": "env-key",
  64. "DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
  65. },
  66. ) as harness:
  67. result = harness.run("say hello", session_id="main")
  68. assert result.status == "ok"
  69. assert result.final_response == "hello from runtime"
  70. assert result.events[0]["type"] == "assistant/message"
  71. dumped_env = json.loads(env_dump.read_text())
  72. assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
  73. assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
  74. assert dumped_env["DSH_CWD"] == str(tmp_path)
  75. assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions")
  76. assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml")
  77. def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
  78. script = tmp_path / "fake_runtime.py"
  79. script.write_text(
  80. """
  81. import json
  82. import sys
  83. for line in sys.stdin:
  84. msg = json.loads(line)
  85. method = msg.get("method")
  86. if method == "initialize":
  87. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  88. elif method == "session/prompt":
  89. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  90. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
  91. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  92. elif method == "shutdown":
  93. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  94. break
  95. """.strip()
  96. )
  97. seen: list[str] = []
  98. with DeepSeekHarness(
  99. launch_args_override=(sys.executable, str(script)),
  100. cwd=str(tmp_path),
  101. ) as harness:
  102. session = harness.start_session("main")
  103. result = session.run(
  104. "spawn a helper",
  105. on_notification=lambda notification: seen.append(notification.method),
  106. )
  107. assert result.status == "ok"
  108. assert seen == ["subagent.started", "session.finished"]
  109. def test_relative_cwd_is_absolute_in_process_environment_and_wire(
  110. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  111. ) -> None:
  112. script = tmp_path / "capture_cwd.py"
  113. capture = tmp_path / "cwd.json"
  114. script.write_text(
  115. """
  116. import json
  117. import os
  118. import sys
  119. for line in sys.stdin:
  120. msg = json.loads(line)
  121. if msg.get("method") == "initialize":
  122. json.dump({"process": os.getcwd(), "environment": os.environ.get("DSH_CWD"), "wire": msg["params"]["cwd"]}, open(os.environ["CAPTURE"], "w"))
  123. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  124. elif msg.get("method") == "shutdown":
  125. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  126. break
  127. """.strip()
  128. )
  129. monkeypatch.chdir(tmp_path)
  130. with DeepSeekHarness(
  131. cwd=".",
  132. runtime_cwd=".",
  133. launch_args_override=(sys.executable, str(script)),
  134. env={"CAPTURE": str(capture)},
  135. ):
  136. pass
  137. expected = str(tmp_path.resolve())
  138. assert json.loads(capture.read_text()) == {
  139. "process": expected,
  140. "environment": expected,
  141. "wire": expected,
  142. }
  143. def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
  144. script = tmp_path / "fake_runtime.py"
  145. script.write_text(
  146. """
  147. import json
  148. import sys
  149. for line in sys.stdin:
  150. msg = json.loads(line)
  151. method = msg.get("method")
  152. if method == "initialize":
  153. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  154. elif method == "session/prompt":
  155. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  156. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
  157. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
  158. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  159. elif method == "shutdown":
  160. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  161. break
  162. """.strip()
  163. )
  164. with DeepSeekHarness(
  165. launch_args_override=(sys.executable, str(script)),
  166. cwd=str(tmp_path),
  167. ) as harness:
  168. result = harness.run("spawn a helper", session_id="main")
  169. assert result.status == "ok"
  170. assert [notification.method for notification in result.notifications] == [
  171. "subagent.started",
  172. "subagent.finished",
  173. "session.finished",
  174. ]
  175. def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
  176. script = tmp_path / "fake_runtime.py"
  177. script.write_text(
  178. """
  179. import json
  180. import sys
  181. for line in sys.stdin:
  182. msg = json.loads(line)
  183. method = msg.get("method")
  184. if method == "initialize":
  185. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  186. elif method == "session/prompt":
  187. params = msg.get("params") or {}
  188. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "other", "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "wrong session"}]}}}}), flush=True)
  189. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "other", "status": "ok"}}), flush=True)
  190. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "right session"}]}}}}), flush=True)
  191. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
  192. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  193. elif method == "shutdown":
  194. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  195. break
  196. """.strip()
  197. )
  198. with DeepSeekHarness(
  199. launch_args_override=(sys.executable, str(script)),
  200. cwd=str(tmp_path),
  201. ) as harness:
  202. result = harness.run("stay in your lane", session_id="main")
  203. assert result.status == "ok"
  204. assert result.final_response == "right session"
  205. assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main", "main"]
  206. def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
  207. script = tmp_path / "fake_runtime.py"
  208. script.write_text(
  209. """
  210. import json
  211. import sys
  212. for line in sys.stdin:
  213. msg = json.loads(line)
  214. method = msg.get("method")
  215. if method == "initialize":
  216. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  217. elif method == "session/prompt":
  218. params = msg.get("params") or {}
  219. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "ok"}]}}}}), flush=True)
  220. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
  221. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  222. elif method == "shutdown":
  223. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  224. break
  225. """.strip()
  226. )
  227. with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  228. result = harness.run("one turn", session_id="main")
  229. assert result.status == "ok"
  230. assert harness.client._notifications.qsize() == 0
  231. def test_session_run_waits_for_late_finished_without_replaying_stale_notifications(tmp_path: Path) -> None:
  232. script = tmp_path / "fake_runtime.py"
  233. script.write_text(
  234. """
  235. import json
  236. import sys
  237. import time
  238. turn = 0
  239. for line in sys.stdin:
  240. msg = json.loads(line)
  241. method = msg.get("method")
  242. if method == "initialize":
  243. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  244. elif method == "session/prompt":
  245. turn += 1
  246. params = msg.get("params") or {}
  247. session_id = params["sessionId"]
  248. if turn == 1:
  249. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "first"}]}}}}), flush=True)
  250. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
  251. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  252. else:
  253. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  254. time.sleep(0.05)
  255. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "second"}]}}}}), flush=True)
  256. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
  257. elif method == "shutdown":
  258. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  259. break
  260. """.strip()
  261. )
  262. with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  263. first = harness.run("first turn", session_id="main")
  264. second = harness.run("second turn", session_id="main")
  265. assert first.final_response == "first"
  266. assert second.final_response == "second"
  267. assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main", "main"]
  268. def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
  269. script = tmp_path / "fake_bridge.py"
  270. script.write_text(
  271. """
  272. import json
  273. import sys
  274. for line in sys.stdin:
  275. msg = json.loads(line)
  276. method = msg.get("method")
  277. if method == "initialize":
  278. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  279. elif method == "session/prompt":
  280. params = msg.get("params") or {}
  281. print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
  282. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  283. elif method == "shutdown":
  284. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  285. break
  286. """.strip()
  287. )
  288. with HarnessClient(
  289. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  290. ) as client:
  291. init = client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  292. assert init.serverInfo.name == "fake-dsh"
  293. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  294. notification = client.next_notification()
  295. assert notification.method == "llm/request"
  296. assert notification.payload["requestId"] == "req-1"
  297. assert notification.payload["sessionId"] == "main"
  298. def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
  299. client = HarnessClient()
  300. with client.subscribe_session_notifications("main"):
  301. client._handle_message({
  302. "jsonrpc": "2.0",
  303. "method": "session.event",
  304. "params": {"sessionId": "other", "event": {"type": "assistant/message"}},
  305. })
  306. assert client._notifications.qsize() == 1
  307. notification = client._notifications.get_nowait()
  308. assert not isinstance(notification, BaseException)
  309. assert notification.method == "session.event"
  310. assert notification.payload["sessionId"] == "other"
  311. def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
  312. script = tmp_path / "fake_bridge.py"
  313. script.write_text(
  314. """
  315. import json
  316. import sys
  317. for line in sys.stdin:
  318. msg = json.loads(line)
  319. method = msg.get("method")
  320. if method == "initialize":
  321. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  322. elif method in {"emit-first", "emit-second"}:
  323. print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
  324. elif method == "session/prompt":
  325. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  326. elif method == "shutdown":
  327. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  328. break
  329. """.strip()
  330. )
  331. def broken_filter(_notification: object) -> bool:
  332. raise RuntimeError("bad notification filter")
  333. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  334. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  335. with (
  336. client.subscribe_notifications(broken_filter) as broken,
  337. client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
  338. ):
  339. client.notify("emit-first")
  340. with pytest.raises(RuntimeError, match="bad notification filter"):
  341. broken.next()
  342. assert healthy.next().payload == {"source": "emit-first"}
  343. assert client._notifications.qsize() == 0
  344. client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
  345. client.notify("emit-second")
  346. assert healthy.next().payload == {"source": "emit-second"}
  347. def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
  348. script = tmp_path / "fake_bridge.py"
  349. script.write_text(
  350. """
  351. import json
  352. import sys
  353. for line in sys.stdin:
  354. msg = json.loads(line)
  355. method = msg.get("method")
  356. if method == "initialize":
  357. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  358. elif method == "session/prompt":
  359. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
  360. elif method == "shutdown":
  361. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  362. break
  363. """.strip()
  364. )
  365. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  366. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  367. with pytest.raises(ValueError):
  368. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  369. def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
  370. script = tmp_path / "fake_bridge.py"
  371. script.write_text(
  372. """
  373. import json
  374. import sys
  375. for line in sys.stdin:
  376. msg = json.loads(line)
  377. method = msg.get("method")
  378. if method == "initialize":
  379. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  380. print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
  381. elif "id" in msg and "method" not in msg:
  382. print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
  383. elif method == "shutdown":
  384. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  385. break
  386. """.strip()
  387. )
  388. with HarnessClient(
  389. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  390. ) as client:
  391. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  392. request = client.next_request()
  393. assert request.id == "bridge-req-1"
  394. assert request.method == "llm.request"
  395. assert request.payload["requestId"] == "req-1"
  396. client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
  397. notification = client.next_notification()
  398. assert notification.method == "response/seen"
  399. assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
  400. def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
  401. script = tmp_path / "fake_bridge.py"
  402. script.write_text(
  403. """
  404. import json
  405. import sys
  406. print("node warning: experimental loader", flush=True)
  407. for line in sys.stdin:
  408. msg = json.loads(line)
  409. if msg.get("method") == "initialize":
  410. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  411. elif msg.get("method") == "shutdown":
  412. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  413. break
  414. """.strip()
  415. )
  416. with HarnessClient(
  417. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  418. ) as client:
  419. init = client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  420. assert init.serverInfo.name == "fake-dsh"
  421. def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
  422. script = tmp_path / "fake_bridge.py"
  423. script.write_text(
  424. """
  425. import time
  426. time.sleep(60)
  427. """.strip()
  428. )
  429. with HarnessClient(
  430. HarnessConfig(
  431. launch_args_override=(sys.executable, str(script)),
  432. request_timeout_seconds=0.1,
  433. )
  434. ) as client:
  435. start = time.monotonic()
  436. try:
  437. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  438. except TimeoutError:
  439. assert time.monotonic() - start < 2
  440. else:
  441. raise AssertionError("initialize should time out")
  442. def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
  443. script = tmp_path / "fake_bridge.py"
  444. script.write_text(
  445. """
  446. import json
  447. import signal
  448. import sys
  449. import time
  450. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  451. for line in sys.stdin:
  452. msg = json.loads(line)
  453. if msg.get("method") == "initialize":
  454. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  455. elif msg.get("method") == "shutdown":
  456. time.sleep(60)
  457. """.strip()
  458. )
  459. client = HarnessClient(
  460. HarnessConfig(
  461. launch_args_override=(sys.executable, str(script)),
  462. shutdown_timeout_seconds=0.1,
  463. )
  464. )
  465. client.start()
  466. proc = client._proc
  467. assert proc is not None
  468. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  469. start = time.monotonic()
  470. client.close()
  471. assert time.monotonic() - start < 2
  472. assert proc.poll() is not None
  473. assert client._proc is None
  474. def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
  475. script = tmp_path / "rejecting_runtime.py"
  476. script.write_text(
  477. """
  478. import json
  479. import sys
  480. for line in sys.stdin:
  481. msg = json.loads(line)
  482. if msg.get("method") == "initialize":
  483. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
  484. elif msg.get("method") == "shutdown":
  485. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  486. break
  487. """.strip()
  488. )
  489. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  490. client.start()
  491. proc = client._proc
  492. assert proc is not None
  493. with pytest.raises(Exception, match="bad initialize"):
  494. client.initialize(provider="deepseek", cwd=".", model="dsagent")
  495. assert proc.wait(timeout=1) is not None
  496. assert client._proc is None
  497. def test_public_signatures_omit_unsupported_wire_parameters() -> None:
  498. from deepseek_harness import DeepSeekHarnessConfig, Session
  499. assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
  500. assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
  501. assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
  502. assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
  503. assert "profile" not in inspect.signature(Session.run).parameters
  504. assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
  505. assert "client_name" not in HarnessConfig.__dataclass_fields__
  506. assert "client_version" not in HarnessConfig.__dataclass_fields__
  507. def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
  508. HarnessClient().close()
  509. script = tmp_path / "fake_bridge.py"
  510. script.write_text(
  511. """
  512. import json
  513. import sys
  514. for line in sys.stdin:
  515. msg = json.loads(line)
  516. if msg.get("method") == "initialize":
  517. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  518. elif msg.get("method") == "shutdown":
  519. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  520. break
  521. """.strip()
  522. )
  523. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  524. client.start()
  525. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  526. client.close()
  527. client.close()
  528. def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
  529. script = tmp_path / "crashing_runtime.py"
  530. script.write_text(
  531. """
  532. import sys
  533. print("fatal bridge exploded", file=sys.stderr, flush=True)
  534. sys.exit(42)
  535. """.strip()
  536. )
  537. with HarnessClient(
  538. HarnessConfig(
  539. launch_args_override=(sys.executable, str(script)),
  540. request_timeout_seconds=2,
  541. )
  542. ) as client:
  543. with pytest.raises(Exception, match="fatal bridge exploded"):
  544. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  545. def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
  546. script = tmp_path / "fake_bridge.py"
  547. output = tmp_path / "seen.jsonl"
  548. script.write_text(
  549. """
  550. import json
  551. import os
  552. import sys
  553. with open(os.environ["SEEN"], "w") as seen:
  554. for line in sys.stdin:
  555. seen.write(line)
  556. seen.flush()
  557. msg = json.loads(line)
  558. if "id" in msg and msg.get("method") == "initialize":
  559. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  560. elif "id" in msg and msg.get("method") == "shutdown":
  561. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  562. break
  563. """.strip()
  564. )
  565. with HarnessClient(
  566. HarnessConfig(
  567. launch_args_override=(sys.executable, str(script)),
  568. env={"SEEN": str(output)},
  569. )
  570. ) as client:
  571. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  572. threads = [
  573. threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
  574. for index in range(50)
  575. ]
  576. for thread in threads:
  577. thread.start()
  578. for thread in threads:
  579. thread.join()
  580. for line in output.read_text().splitlines():
  581. json.loads(line)
  582. def _install_fake_bundled_runtime(
  583. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  584. ) -> Path:
  585. """Install a fake runtime package that records config and serves lifecycle calls.
  586. Returns the fake bundled default config path.
  587. """
  588. runtime = tmp_path / "dsh-jsonrpc-agent"
  589. runtime.write_text(
  590. """#!/usr/bin/env python3
  591. import json
  592. import os
  593. import sys
  594. json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
  595. for line in sys.stdin:
  596. msg = json.loads(line)
  597. if msg.get("method") == "initialize":
  598. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
  599. elif msg.get("method") == "shutdown":
  600. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  601. break
  602. """.strip()
  603. )
  604. runtime.chmod(0o755)
  605. default_config = tmp_path / "default-cordis.yml"
  606. module_dir = tmp_path / "deepseek_harness_runtime"
  607. module_dir.mkdir()
  608. (module_dir / "__init__.py").write_text(
  609. f"""
  610. def resolve_bundled_launch_args(mode=None):
  611. return ({str(runtime)!r},)
  612. def bundled_default_config_path():
  613. return {str(default_config)!r}
  614. """.strip()
  615. )
  616. monkeypatch.syspath_prepend(str(tmp_path))
  617. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  618. return default_config
  619. @pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
  620. def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
  621. tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
  622. ) -> None:
  623. env_dump = tmp_path / "env.json"
  624. default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
  625. if ambient_config is None:
  626. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  627. else:
  628. monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
  629. with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
  630. init = client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  631. assert init.serverInfo.name == "bundled-runtime"
  632. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
  633. def test_client_respects_explicit_config_over_bundled_default(
  634. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  635. ) -> None:
  636. env_dump = tmp_path / "env.json"
  637. _install_fake_bundled_runtime(tmp_path, monkeypatch)
  638. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  639. with HarnessClient(
  640. HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
  641. ) as client:
  642. client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  643. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
  644. def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
  645. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  646. monkeypatch.setattr(sys, "path", [])
  647. with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
  648. HarnessClient().start()