test_client.py 48 KB

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