test_client.py 44 KB

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