test_client.py 39 KB

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