test_client.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  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. 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_session_subscription_preserves_reused_child_ancestry_after_late_finish() -> None:
  389. client = HarnessClient()
  390. old_seen: list[Notification] = []
  391. new_seen: list[Notification] = []
  392. with (
  393. client.subscribe_session_notifications("old-parent") as old_subscription,
  394. client.subscribe_session_notifications("new-parent") as new_subscription,
  395. ):
  396. client._handle_message({
  397. "jsonrpc": "2.0",
  398. "method": "subagent.started",
  399. "params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
  400. })
  401. old_subscription.drain(old_seen.append)
  402. new_subscription.drain(new_seen.append)
  403. assert [notification.method for notification in old_seen] == ["subagent.started"]
  404. assert new_seen == []
  405. client._handle_message({
  406. "jsonrpc": "2.0",
  407. "method": "subagent.started",
  408. "params": {"parentSessionId": "new-parent", "childSessionId": "reused-child"},
  409. })
  410. old_subscription.drain(old_seen.append)
  411. new_subscription.drain(new_seen.append)
  412. assert [notification.method for notification in new_seen] == ["subagent.started"]
  413. client._handle_message({
  414. "jsonrpc": "2.0",
  415. "method": "subagent.finished",
  416. "params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
  417. })
  418. old_subscription.drain(old_seen.append)
  419. new_subscription.drain(new_seen.append)
  420. assert [notification.method for notification in old_seen] == [
  421. "subagent.started",
  422. "subagent.finished",
  423. ]
  424. assert [notification.method for notification in new_seen] == ["subagent.started"]
  425. client._handle_message({
  426. "jsonrpc": "2.0",
  427. "method": "session.event",
  428. "params": {"sessionId": "reused-child", "event": {"type": "assistant/message"}},
  429. })
  430. old_subscription.drain(old_seen.append)
  431. new_subscription.drain(new_seen.append)
  432. assert [notification.method for notification in old_seen] == [
  433. "subagent.started",
  434. "subagent.finished",
  435. ]
  436. assert [notification.method for notification in new_seen] == [
  437. "subagent.started",
  438. "session.event",
  439. ]
  440. assert client._notifications.qsize() == 0
  441. def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
  442. script = tmp_path / "fake_bridge.py"
  443. script.write_text(
  444. """
  445. import json
  446. import sys
  447. for line in sys.stdin:
  448. msg = json.loads(line)
  449. method = msg.get("method")
  450. if method == "initialize":
  451. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  452. elif method in {"emit-first", "emit-second"}:
  453. print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
  454. elif method == "session/prompt":
  455. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": True}}), flush=True)
  456. elif method == "shutdown":
  457. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  458. break
  459. """.strip()
  460. )
  461. def broken_filter(_notification: object) -> bool:
  462. raise RuntimeError("bad notification filter")
  463. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  464. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  465. with (
  466. client.subscribe_notifications(broken_filter) as broken,
  467. client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
  468. ):
  469. client.notify("emit-first")
  470. with pytest.raises(RuntimeError, match="bad notification filter"):
  471. broken.next()
  472. assert healthy.next().payload == {"source": "emit-first"}
  473. assert client._notifications.qsize() == 0
  474. client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
  475. client.notify("emit-second")
  476. assert healthy.next().payload == {"source": "emit-second"}
  477. def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
  478. script = tmp_path / "fake_bridge.py"
  479. script.write_text(
  480. """
  481. import json
  482. import sys
  483. for line in sys.stdin:
  484. msg = json.loads(line)
  485. method = msg.get("method")
  486. if method == "initialize":
  487. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  488. elif method == "session/prompt":
  489. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
  490. elif method == "shutdown":
  491. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  492. break
  493. """.strip()
  494. )
  495. with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client:
  496. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  497. with pytest.raises(ValueError):
  498. client.session_prompt("main", [{"type": "text", "text": "fix it"}])
  499. def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
  500. script = tmp_path / "fake_bridge.py"
  501. script.write_text(
  502. """
  503. import json
  504. import sys
  505. for line in sys.stdin:
  506. msg = json.loads(line)
  507. method = msg.get("method")
  508. if method == "initialize":
  509. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  510. print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
  511. elif "id" in msg and "method" not in msg:
  512. print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
  513. elif method == "shutdown":
  514. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  515. break
  516. """.strip()
  517. )
  518. with HarnessClient(
  519. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  520. ) as client:
  521. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  522. request = client.next_request()
  523. assert request.id == "bridge-req-1"
  524. assert request.method == "llm.request"
  525. assert request.payload["requestId"] == "req-1"
  526. client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
  527. notification = client.next_notification()
  528. assert notification.method == "response/seen"
  529. assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
  530. def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
  531. script = tmp_path / "fake_bridge.py"
  532. script.write_text(
  533. """
  534. import json
  535. import sys
  536. print("node warning: experimental loader", flush=True)
  537. for line in sys.stdin:
  538. msg = json.loads(line)
  539. if msg.get("method") == "initialize":
  540. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  541. elif msg.get("method") == "shutdown":
  542. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  543. break
  544. """.strip()
  545. )
  546. with HarnessClient(
  547. HarnessConfig(launch_args_override=(sys.executable, str(script)))
  548. ) as client:
  549. init = client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  550. assert init.serverInfo.name == "fake-dsh"
  551. def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
  552. script = tmp_path / "fake_bridge.py"
  553. script.write_text(
  554. """
  555. import time
  556. time.sleep(60)
  557. """.strip()
  558. )
  559. with HarnessClient(
  560. HarnessConfig(
  561. launch_args_override=(sys.executable, str(script)),
  562. request_timeout_seconds=0.1,
  563. )
  564. ) as client:
  565. start = time.monotonic()
  566. try:
  567. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  568. except TimeoutError:
  569. assert time.monotonic() - start < 2
  570. else:
  571. raise AssertionError("initialize should time out")
  572. def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
  573. script = tmp_path / "fake_bridge.py"
  574. script.write_text(
  575. """
  576. import json
  577. import signal
  578. import sys
  579. import time
  580. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  581. for line in sys.stdin:
  582. msg = json.loads(line)
  583. if msg.get("method") == "initialize":
  584. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  585. elif msg.get("method") == "shutdown":
  586. time.sleep(60)
  587. """.strip()
  588. )
  589. client = HarnessClient(
  590. HarnessConfig(
  591. launch_args_override=(sys.executable, str(script)),
  592. shutdown_timeout_seconds=0.1,
  593. )
  594. )
  595. client.start()
  596. proc = client._proc
  597. assert proc is not None
  598. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  599. start = time.monotonic()
  600. client.close()
  601. assert time.monotonic() - start < 2
  602. assert proc.poll() is not None
  603. assert client._proc is None
  604. def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
  605. script = tmp_path / "rejecting_runtime.py"
  606. script.write_text(
  607. """
  608. import json
  609. import sys
  610. for line in sys.stdin:
  611. msg = json.loads(line)
  612. if msg.get("method") == "initialize":
  613. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
  614. elif msg.get("method") == "shutdown":
  615. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  616. break
  617. """.strip()
  618. )
  619. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  620. client.start()
  621. proc = client._proc
  622. assert proc is not None
  623. with pytest.raises(Exception, match="bad initialize"):
  624. client.initialize(provider="deepseek", cwd=".", model="dsagent")
  625. assert proc.wait(timeout=1) is not None
  626. assert client._proc is None
  627. def test_public_signatures_omit_unsupported_wire_parameters() -> None:
  628. from deepseek_harness import DeepSeekHarnessConfig, Session
  629. assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
  630. assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
  631. assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
  632. assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
  633. assert "profile" not in inspect.signature(Session.run).parameters
  634. assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
  635. assert "client_name" not in HarnessConfig.__dataclass_fields__
  636. assert "client_version" not in HarnessConfig.__dataclass_fields__
  637. def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
  638. HarnessClient().close()
  639. script = tmp_path / "fake_bridge.py"
  640. script.write_text(
  641. """
  642. import json
  643. import sys
  644. for line in sys.stdin:
  645. msg = json.loads(line)
  646. if msg.get("method") == "initialize":
  647. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  648. elif msg.get("method") == "shutdown":
  649. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  650. break
  651. """.strip()
  652. )
  653. client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script))))
  654. client.start()
  655. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  656. client.close()
  657. client.close()
  658. def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
  659. script = tmp_path / "crashing_runtime.py"
  660. script.write_text(
  661. """
  662. import sys
  663. print("fatal bridge exploded", file=sys.stderr, flush=True)
  664. sys.exit(42)
  665. """.strip()
  666. )
  667. with HarnessClient(
  668. HarnessConfig(
  669. launch_args_override=(sys.executable, str(script)),
  670. request_timeout_seconds=2,
  671. )
  672. ) as client:
  673. with pytest.raises(Exception, match="fatal bridge exploded"):
  674. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  675. def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
  676. script = tmp_path / "fake_bridge.py"
  677. output = tmp_path / "seen.jsonl"
  678. script.write_text(
  679. """
  680. import json
  681. import os
  682. import sys
  683. with open(os.environ["SEEN"], "w") as seen:
  684. for line in sys.stdin:
  685. seen.write(line)
  686. seen.flush()
  687. msg = json.loads(line)
  688. if "id" in msg and msg.get("method") == "initialize":
  689. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
  690. elif "id" in msg and msg.get("method") == "shutdown":
  691. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  692. break
  693. """.strip()
  694. )
  695. with HarnessClient(
  696. HarnessConfig(
  697. launch_args_override=(sys.executable, str(script)),
  698. env={"SEEN": str(output)},
  699. )
  700. ) as client:
  701. client.initialize(provider="deepseek", cwd="/workspace", model="dsagent")
  702. threads = [
  703. threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
  704. for index in range(50)
  705. ]
  706. for thread in threads:
  707. thread.start()
  708. for thread in threads:
  709. thread.join()
  710. for line in output.read_text().splitlines():
  711. json.loads(line)
  712. def _install_fake_bundled_runtime(
  713. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  714. ) -> Path:
  715. """Install a fake runtime package that records config and serves lifecycle calls.
  716. Returns the fake bundled default config path.
  717. """
  718. runtime = tmp_path / "dsh-jsonrpc-agent"
  719. runtime.write_text(
  720. """#!/usr/bin/env python3
  721. import json
  722. import os
  723. import sys
  724. json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
  725. for line in sys.stdin:
  726. msg = json.loads(line)
  727. if msg.get("method") == "initialize":
  728. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
  729. elif msg.get("method") == "shutdown":
  730. print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
  731. break
  732. """.strip()
  733. )
  734. runtime.chmod(0o755)
  735. default_config = tmp_path / "default-cordis.yml"
  736. module_dir = tmp_path / "deepseek_harness_runtime"
  737. module_dir.mkdir()
  738. (module_dir / "__init__.py").write_text(
  739. f"""
  740. def resolve_bundled_launch_args(mode=None):
  741. return ({str(runtime)!r},)
  742. def bundled_default_config_path():
  743. return {str(default_config)!r}
  744. """.strip()
  745. )
  746. monkeypatch.syspath_prepend(str(tmp_path))
  747. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  748. return default_config
  749. @pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
  750. def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
  751. tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
  752. ) -> None:
  753. env_dump = tmp_path / "env.json"
  754. default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
  755. if ambient_config is None:
  756. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  757. else:
  758. monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
  759. with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
  760. init = client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  761. assert init.serverInfo.name == "bundled-runtime"
  762. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
  763. def test_client_respects_explicit_config_over_bundled_default(
  764. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  765. ) -> None:
  766. env_dump = tmp_path / "env.json"
  767. _install_fake_bundled_runtime(tmp_path, monkeypatch)
  768. monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
  769. with HarnessClient(
  770. HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
  771. ) as client:
  772. client.initialize(provider="deepseek", cwd="/workspace", model="deepseek-v4-pro")
  773. assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
  774. def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
  775. monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
  776. monkeypatch.setattr(sys, "path", [])
  777. with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
  778. HarnessClient().start()