test_client.py 36 KB

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