test_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  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(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_rejects_unaccepted_session_prompt_response(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 == "session/prompt":
  323. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
  324. elif method == "shutdown":
  325. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  326. break
  327. """.strip()
  328. )
  329. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  330. client.initialize(cwd="/workspace", model="dsagent")
  331. with pytest.raises(ValueError):
  332. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  333. def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
  334. script = tmp_path / "fake_bridge.py"
  335. script.write_text(
  336. """
  337. import json
  338. import sys
  339. for line in sys.stdin:
  340. msg = json.loads(line)
  341. method = msg.get("method")
  342. if method == "initialize":
  343. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  344. print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
  345. elif "id" in msg and "method" not in msg:
  346. print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
  347. elif method == "shutdown":
  348. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  349. break
  350. """.strip()
  351. )
  352. with HarnessClient(
  353. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  354. ) as client:
  355. client.initialize(cwd="/workspace", model="dsagent")
  356. request = client.next_request()
  357. assert request.id == "bridge-req-1"
  358. assert request.method == "llm.request"
  359. assert request.payload["requestId"] == "req-1"
  360. client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
  361. notification = client.next_notification()
  362. assert notification.method == "response/seen"
  363. assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
  364. def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
  365. script = tmp_path / "fake_bridge.py"
  366. script.write_text(
  367. """
  368. import json
  369. import sys
  370. print("node warning: experimental loader", flush=True)
  371. for line in sys.stdin:
  372. msg = json.loads(line)
  373. if msg.get("method") == "initialize":
  374. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  375. elif msg.get("method") == "shutdown":
  376. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  377. break
  378. """.strip()
  379. )
  380. with HarnessClient(
  381. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  382. ) as client:
  383. init = client.initialize(cwd="/workspace", model="dsagent")
  384. assert init.serverInfo.name == "fake-dsh"
  385. def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
  386. script = tmp_path / "fake_bridge.py"
  387. script.write_text(
  388. """
  389. import time
  390. time.sleep(60)
  391. """.strip()
  392. )
  393. with HarnessClient(
  394. HarnessConfig(
  395. launch_args_override=(sys.executable, str(script)),
  396. request_timeout_seconds=0.1,
  397. )
  398. ) as client:
  399. start = time.monotonic()
  400. try:
  401. client.initialize(cwd="/workspace", model="dsagent")
  402. except TimeoutError:
  403. assert time.monotonic() - start < 2
  404. else:
  405. raise AssertionError("initialize should time out")
  406. def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
  407. script = tmp_path / "fake_bridge.py"
  408. script.write_text(
  409. """
  410. import json
  411. import signal
  412. import sys
  413. import time
  414. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  415. for line in sys.stdin:
  416. msg = json.loads(line)
  417. if msg.get("method") == "initialize":
  418. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  419. elif msg.get("method") == "shutdown":
  420. time.sleep(60)
  421. """.strip()
  422. )
  423. client = HarnessClient(
  424. HarnessConfig(
  425. launch_args_override=(sys.executable, str(script)),
  426. shutdown_timeout_seconds=0.1,
  427. )
  428. )
  429. client.start()
  430. proc = client._proc
  431. assert proc is not None
  432. client.initialize(cwd="/workspace", model="dsagent")
  433. start = time.monotonic()
  434. client.close()
  435. assert time.monotonic() - start < 2
  436. assert proc.poll() is not None
  437. assert client._proc is None
  438. def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
  439. script = tmp_path / "rejecting_runtime.py"
  440. script.write_text(
  441. """
  442. import json
  443. import sys
  444. for line in sys.stdin:
  445. msg = json.loads(line)
  446. if msg.get("method") == "initialize":
  447. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
  448. elif msg.get("method") == "shutdown":
  449. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  450. break
  451. """.strip()
  452. )
  453. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  454. client.start()
  455. proc = client._proc
  456. assert proc is not None
  457. with pytest.raises(Exception, match="bad initialize"):
  458. client.initialize(cwd=".", model="dsagent")
  459. assert proc.wait(timeout=1) is not None
  460. assert client._proc is None
  461. def test_public_signatures_omit_unsupported_wire_parameters() -> None:
  462. from deepseek_harness import DeepSeekHarnessConfig, Session
  463. assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
  464. assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
  465. assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
  466. assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
  467. assert "profile" not in inspect.signature(Session.run).parameters
  468. assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
  469. assert "client_name" not in HarnessConfig.__dataclass_fields__
  470. assert "client_version" not in HarnessConfig.__dataclass_fields__
  471. def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
  472. HarnessClient().close()
  473. script = tmp_path / "fake_bridge.py"
  474. script.write_text(
  475. """
  476. import json
  477. import sys
  478. for line in sys.stdin:
  479. msg = json.loads(line)
  480. if msg.get("method") == "initialize":
  481. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  482. elif msg.get("method") == "shutdown":
  483. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  484. break
  485. """.strip()
  486. )
  487. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  488. client.start()
  489. client.initialize(cwd="/workspace", model="dsagent")
  490. client.close()
  491. client.close()
  492. def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
  493. script = tmp_path / "crashing_runtime.py"
  494. script.write_text(
  495. """
  496. import sys
  497. print("fatal bridge exploded", file=sys.stderr, flush=True)
  498. sys.exit(42)
  499. """.strip()
  500. )
  501. with HarnessClient(
  502. HarnessConfig(
  503. launch_args_override=(sys.executable, str(script)),
  504. request_timeout_seconds=2,
  505. )
  506. ) as client:
  507. with pytest.raises(Exception, match="fatal bridge exploded"):
  508. client.initialize(cwd="/workspace", model="dsagent")
  509. def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
  510. script = tmp_path / "fake_bridge.py"
  511. output = tmp_path / "seen.jsonl"
  512. script.write_text(
  513. """
  514. import json
  515. import os
  516. import sys
  517. with open(os.environ["SEEN"], "w") as seen:
  518. for line in sys.stdin:
  519. seen.write(line)
  520. seen.flush()
  521. msg = json.loads(line)
  522. if "id" in msg and msg.get("method") == "initialize":
  523. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  524. elif "id" in msg and msg.get("method") == "shutdown":
  525. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  526. break
  527. """.strip()
  528. )
  529. with HarnessClient(
  530. HarnessConfig(
  531. launch_args_override=(sys.executable, str(script)),
  532. env={"SEEN": str(output)},
  533. )
  534. ) as client:
  535. client.initialize(cwd="/workspace", model="dsagent")
  536. threads = [
  537. threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
  538. for index in range(50)
  539. ]
  540. for thread in threads:
  541. thread.start()
  542. for thread in threads:
  543. thread.join()
  544. for line in output.read_text().splitlines():
  545. json.loads(line)
  546. def _install_fake_bundled_runtime(
  547. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  548. ) -> Path:
  549. """Fake the deepseek-harness-runtime-bin package on sys.path.
  550. A stub exe that dumps DSH_CORDIS_CONFIG to $ENV_DUMP before serving
  551. initialize/shutdown, plus a module exposing the resolution surface the
  552. client consumes. Returns the fake bundled default config path.
  553. """
  554. runtime = tmp_path / "dsh-jsonrpc-agent"
  555. runtime.write_text(
  556. """#!/usr/bin/env python3
  557. import json
  558. import os
  559. import sys
  560. json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
  561. for line in sys.stdin:
  562. msg = json.loads(line)
  563. if msg.get("method") == "initialize":
  564. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
  565. elif msg.get("method") == "shutdown":
  566. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  567. break
  568. """.strip()
  569. )
  570. runtime.chmod(0o755)
  571. default_config = tmp_path / "default-cordis.yml"
  572. module_dir = tmp_path / "deepseek_harness_runtime"
  573. module_dir.mkdir()
  574. (module_dir / "__init__.py").write_text(
  575. f"""
  576. def resolve_bundled_launch_args(mode=None):
  577. return ({str(runtime)!r},)
  578. def bundled_default_config_path():
  579. return {str(default_config)!r}
  580. """.strip()
  581. )
  582. monkeypatch.syspath_prepend(str(tmp_path))
  583. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  584. return default_config
  585. @pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
  586. def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
  587. tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
  588. ) -> None:
  589. env_dump = tmp_path / "env.json"
  590. default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
  591. if ambient_config is None:
  592. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  593. else:
  594. monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
  595. with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
  596. init = client.initialize(cwd="/workspace", model="deepseek-v4-pro")
  597. assert init.serverInfo.name == "bundled-runtime"
  598. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
  599. def test_client_respects_explicit_config_over_bundled_default(
  600. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  601. ) -> None:
  602. env_dump = tmp_path / "env.json"
  603. _install_fake_bundled_runtime(tmp_path, monkeypatch)
  604. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  605. with HarnessClient(
  606. HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
  607. ) as client:
  608. client.initialize(cwd="/workspace", model="deepseek-v4-pro")
  609. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
  610. def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
  611. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  612. monkeypatch.setattr(sys, "path", [])
  613. with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
  614. HarnessClient().start()