test_client.py 36 KB

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