test_client.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. from __future__ import annotations
  2. import json
  3. import inspect
  4. import sys
  5. import threading
  6. import time
  7. from pathlib import Path
  8. import pytest
  9. from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
  10. def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
  11. script = tmp_path / "fake_runtime.py"
  12. env_dump = tmp_path / "env.json"
  13. script.write_text(
  14. """
  15. import json
  16. import os
  17. import sys
  18. env_dump = os.environ["ENV_DUMP"]
  19. json.dump({
  20. "DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
  21. "DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
  22. "DSH_CWD": os.environ.get("DSH_CWD"),
  23. "DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
  24. "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
  25. }, open(env_dump, "w"))
  26. for line in sys.stdin:
  27. msg = json.loads(line)
  28. method = msg.get("method")
  29. if method == "initialize":
  30. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  31. elif method == "session/prompt":
  32. params = msg.get("params") or {}
  33. print(json.dumps({
  34. "jsonrpc": "2.0",
  35. "method": "session.event",
  36. "params": {
  37. "sessionId": params["sessionId"],
  38. "event": {
  39. "type": "assistant/message",
  40. "data": {"content": [{"type": "text", "text": "hello from runtime"}]},
  41. },
  42. },
  43. }), flush=True)
  44. print(json.dumps({
  45. "jsonrpc": "2.0",
  46. "method": "session.finished",
  47. "params": {"sessionId": params["sessionId"], "status": "ok"},
  48. }), flush=True)
  49. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  50. elif method == "shutdown":
  51. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  52. break
  53. """.strip()
  54. )
  55. with DeepSeekHarness(
  56. model="deepseek-v4-flash",
  57. cwd=str(tmp_path),
  58. cordis=str(tmp_path / "cordis.yml"),
  59. session_root=str(tmp_path / "sessions"),
  60. launch_args_override=(sys.executable, str(script)),
  61. env={
  62. "ENV_DUMP": str(env_dump),
  63. "DEEPSEEK_API_KEY": "env-key",
  64. "DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
  65. },
  66. ) as harness:
  67. result = harness.run("say hello", session_id="main")
  68. assert result.status == "ok"
  69. assert result.final_response == "hello from runtime"
  70. assert result.events[0]["type"] == "assistant/message"
  71. dumped_env = json.loads(env_dump.read_text())
  72. assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
  73. assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
  74. assert dumped_env["DSH_CWD"] == str(tmp_path)
  75. assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions")
  76. assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml")
  77. def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
  78. script = tmp_path / "fake_runtime.py"
  79. script.write_text(
  80. """
  81. import json
  82. import sys
  83. for line in sys.stdin:
  84. msg = json.loads(line)
  85. method = msg.get("method")
  86. if method == "initialize":
  87. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  88. elif method == "session/prompt":
  89. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  90. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
  91. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  92. elif method == "shutdown":
  93. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  94. break
  95. """.strip()
  96. )
  97. seen: list[str] = []
  98. with DeepSeekHarness(
  99. launch_args_override=(sys.executable, str(script)),
  100. cwd=str(tmp_path),
  101. ) as harness:
  102. session = harness.start_session("main")
  103. result = session.run(
  104. "spawn a helper",
  105. on_notification=lambda notification: seen.append(notification.method),
  106. )
  107. assert result.status == "ok"
  108. assert seen == ["subagent.started", "session.finished"]
  109. def test_relative_cwd_is_absolute_in_process_environment_and_wire(
  110. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  111. ) -> None:
  112. script = tmp_path / "capture_cwd.py"
  113. capture = tmp_path / "cwd.json"
  114. script.write_text(
  115. """
  116. import json
  117. import os
  118. import sys
  119. for line in sys.stdin:
  120. msg = json.loads(line)
  121. if msg.get("method") == "initialize":
  122. json.dump({"process": os.getcwd(), "environment": os.environ.get("DSH_CWD"), "wire": msg["params"]["cwd"]}, open(os.environ["CAPTURE"], "w"))
  123. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  124. elif msg.get("method") == "shutdown":
  125. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  126. break
  127. """.strip()
  128. )
  129. monkeypatch.chdir(tmp_path)
  130. with DeepSeekHarness(
  131. cwd=".",
  132. runtime_cwd=".",
  133. launch_args_override=(sys.executable, str(script)),
  134. env={"CAPTURE": str(capture)},
  135. ):
  136. pass
  137. expected = str(tmp_path.resolve())
  138. assert json.loads(capture.read_text()) == {
  139. "process": expected,
  140. "environment": expected,
  141. "wire": expected,
  142. }
  143. def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
  144. script = tmp_path / "fake_runtime.py"
  145. script.write_text(
  146. """
  147. import json
  148. import sys
  149. for line in sys.stdin:
  150. msg = json.loads(line)
  151. method = msg.get("method")
  152. if method == "initialize":
  153. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  154. elif method == "session/prompt":
  155. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
  156. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
  157. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "main", "status": "ok"}}), flush=True)
  158. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  159. elif method == "shutdown":
  160. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  161. break
  162. """.strip()
  163. )
  164. with DeepSeekHarness(
  165. launch_args_override=(sys.executable, str(script)),
  166. cwd=str(tmp_path),
  167. ) as harness:
  168. result = harness.run("spawn a helper", session_id="main")
  169. assert result.status == "ok"
  170. assert [notification.method for notification in result.notifications] == [
  171. "subagent.started",
  172. "subagent.finished",
  173. "session.finished",
  174. ]
  175. def test_session_run_collects_nested_subagent_tree_without_polluting_root_events(
  176. tmp_path: Path,
  177. ) -> None:
  178. script = tmp_path / "fake_runtime.py"
  179. script.write_text(
  180. """
  181. import json
  182. import sys
  183. for line in sys.stdin:
  184. msg = json.loads(line)
  185. method = msg.get("method")
  186. if method == "initialize":
  187. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  188. elif method == "session/prompt":
  189. root = (msg.get("params") or {})["sessionId"]
  190. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": root, "childSessionId": "child"}}), flush=True)
  191. 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)
  192. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "child", "childSessionId": "grandchild"}}), flush=True)
  193. 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)
  194. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "child", "childSessionId": "grandchild", "status": "ok"}}), flush=True)
  195. print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": root, "childSessionId": "child", "status": "ok"}}), flush=True)
  196. 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)
  197. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": root, "status": "ok"}}), flush=True)
  198. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  199. elif method == "shutdown":
  200. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  201. break
  202. """.strip()
  203. )
  204. seen: list[str] = []
  205. with DeepSeekHarness(
  206. launch_args_override=(sys.executable, str(script)),
  207. cwd=str(tmp_path),
  208. ) as harness:
  209. result = harness.run(
  210. "delegate recursively",
  211. session_id="main",
  212. on_notification=lambda notification: seen.append(notification.method),
  213. )
  214. assert harness.client._notifications.qsize() == 0
  215. assert result.status == "ok"
  216. assert result.final_response == "root response"
  217. assert [event["data"]["content"][0]["text"] for event in result.events] == ["root response"]
  218. assert [notification.method for notification in result.notifications] == [
  219. "subagent.started",
  220. "session.event",
  221. "subagent.started",
  222. "session.event",
  223. "subagent.finished",
  224. "subagent.finished",
  225. "session.event",
  226. "session.finished",
  227. ]
  228. assert seen == [notification.method for notification in result.notifications]
  229. def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
  230. script = tmp_path / "fake_runtime.py"
  231. script.write_text(
  232. """
  233. import json
  234. import sys
  235. for line in sys.stdin:
  236. msg = json.loads(line)
  237. method = msg.get("method")
  238. if method == "initialize":
  239. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  240. elif method == "session/prompt":
  241. params = msg.get("params") or {}
  242. 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)
  243. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": "other", "status": "ok"}}), flush=True)
  244. 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)
  245. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
  246. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  247. elif method == "shutdown":
  248. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  249. break
  250. """.strip()
  251. )
  252. with DeepSeekHarness(
  253. launch_args_override=(sys.executable, str(script)),
  254. cwd=str(tmp_path),
  255. ) as harness:
  256. result = harness.run("stay in your lane", session_id="main")
  257. assert result.status == "ok"
  258. assert result.final_response == "right session"
  259. assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main", "main"]
  260. def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
  261. script = tmp_path / "fake_runtime.py"
  262. script.write_text(
  263. """
  264. import json
  265. import sys
  266. for line in sys.stdin:
  267. msg = json.loads(line)
  268. method = msg.get("method")
  269. if method == "initialize":
  270. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  271. elif method == "session/prompt":
  272. params = msg.get("params") or {}
  273. 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)
  274. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": params["sessionId"], "status": "ok"}}), flush=True)
  275. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  276. elif method == "shutdown":
  277. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  278. break
  279. """.strip()
  280. )
  281. with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  282. result = harness.run("one turn", session_id="main")
  283. assert result.status == "ok"
  284. assert harness.client._notifications.qsize() == 0
  285. def test_session_run_waits_for_late_finished_without_replaying_stale_notifications(tmp_path: Path) -> None:
  286. script = tmp_path / "fake_runtime.py"
  287. script.write_text(
  288. """
  289. import json
  290. import sys
  291. import time
  292. turn = 0
  293. for line in sys.stdin:
  294. msg = json.loads(line)
  295. method = msg.get("method")
  296. if method == "initialize":
  297. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
  298. elif method == "session/prompt":
  299. turn += 1
  300. params = msg.get("params") or {}
  301. session_id = params["sessionId"]
  302. if turn == 1:
  303. 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)
  304. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
  305. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  306. else:
  307. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  308. time.sleep(0.05)
  309. 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)
  310. print(json.dumps({"jsonrpc": "2.0", "method": "session.finished", "params": {"sessionId": session_id, "status": "ok"}}), flush=True)
  311. elif method == "shutdown":
  312. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  313. break
  314. """.strip()
  315. )
  316. with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
  317. first = harness.run("first turn", session_id="main")
  318. second = harness.run("second turn", session_id="main")
  319. assert first.final_response == "first"
  320. assert second.final_response == "second"
  321. assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main", "main"]
  322. def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
  323. script = tmp_path / "fake_bridge.py"
  324. script.write_text(
  325. """
  326. import json
  327. import sys
  328. for line in sys.stdin:
  329. msg = json.loads(line)
  330. method = msg.get("method")
  331. if method == "initialize":
  332. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  333. elif method == "session/prompt":
  334. params = msg.get("params") or {}
  335. print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
  336. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  337. elif method == "shutdown":
  338. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  339. break
  340. """.strip()
  341. )
  342. with HarnessClient(
  343. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  344. ) as client:
  345. init = client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  346. assert init.serverInfo.name == "fake-dsh"
  347. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  348. notification = client.next_notification()
  349. assert notification.method == "llm/request"
  350. assert notification.payload["requestId"] == "req-1"
  351. assert notification.payload["sessionId"] == "main"
  352. def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
  353. client = HarnessClient()
  354. with client.subscribe_session_notifications("main"):
  355. client._handle_message({
  356. "jsonrpc": "2.0",
  357. "method": "session.event",
  358. "params": {"sessionId": "other", "event": {"type": "assistant/message"}},
  359. })
  360. assert client._notifications.qsize() == 1
  361. notification = client._notifications.get_nowait()
  362. assert not isinstance(notification, BaseException)
  363. assert notification.method == "session.event"
  364. assert notification.payload["sessionId"] == "other"
  365. def test_session_subscription_keeps_descendant_relationships_across_subscriptions() -> None:
  366. client = HarnessClient()
  367. with client.subscribe_session_notifications("main") as first:
  368. client._handle_message({
  369. "jsonrpc": "2.0",
  370. "method": "subagent.started",
  371. "params": {"parentSessionId": "main", "childSessionId": "child"},
  372. })
  373. assert first.next().payload["childSessionId"] == "child"
  374. with client.subscribe_session_notifications("main") as second:
  375. client._handle_message({
  376. "jsonrpc": "2.0",
  377. "method": "subagent.started",
  378. "params": {"parentSessionId": "child", "childSessionId": "grandchild"},
  379. })
  380. client._handle_message({
  381. "jsonrpc": "2.0",
  382. "method": "session.event",
  383. "params": {"sessionId": "grandchild", "event": {"type": "assistant/message"}},
  384. })
  385. assert second.next().payload["childSessionId"] == "grandchild"
  386. assert second.next().payload["sessionId"] == "grandchild"
  387. assert client._notifications.qsize() == 0
  388. def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
  389. script = tmp_path / "fake_bridge.py"
  390. script.write_text(
  391. """
  392. import json
  393. import sys
  394. for line in sys.stdin:
  395. msg = json.loads(line)
  396. method = msg.get("method")
  397. if method == "initialize":
  398. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  399. elif method in {"emit-first", "emit-second"}:
  400. print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
  401. elif method == "session/prompt":
  402. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  403. elif method == "shutdown":
  404. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  405. break
  406. """.strip()
  407. )
  408. def broken_filter(_notification: object) -> bool:
  409. raise RuntimeError("bad notification filter")
  410. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  411. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  412. with (
  413. client.subscribe_notifications(broken_filter) as broken,
  414. client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
  415. ):
  416. client.notify("emit-first")
  417. with pytest.raises(RuntimeError, match="bad notification filter"):
  418. broken.next()
  419. assert healthy.next().payload == {"source": "emit-first"}
  420. assert client._notifications.qsize() == 0
  421. client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
  422. client.notify("emit-second")
  423. assert healthy.next().payload == {"source": "emit-second"}
  424. def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
  425. script = tmp_path / "fake_bridge.py"
  426. script.write_text(
  427. """
  428. import json
  429. import sys
  430. for line in sys.stdin:
  431. msg = json.loads(line)
  432. method = msg.get("method")
  433. if method == "initialize":
  434. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  435. elif method == "session/prompt":
  436. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
  437. elif method == "shutdown":
  438. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  439. break
  440. """.strip()
  441. )
  442. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  443. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  444. with pytest.raises(ValueError):
  445. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  446. def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
  447. script = tmp_path / "fake_bridge.py"
  448. script.write_text(
  449. """
  450. import json
  451. import sys
  452. for line in sys.stdin:
  453. msg = json.loads(line)
  454. method = msg.get("method")
  455. if method == "initialize":
  456. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  457. print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
  458. elif "id" in msg and "method" not in msg:
  459. print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
  460. elif method == "shutdown":
  461. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  462. break
  463. """.strip()
  464. )
  465. with HarnessClient(
  466. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  467. ) as client:
  468. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  469. request = client.next_request()
  470. assert request.id == "bridge-req-1"
  471. assert request.method == "llm.request"
  472. assert request.payload["requestId"] == "req-1"
  473. client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
  474. notification = client.next_notification()
  475. assert notification.method == "response/seen"
  476. assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
  477. def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
  478. script = tmp_path / "fake_bridge.py"
  479. script.write_text(
  480. """
  481. import json
  482. import sys
  483. print("node warning: experimental loader", flush=True)
  484. for line in sys.stdin:
  485. msg = json.loads(line)
  486. if msg.get("method") == "initialize":
  487. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  488. elif msg.get("method") == "shutdown":
  489. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  490. break
  491. """.strip()
  492. )
  493. with HarnessClient(
  494. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  495. ) as client:
  496. init = client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  497. assert init.serverInfo.name == "fake-dsh"
  498. def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
  499. script = tmp_path / "fake_bridge.py"
  500. script.write_text(
  501. """
  502. import time
  503. time.sleep(60)
  504. """.strip()
  505. )
  506. with HarnessClient(
  507. HarnessConfig(
  508. launch_args_override=(sys.executable, str(script)),
  509. request_timeout_seconds=0.1,
  510. )
  511. ) as client:
  512. start = time.monotonic()
  513. try:
  514. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  515. except TimeoutError:
  516. assert time.monotonic() - start < 2
  517. else:
  518. raise AssertionError("initialize should time out")
  519. def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
  520. script = tmp_path / "fake_bridge.py"
  521. script.write_text(
  522. """
  523. import json
  524. import signal
  525. import sys
  526. import time
  527. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  528. for line in sys.stdin:
  529. msg = json.loads(line)
  530. if msg.get("method") == "initialize":
  531. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  532. elif msg.get("method") == "shutdown":
  533. time.sleep(60)
  534. """.strip()
  535. )
  536. client = HarnessClient(
  537. HarnessConfig(
  538. launch_args_override=(sys.executable, str(script)),
  539. shutdown_timeout_seconds=0.1,
  540. )
  541. )
  542. client.start()
  543. proc = client._proc
  544. assert proc is not None
  545. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  546. start = time.monotonic()
  547. client.close()
  548. assert time.monotonic() - start < 2
  549. assert proc.poll() is not None
  550. assert client._proc is None
  551. def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
  552. script = tmp_path / "rejecting_runtime.py"
  553. script.write_text(
  554. """
  555. import json
  556. import sys
  557. for line in sys.stdin:
  558. msg = json.loads(line)
  559. if msg.get("method") == "initialize":
  560. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
  561. elif msg.get("method") == "shutdown":
  562. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  563. break
  564. """.strip()
  565. )
  566. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  567. client.start()
  568. proc = client._proc
  569. assert proc is not None
  570. with pytest.raises(Exception, match="bad initialize"):
  571. client.initialize(provider="deepseek", cwd=".", model="dsagent")
  572. assert proc.wait(timeout=1) is not None
  573. assert client._proc is None
  574. def test_public_signatures_omit_unsupported_wire_parameters() -> None:
  575. from deepseek_harness import DeepSeekHarnessConfig, Session
  576. assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
  577. assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
  578. assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
  579. assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
  580. assert "profile" not in inspect.signature(Session.run).parameters
  581. assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
  582. assert "client_name" not in HarnessConfig.__dataclass_fields__
  583. assert "client_version" not in HarnessConfig.__dataclass_fields__
  584. def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
  585. HarnessClient().close()
  586. script = tmp_path / "fake_bridge.py"
  587. script.write_text(
  588. """
  589. import json
  590. import sys
  591. for line in sys.stdin:
  592. msg = json.loads(line)
  593. if msg.get("method") == "initialize":
  594. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  595. elif msg.get("method") == "shutdown":
  596. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  597. break
  598. """.strip()
  599. )
  600. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  601. client.start()
  602. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  603. client.close()
  604. client.close()
  605. def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
  606. script = tmp_path / "crashing_runtime.py"
  607. script.write_text(
  608. """
  609. import sys
  610. print("fatal bridge exploded", file=sys.stderr, flush=True)
  611. sys.exit(42)
  612. """.strip()
  613. )
  614. with HarnessClient(
  615. HarnessConfig(
  616. launch_args_override=(sys.executable, str(script)),
  617. request_timeout_seconds=2,
  618. )
  619. ) as client:
  620. with pytest.raises(Exception, match="fatal bridge exploded"):
  621. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  622. def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
  623. script = tmp_path / "fake_bridge.py"
  624. output = tmp_path / "seen.jsonl"
  625. script.write_text(
  626. """
  627. import json
  628. import os
  629. import sys
  630. with open(os.environ["SEEN"], "w") as seen:
  631. for line in sys.stdin:
  632. seen.write(line)
  633. seen.flush()
  634. msg = json.loads(line)
  635. if "id" in msg and msg.get("method") == "initialize":
  636. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  637. elif "id" in msg and msg.get("method") == "shutdown":
  638. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  639. break
  640. """.strip()
  641. )
  642. with HarnessClient(
  643. HarnessConfig(
  644. launch_args_override=(sys.executable, str(script)),
  645. env={"SEEN": str(output)},
  646. )
  647. ) as client:
  648. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  649. threads = [
  650. threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
  651. for index in range(50)
  652. ]
  653. for thread in threads:
  654. thread.start()
  655. for thread in threads:
  656. thread.join()
  657. for line in output.read_text().splitlines():
  658. json.loads(line)
  659. def _install_fake_bundled_runtime(
  660. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  661. ) -> Path:
  662. """Install a fake runtime package that records config and serves lifecycle calls.
  663. Returns the fake bundled default config path.
  664. """
  665. runtime = tmp_path / "dsh-jsonrpc-agent"
  666. runtime.write_text(
  667. """#!/usr/bin/env python3
  668. import json
  669. import os
  670. import sys
  671. json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
  672. for line in sys.stdin:
  673. msg = json.loads(line)
  674. if msg.get("method") == "initialize":
  675. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
  676. elif msg.get("method") == "shutdown":
  677. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  678. break
  679. """.strip()
  680. )
  681. runtime.chmod(0o755)
  682. default_config = tmp_path / "default-cordis.yml"
  683. module_dir = tmp_path / "deepseek_harness_runtime"
  684. module_dir.mkdir()
  685. (module_dir / "__init__.py").write_text(
  686. f"""
  687. def resolve_bundled_launch_args(mode=None):
  688. return ({str(runtime)!r},)
  689. def bundled_default_config_path():
  690. return {str(default_config)!r}
  691. """.strip()
  692. )
  693. monkeypatch.syspath_prepend(str(tmp_path))
  694. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  695. return default_config
  696. @pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
  697. def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
  698. tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
  699. ) -> None:
  700. env_dump = tmp_path / "env.json"
  701. default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
  702. if ambient_config is None:
  703. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  704. else:
  705. monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
  706. with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
  707. init = client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  708. assert init.serverInfo.name == "bundled-runtime"
  709. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
  710. def test_client_respects_explicit_config_over_bundled_default(
  711. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  712. ) -> None:
  713. env_dump = tmp_path / "env.json"
  714. _install_fake_bundled_runtime(tmp_path, monkeypatch)
  715. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  716. with HarnessClient(
  717. HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
  718. ) as client:
  719. client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  720. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
  721. def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
  722. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  723. monkeypatch.setattr(sys, "path", [])
  724. with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
  725. HarnessClient().start()