test_client.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088
  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, Notification, RunResult, SdkProtocolError
  10. from deepseek_harness.errors import JsonRpcError
  11. def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
  12. script = tmp_path / "fake_runtime.py"
  13. env_dump = tmp_path / "env.json"
  14. init_dump = tmp_path / "init.json"
  15. script.write_text(
  16. """
  17. import json
  18. import os
  19. import sys
  20. env_dump = os.environ["ENV_DUMP"]
  21. json.dump({
  22. "DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
  23. "DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
  24. "DSH_CWD": os.environ.get("DSH_CWD"),
  25. "DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
  26. "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
  27. }, open(env_dump, "w"))
  28. for line in sys.stdin:
  29. msg = json.loads(line)
  30. method = msg.get("method")
  31. if method == "initialize":
  32. json.dump(msg.get("params"), open(os.environ["INIT_DUMP"], "w"))
  33. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  34. elif method == "session/prompt":
  35. params = msg.get("params") or {}
  36. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  37. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
  38. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  39. print(json.dumps({
  40. "jsonrpc": "2.0",
  41. "method": "session.event",
  42. "params": {
  43. "sessionId": params["sessionId"],
  44. "event": {
  45. "type": "image/offload",
  46. "data": {"turn": 1, "step": 1, "watermark": {"seq": 7, "path": [3]}},
  47. },
  48. },
  49. }), flush=True)
  50. print(json.dumps({
  51. "jsonrpc": "2.0",
  52. "method": "session.event",
  53. "params": {
  54. "sessionId": params["sessionId"],
  55. "event": {
  56. "type": "assistant/message",
  57. "data": {
  58. "message": {
  59. "role": "assistant",
  60. "content": [{"type": "text", "text": "hello from runtime"}],
  61. },
  62. },
  63. },
  64. },
  65. }), flush=True)
  66. print(json.dumps({
  67. "jsonrpc": "2.0",
  68. "method": "session.event",
  69. "params": {
  70. "sessionId": params["sessionId"],
  71. "event": {
  72. "type": "turn/end",
  73. "data": {"turn": 1, "reason": {"kind": "completed"}},
  74. },
  75. },
  76. }), flush=True)
  77. print(json.dumps({
  78. "jsonrpc": "2.0",
  79. "method": "session.event",
  80. "params": {
  81. "sessionId": params["sessionId"],
  82. "event": {
  83. "type": "turn/end",
  84. "data": {"turn": 2, "reason": {"kind": "max-tokens"}},
  85. },
  86. },
  87. }), flush=True)
  88. print(json.dumps({
  89. "jsonrpc": "2.0",
  90. "method": "session.status",
  91. "params": {"sessionId": params["sessionId"], "status": "idle"},
  92. }), flush=True)
  93. elif method == "shutdown":
  94. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  95. break
  96. """.strip()
  97. )
  98. with DeepSeekHarness(
  99. model="deepseek-v4-flash",
  100. reasoning_effort="max",
  101. max_tokens=4096,
  102. cwd=str(tmp_path),
  103. _launch_args=(sys.executable, str(script)),
  104. env={
  105. "ENV_DUMP": str(env_dump),
  106. "INIT_DUMP": str(init_dump),
  107. "DEEPSEEK_API_KEY": "env-key",
  108. "DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
  109. },
  110. ) as harness:
  111. result = harness.run("say hello", session_id="main")
  112. assert result.final_response == "hello from runtime"
  113. assert result.finish_reason == "max-tokens"
  114. assert result.events[1] == {
  115. "type": "image/offload",
  116. "data": {"turn": 1, "step": 1, "watermark": {"seq": 7, "path": [3]}},
  117. }
  118. assert result.events[-1]["type"] == "turn/end"
  119. dumped_env = json.loads(env_dump.read_text())
  120. assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
  121. assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
  122. assert dumped_env["DSH_CWD"] is None
  123. assert dumped_env["DSH_SESSION_ROOT"] is None
  124. assert dumped_env["DSH_CORDIS_CONFIG"] is None
  125. assert json.loads(init_dump.read_text()) == {
  126. "cwd": str(tmp_path),
  127. "provider": "deepseek-official",
  128. "model": "deepseek-v4-flash",
  129. "reasoningEffort": "max",
  130. "maxTokens": 4096,
  131. }
  132. def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
  133. script = tmp_path / "fake_runtime.py"
  134. script.write_text(
  135. """
  136. import json
  137. import sys
  138. for line in sys.stdin:
  139. msg = json.loads(line)
  140. method = msg.get("method")
  141. if method == "initialize":
  142. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  143. elif method == "session/prompt":
  144. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "main", "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  145. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "running"}}), flush=True)
  146. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  147. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  148. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "idle"}}), flush=True)
  149. elif method == "shutdown":
  150. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  151. break
  152. """.strip()
  153. )
  154. seen: list[str] = []
  155. with DeepSeekHarness(
  156. _launch_args=(sys.executable, str(script)),
  157. cwd=str(tmp_path),
  158. ) as harness:
  159. session = harness.start_session("main")
  160. result = session.run(
  161. "spawn a helper",
  162. on_notification=lambda notification: seen.append(notification.method),
  163. )
  164. assert seen == ["session.event", "session.status", "subagent.started", "session.status"]
  165. assert result.finish_reason is None
  166. def test_high_level_sdk_rejects_turn_end_without_reason_kind(tmp_path: Path) -> None:
  167. script = tmp_path / "fake_runtime.py"
  168. script.write_text(
  169. """
  170. import json
  171. import sys
  172. for line in sys.stdin:
  173. msg = json.loads(line)
  174. method = msg.get("method")
  175. if method == "initialize":
  176. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  177. elif method == "session/prompt":
  178. params = msg.get("params") or {}
  179. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  180. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  181. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "turn/end", "data": {"turn": 1, "reason": {}}}}}), flush=True)
  182. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
  183. elif method == "shutdown":
  184. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  185. break
  186. """.strip()
  187. )
  188. with DeepSeekHarness(
  189. _launch_args=(sys.executable, str(script)),
  190. cwd=str(tmp_path),
  191. ) as harness:
  192. with pytest.raises(
  193. SdkProtocolError,
  194. match=r"turn/end event requires a string data\.reason\.kind",
  195. ):
  196. harness.run("reject malformed turn ending", session_id="main")
  197. def test_relative_cwd_is_absolute_in_process_environment_and_wire(
  198. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  199. ) -> None:
  200. script = tmp_path / "capture_cwd.py"
  201. capture = tmp_path / "cwd.json"
  202. script.write_text(
  203. """
  204. import json
  205. import os
  206. import sys
  207. for line in sys.stdin:
  208. msg = json.loads(line)
  209. if msg.get("method") == "initialize":
  210. json.dump({"process": os.getcwd(), "environment": os.environ.get("DSH_CWD"), "wire": msg["params"]["cwd"]}, open(os.environ["CAPTURE"], "w"))
  211. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  212. elif msg.get("method") == "shutdown":
  213. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  214. break
  215. """.strip()
  216. )
  217. monkeypatch.chdir(tmp_path)
  218. with DeepSeekHarness(
  219. cwd=".",
  220. runtime_cwd=".",
  221. _launch_args=(sys.executable, str(script)),
  222. env={"CAPTURE": str(capture)},
  223. ):
  224. pass
  225. expected = str(tmp_path.resolve())
  226. assert json.loads(capture.read_text()) == {
  227. "process": expected,
  228. "environment": None,
  229. "wire": expected,
  230. }
  231. def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
  232. script = tmp_path / "fake_runtime.py"
  233. script.write_text(
  234. """
  235. import json
  236. import sys
  237. for line in sys.stdin:
  238. msg = json.loads(line)
  239. method = msg.get("method")
  240. if method == "initialize":
  241. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  242. elif method == "session/prompt":
  243. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "main", "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  244. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "running"}}), flush=True)
  245. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  246. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  247. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
  248. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "idle"}}), flush=True)
  249. elif method == "shutdown":
  250. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  251. break
  252. """.strip()
  253. )
  254. with DeepSeekHarness(
  255. _launch_args=(sys.executable, str(script)),
  256. cwd=str(tmp_path),
  257. ) as harness:
  258. result = harness.run("spawn a helper", session_id="main")
  259. assert [notification.method for notification in result.notifications] == [
  260. "session.event",
  261. "session.status",
  262. "subagent.started",
  263. "subagent.finished",
  264. "session.status",
  265. ]
  266. def test_session_run_collects_nested_subagent_tree_without_polluting_root_events(
  267. tmp_path: Path,
  268. ) -> None:
  269. script = tmp_path / "fake_runtime.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-runtime"}}}), flush=True)
  279. elif method == "session/prompt":
  280. root = (msg.get("params") or {})["sessionId"]
  281. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": root, "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  282. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": root, "status": "running"}}), flush=True)
  283. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  284. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": root, "childSessionId": "child"}}), flush=True)
  285. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "child", "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "child response"}]}}}}), flush=True)
  286. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "child", "childSessionId": "grandchild"}}), flush=True)
  287. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": "grandchild", "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "grandchild response"}]}}}}), flush=True)
  288. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "child", "childSessionId": "grandchild", "status": "ok"}}), flush=True)
  289. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": root, "childSessionId": "child", "status": "ok"}}), flush=True)
  290. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": root, "event": {"type": "assistant/message", "data": {"content": [{"type": "text", "text": "root response"}]}}}}), flush=True)
  291. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": root, "status": "idle"}}), flush=True)
  292. elif method == "shutdown":
  293. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  294. break
  295. """.strip()
  296. )
  297. seen: list[str] = []
  298. with DeepSeekHarness(
  299. _launch_args=(sys.executable, str(script)),
  300. cwd=str(tmp_path),
  301. ) as harness:
  302. result = harness.run(
  303. "delegate recursively",
  304. session_id="main",
  305. on_notification=lambda notification: seen.append(notification.method),
  306. )
  307. assert harness.client._notifications.qsize() == 0
  308. assert result.final_response == "root response"
  309. assert [event["data"]["content"][0]["text"] for event in result.events if event["type"] == "assistant/message"] == ["root response"]
  310. assert [notification.method for notification in result.notifications] == [
  311. "session.event",
  312. "session.status",
  313. "subagent.started",
  314. "session.event",
  315. "subagent.started",
  316. "session.event",
  317. "subagent.finished",
  318. "subagent.finished",
  319. "session.event",
  320. "session.status",
  321. ]
  322. assert seen == [notification.method for notification in result.notifications]
  323. def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
  324. script = tmp_path / "fake_runtime.py"
  325. script.write_text(
  326. """
  327. import json
  328. import sys
  329. for line in sys.stdin:
  330. msg = json.loads(line)
  331. method = msg.get("method")
  332. if method == "initialize":
  333. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  334. elif method == "session/prompt":
  335. params = msg.get("params") or {}
  336. 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)
  337. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "other", "status": "idle"}}), flush=True)
  338. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  339. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
  340. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  341. 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)
  342. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
  343. elif method == "shutdown":
  344. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  345. break
  346. """.strip()
  347. )
  348. with DeepSeekHarness(
  349. _launch_args=(sys.executable, str(script)),
  350. cwd=str(tmp_path),
  351. ) as harness:
  352. result = harness.run("stay in your lane", session_id="main")
  353. assert result.final_response == "right session"
  354. assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main"] * 4
  355. def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
  356. script = tmp_path / "fake_runtime.py"
  357. script.write_text(
  358. """
  359. import json
  360. import sys
  361. for line in sys.stdin:
  362. msg = json.loads(line)
  363. method = msg.get("method")
  364. if method == "initialize":
  365. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  366. elif method == "session/prompt":
  367. params = msg.get("params") or {}
  368. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": "message-1"}]}}}}), flush=True)
  369. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
  370. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  371. 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)
  372. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
  373. elif method == "shutdown":
  374. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  375. break
  376. """.strip()
  377. )
  378. with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  379. result = harness.run("one turn", session_id="main")
  380. assert harness.client._notifications.qsize() == 0
  381. def test_session_run_waits_for_late_idle_without_replaying_stale_notifications(tmp_path: Path) -> None:
  382. script = tmp_path / "fake_runtime.py"
  383. script.write_text(
  384. """
  385. import json
  386. import sys
  387. import time
  388. turn = 0
  389. for line in sys.stdin:
  390. msg = json.loads(line)
  391. method = msg.get("method")
  392. if method == "initialize":
  393. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  394. elif method == "session/prompt":
  395. turn += 1
  396. params = msg.get("params") or {}
  397. session_id = params["sessionId"]
  398. message_id = f"message-{turn}"
  399. print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": session_id, "event": {"type": "agent/inbox/spliced", "data": {"target": "next-turn", "start": 0, "inserted": [{"id": message_id}]}}}}), flush=True)
  400. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "running"}}), flush=True)
  401. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": message_id}}), flush=True)
  402. if turn == 1:
  403. 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)
  404. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "idle"}}), flush=True)
  405. else:
  406. time.sleep(0.05)
  407. 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)
  408. print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "idle"}}), flush=True)
  409. elif method == "shutdown":
  410. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  411. break
  412. """.strip()
  413. )
  414. with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  415. first = harness.run("first turn", session_id="main")
  416. second = harness.run("second turn", session_id="main")
  417. assert first.final_response == "first"
  418. assert second.final_response == "second"
  419. assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main"] * 4
  420. def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
  421. script = tmp_path / "fake_bridge.py"
  422. script.write_text(
  423. """
  424. import json
  425. import sys
  426. for line in sys.stdin:
  427. msg = json.loads(line)
  428. method = msg.get("method")
  429. if method == "initialize":
  430. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  431. elif method == "session/prompt":
  432. params = msg.get("params") or {}
  433. print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
  434. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  435. elif method == "shutdown":
  436. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  437. break
  438. """.strip()
  439. )
  440. with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
  441. init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  442. assert init.serverInfo.name == "fake-dsh"
  443. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  444. notification = client.next_notification()
  445. assert notification.method == "llm/request"
  446. assert notification.payload["requestId"] == "req-1"
  447. assert notification.payload["sessionId"] == "main"
  448. def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
  449. client = HarnessClient()
  450. with client.subscribe_session_notifications("main"):
  451. client._handle_message({
  452. "jsonrpc": "2.0",
  453. "method": "session.event",
  454. "params": {"sessionId": "other", "event": {"type": "assistant/message"}},
  455. })
  456. assert client._notifications.qsize() == 1
  457. notification = client._notifications.get_nowait()
  458. assert not isinstance(notification, BaseException)
  459. assert notification.method == "session.event"
  460. assert notification.payload["sessionId"] == "other"
  461. def test_session_subscription_keeps_descendant_relationships_across_subscriptions() -> None:
  462. client = HarnessClient()
  463. with client.subscribe_session_notifications("main") as first:
  464. client._handle_message({
  465. "jsonrpc": "2.0",
  466. "method": "subagent.started",
  467. "params": {"parentSessionId": "main", "childSessionId": "child"},
  468. })
  469. assert first.next().payload["childSessionId"] == "child"
  470. with client.subscribe_session_notifications("main") as second:
  471. client._handle_message({
  472. "jsonrpc": "2.0",
  473. "method": "subagent.started",
  474. "params": {"parentSessionId": "child", "childSessionId": "grandchild"},
  475. })
  476. client._handle_message({
  477. "jsonrpc": "2.0",
  478. "method": "session.event",
  479. "params": {"sessionId": "grandchild", "event": {"type": "assistant/message"}},
  480. })
  481. assert second.next().payload["childSessionId"] == "grandchild"
  482. assert second.next().payload["sessionId"] == "grandchild"
  483. assert client._notifications.qsize() == 0
  484. def test_session_subscription_preserves_reused_child_ancestry_after_late_finish() -> None:
  485. client = HarnessClient()
  486. old_seen: list[Notification] = []
  487. new_seen: list[Notification] = []
  488. with (
  489. client.subscribe_session_notifications("old-parent") as old_subscription,
  490. client.subscribe_session_notifications("new-parent") as new_subscription,
  491. ):
  492. client._handle_message({
  493. "jsonrpc": "2.0",
  494. "method": "subagent.started",
  495. "params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
  496. })
  497. old_subscription.drain(old_seen.append)
  498. new_subscription.drain(new_seen.append)
  499. assert [notification.method for notification in old_seen] == ["subagent.started"]
  500. assert new_seen == []
  501. client._handle_message({
  502. "jsonrpc": "2.0",
  503. "method": "subagent.started",
  504. "params": {"parentSessionId": "new-parent", "childSessionId": "reused-child"},
  505. })
  506. old_subscription.drain(old_seen.append)
  507. new_subscription.drain(new_seen.append)
  508. assert [notification.method for notification in new_seen] == ["subagent.started"]
  509. client._handle_message({
  510. "jsonrpc": "2.0",
  511. "method": "subagent.finished",
  512. "params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
  513. })
  514. old_subscription.drain(old_seen.append)
  515. new_subscription.drain(new_seen.append)
  516. assert [notification.method for notification in old_seen] == [
  517. "subagent.started",
  518. "subagent.finished",
  519. ]
  520. assert [notification.method for notification in new_seen] == ["subagent.started"]
  521. client._handle_message({
  522. "jsonrpc": "2.0",
  523. "method": "session.event",
  524. "params": {"sessionId": "reused-child", "event": {"type": "assistant/message"}},
  525. })
  526. old_subscription.drain(old_seen.append)
  527. new_subscription.drain(new_seen.append)
  528. assert [notification.method for notification in old_seen] == [
  529. "subagent.started",
  530. "subagent.finished",
  531. ]
  532. assert [notification.method for notification in new_seen] == [
  533. "subagent.started",
  534. "session.event",
  535. ]
  536. assert client._notifications.qsize() == 0
  537. def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
  538. script = tmp_path / "fake_bridge.py"
  539. script.write_text(
  540. """
  541. import json
  542. import sys
  543. for line in sys.stdin:
  544. msg = json.loads(line)
  545. method = msg.get("method")
  546. if method == "initialize":
  547. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  548. elif method in {"emit-first", "emit-second"}:
  549. print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
  550. elif method == "session/prompt":
  551. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
  552. elif method == "shutdown":
  553. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  554. break
  555. """.strip()
  556. )
  557. def broken_filter(_notification: object) -> bool:
  558. raise RuntimeError("bad notification filter")
  559. with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
  560. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  561. with (
  562. client.subscribe_notifications(broken_filter) as broken,
  563. client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
  564. ):
  565. client.notify("emit-first")
  566. with pytest.raises(RuntimeError, match="bad notification filter"):
  567. broken.next()
  568. assert healthy.next().payload == {"source": "emit-first"}
  569. assert client._notifications.qsize() == 0
  570. client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
  571. client.notify("emit-second")
  572. assert healthy.next().payload == {"source": "emit-second"}
  573. def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
  574. script = tmp_path / "fake_bridge.py"
  575. script.write_text(
  576. """
  577. import json
  578. import sys
  579. for line in sys.stdin:
  580. msg = json.loads(line)
  581. method = msg.get("method")
  582. if method == "initialize":
  583. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  584. elif method == "session/prompt":
  585. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
  586. elif method == "shutdown":
  587. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  588. break
  589. """.strip()
  590. )
  591. with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
  592. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  593. with pytest.raises(ValueError):
  594. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  595. def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
  596. script = tmp_path / "fake_bridge.py"
  597. script.write_text(
  598. """
  599. import json
  600. import sys
  601. for line in sys.stdin:
  602. msg = json.loads(line)
  603. method = msg.get("method")
  604. if method == "initialize":
  605. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  606. print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
  607. elif "id" in msg and "method" not in msg:
  608. print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
  609. elif method == "shutdown":
  610. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  611. break
  612. """.strip()
  613. )
  614. with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
  615. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  616. request = client.next_request()
  617. assert request.id == "bridge-req-1"
  618. assert request.method == "llm.request"
  619. assert request.payload["requestId"] == "req-1"
  620. client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
  621. notification = client.next_notification()
  622. assert notification.method == "response/seen"
  623. assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
  624. def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
  625. script = tmp_path / "fake_bridge.py"
  626. script.write_text(
  627. """
  628. import json
  629. import sys
  630. print("node warning: experimental loader", flush=True)
  631. for line in sys.stdin:
  632. msg = json.loads(line)
  633. if msg.get("method") == "initialize":
  634. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  635. elif msg.get("method") == "shutdown":
  636. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  637. break
  638. """.strip()
  639. )
  640. with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
  641. init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  642. assert init.serverInfo.name == "fake-dsh"
  643. def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
  644. script = tmp_path / "fake_bridge.py"
  645. script.write_text(
  646. """
  647. import sys
  648. import time
  649. print("bridge is still starting", file=sys.stderr, flush=True)
  650. time.sleep(60)
  651. """.strip()
  652. )
  653. with HarnessClient(
  654. HarnessConfig(
  655. profile="web",
  656. initialize_timeout_seconds=0.1,
  657. ),
  658. _launch_args=(sys.executable, str(script)),
  659. ) as client:
  660. start = time.monotonic()
  661. try:
  662. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  663. except TimeoutError as exc:
  664. assert time.monotonic() - start < 2
  665. assert "bridge is still starting" in str(exc)
  666. assert "profile 'web'" in str(exc)
  667. else:
  668. raise AssertionError("initialize should time out")
  669. def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
  670. script = tmp_path / "fake_bridge.py"
  671. script.write_text(
  672. """
  673. import json
  674. import signal
  675. import sys
  676. import time
  677. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  678. for line in sys.stdin:
  679. msg = json.loads(line)
  680. if msg.get("method") == "initialize":
  681. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  682. elif msg.get("method") == "shutdown":
  683. time.sleep(60)
  684. """.strip()
  685. )
  686. client = HarnessClient(
  687. HarnessConfig(
  688. shutdown_timeout_seconds=0.1,
  689. ),
  690. _launch_args=(sys.executable, str(script)),
  691. )
  692. client.start()
  693. proc = client._proc
  694. assert proc is not None
  695. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  696. start = time.monotonic()
  697. client.close()
  698. assert time.monotonic() - start < 2
  699. assert proc.poll() is not None
  700. assert client._proc is None
  701. def test_client_close_allows_eof_quiescence_after_shutdown_response(tmp_path: Path) -> None:
  702. script = tmp_path / "fake_runtime.py"
  703. marker = tmp_path / "quiesced.txt"
  704. script.write_text(
  705. """
  706. import json
  707. import os
  708. from pathlib import Path
  709. import sys
  710. import time
  711. for line in sys.stdin:
  712. msg = json.loads(line)
  713. if msg.get("method") == "initialize":
  714. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  715. elif msg.get("method") == "shutdown":
  716. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  717. time.sleep(0.05)
  718. Path(os.environ["QUIESCED_MARKER"]).write_text("quiesced")
  719. """.strip()
  720. )
  721. client = HarnessClient(
  722. HarnessConfig(
  723. env={"QUIESCED_MARKER": str(marker)},
  724. shutdown_timeout_seconds=1,
  725. ),
  726. _launch_args=(sys.executable, str(script)),
  727. )
  728. client.start()
  729. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  730. client.close()
  731. assert marker.read_text() == "quiesced"
  732. def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
  733. script = tmp_path / "rejecting_runtime.py"
  734. script.write_text(
  735. """
  736. import json
  737. import sys
  738. for line in sys.stdin:
  739. msg = json.loads(line)
  740. if msg.get("method") == "initialize":
  741. print("initialize diagnostic", file=sys.stderr, flush=True)
  742. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
  743. elif msg.get("method") == "shutdown":
  744. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  745. break
  746. """.strip()
  747. )
  748. client = HarnessClient(_launch_args=(sys.executable, str(script)))
  749. client.start()
  750. proc = client._proc
  751. assert proc is not None
  752. with pytest.raises(JsonRpcError, match="bad initialize") as excinfo:
  753. client.initialize(provider="deepseek-official", cwd=".", model="dsagent")
  754. assert excinfo.value.code == -32000
  755. assert "initialize diagnostic" in str(excinfo.value)
  756. assert proc.wait(timeout=1) is not None
  757. assert client._proc is None
  758. def test_public_signatures_omit_unsupported_wire_parameters() -> None:
  759. from deepseek_harness import DeepSeekHarnessConfig, Session
  760. assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
  761. assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
  762. assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
  763. assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
  764. assert "profile" not in inspect.signature(Session.run).parameters
  765. assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
  766. assert "max_tokens" in DeepSeekHarnessConfig.__dataclass_fields__
  767. assert "reasoning_effort" in DeepSeekHarnessConfig.__dataclass_fields__
  768. assert "max_tokens" in inspect.signature(HarnessClient.initialize).parameters
  769. assert "reasoning_effort" in inspect.signature(HarnessClient.initialize).parameters
  770. assert "client_name" not in HarnessConfig.__dataclass_fields__
  771. assert "client_version" not in HarnessConfig.__dataclass_fields__
  772. assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set(
  773. DeepSeekHarnessConfig.__dataclass_fields__
  774. )
  775. assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set(
  776. HarnessConfig.__dataclass_fields__
  777. )
  778. assert "initialize_timeout_seconds" in DeepSeekHarnessConfig.__dataclass_fields__
  779. assert "initialize_timeout_seconds" in HarnessConfig.__dataclass_fields__
  780. assert DeepSeekHarnessConfig().initialize_timeout_seconds == 30.0
  781. assert HarnessConfig().initialize_timeout_seconds == 30.0
  782. for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"):
  783. assert removed not in DeepSeekHarnessConfig.__dataclass_fields__
  784. assert removed not in HarnessConfig.__dataclass_fields__
  785. assert "_launch_args" not in HarnessConfig.__dataclass_fields__
  786. assert "session_root" not in RunResult.__dataclass_fields__
  787. def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
  788. HarnessClient().close()
  789. script = tmp_path / "fake_bridge.py"
  790. script.write_text(
  791. """
  792. import json
  793. import sys
  794. for line in sys.stdin:
  795. msg = json.loads(line)
  796. if msg.get("method") == "initialize":
  797. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  798. elif msg.get("method") == "shutdown":
  799. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  800. break
  801. """.strip()
  802. )
  803. client = HarnessClient(_launch_args=(sys.executable, str(script)))
  804. client.start()
  805. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  806. client.close()
  807. client.close()
  808. def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
  809. script = tmp_path / "crashing_runtime.py"
  810. script.write_text(
  811. """
  812. import sys
  813. print("fatal bridge exploded", file=sys.stderr, flush=True)
  814. sys.exit(42)
  815. """.strip()
  816. )
  817. with HarnessClient(
  818. HarnessConfig(
  819. request_timeout_seconds=2,
  820. ),
  821. _launch_args=(sys.executable, str(script)),
  822. ) as client:
  823. with pytest.raises(Exception, match="fatal bridge exploded"):
  824. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  825. def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
  826. script = tmp_path / "fake_bridge.py"
  827. output = tmp_path / "seen.jsonl"
  828. script.write_text(
  829. """
  830. import json
  831. import os
  832. import sys
  833. with open(os.environ["SEEN"], "w") as seen:
  834. for line in sys.stdin:
  835. seen.write(line)
  836. seen.flush()
  837. msg = json.loads(line)
  838. if "id" in msg and msg.get("method") == "initialize":
  839. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  840. elif "id" in msg and msg.get("method") == "shutdown":
  841. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  842. break
  843. """.strip()
  844. )
  845. with HarnessClient(
  846. HarnessConfig(
  847. env={"SEEN": str(output)},
  848. ),
  849. _launch_args=(sys.executable, str(script)),
  850. ) as client:
  851. client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
  852. threads = [
  853. threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
  854. for index in range(50)
  855. ]
  856. for thread in threads:
  857. thread.start()
  858. for thread in threads:
  859. thread.join()
  860. for line in output.read_text().splitlines():
  861. json.loads(line)
  862. def _install_fake_bundled_dsh(
  863. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  864. ) -> None:
  865. """Install a fake runtime package that records dsh argv and serves lifecycle calls."""
  866. runtime = tmp_path / "dsh.py"
  867. runtime.write_text(
  868. """
  869. import json
  870. import os
  871. import sys
  872. json.dump({
  873. "argv": sys.argv[1:],
  874. "DSH_HOME": os.environ.get("DSH_HOME"),
  875. "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
  876. }, open(os.environ["ENV_DUMP"], "w"))
  877. for line in sys.stdin:
  878. msg = json.loads(line)
  879. if msg.get("method") == "initialize":
  880. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
  881. elif msg.get("method") == "shutdown":
  882. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  883. break
  884. """.strip()
  885. )
  886. module_dir = tmp_path / "deepseek_harness_runtime"
  887. module_dir.mkdir()
  888. (module_dir / "__init__.py").write_text(
  889. f"""
  890. def resolve_bundled_launch_args(mode=None):
  891. return ({sys.executable!r}, {str(runtime)!r})
  892. """.strip()
  893. )
  894. monkeypatch.syspath_prepend(str(tmp_path))
  895. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  896. def test_client_default_launch_uses_bundled_dsh_sdk_profile_and_explicit_home(
  897. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  898. ) -> None:
  899. env_dump = tmp_path / "env.json"
  900. home = tmp_path / "home"
  901. patch = tmp_path / "sdk.patch.yml"
  902. patch.write_text("[]\n")
  903. _install_fake_bundled_dsh(tmp_path, monkeypatch)
  904. monkeypatch.chdir(tmp_path)
  905. monkeypatch.setenv("DSH_HOME", str(tmp_path / "ambient-home"))
  906. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  907. with HarnessClient(HarnessConfig(
  908. profile="sdk",
  909. patches=("sdk.patch.yml",),
  910. dsh_home=str(home),
  911. env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(tmp_path / "env-home")},
  912. )) as client:
  913. init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro")
  914. assert init.serverInfo.name == "bundled-runtime"
  915. assert json.loads(env_dump.read_text()) == {
  916. "argv": ["--profile", "sdk", "--patch", str(patch)],
  917. "DSH_HOME": str(home),
  918. "DSH_CORDIS_CONFIG": None,
  919. }
  920. def test_client_accepts_explicit_environment_dsh_home(
  921. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  922. ) -> None:
  923. env_dump = tmp_path / "env.json"
  924. home = tmp_path / "environment-home"
  925. _install_fake_bundled_dsh(tmp_path, monkeypatch)
  926. with HarnessClient(
  927. HarnessConfig(profile="custom", env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(home)})
  928. ) as client:
  929. client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro")
  930. assert json.loads(env_dump.read_text()) == {
  931. "argv": ["--profile", "custom"],
  932. "DSH_HOME": str(home),
  933. "DSH_CORDIS_CONFIG": None,
  934. }
  935. def test_client_rejects_an_implicit_default_dsh_home(
  936. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  937. ) -> None:
  938. _install_fake_bundled_dsh(tmp_path, monkeypatch)
  939. monkeypatch.delenv("DSH_HOME", raising=False)
  940. with pytest.raises(ValueError, match="explicit dsh_home or non-empty DSH_HOME"):
  941. HarnessClient(HarnessConfig(env={})).start()
  942. def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
  943. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  944. monkeypatch.setattr(sys, "path", [])
  945. with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
  946. HarnessClient(HarnessConfig(dsh_home="/explicit/home")).start()