1
0

test_client.py 44 KB

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