2026-07-11 14:08:24 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
2026-07-13 16:34:03 +08:00
|
|
|
import inspect
|
2026-07-11 14:08:24 +08:00
|
|
|
import sys
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, RunResult, SdkProtocolError
|
|
|
|
|
from deepseek_harness.errors import JsonRpcError
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
env_dump = tmp_path / "env.json"
|
2026-07-28 17:36:44 +08:00
|
|
|
init_dump = tmp_path / "init.json"
|
2026-07-11 14:08:24 +08:00
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
env_dump = os.environ["ENV_DUMP"]
|
|
|
|
|
json.dump({
|
|
|
|
|
"DEEPSEEK_API_KEY": os.environ.get("DEEPSEEK_API_KEY"),
|
|
|
|
|
"DEEPSEEK_BASE_URL": os.environ.get("DEEPSEEK_BASE_URL"),
|
|
|
|
|
"DSH_CWD": os.environ.get("DSH_CWD"),
|
|
|
|
|
"DSH_SESSION_ROOT": os.environ.get("DSH_SESSION_ROOT"),
|
|
|
|
|
"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
|
|
|
|
|
}, open(env_dump, "w"))
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
2026-07-28 17:36:44 +08:00
|
|
|
json.dump(msg.get("params"), open(os.environ["INIT_DUMP"], "w"))
|
2026-07-11 14:08:24 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
params = msg.get("params") or {}
|
2026-07-30 17:45:58 +08:00
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
print(json.dumps({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {
|
|
|
|
|
"sessionId": params["sessionId"],
|
|
|
|
|
"event": {
|
|
|
|
|
"type": "assistant/message",
|
2026-07-29 16:11:30 +08:00
|
|
|
"data": {
|
|
|
|
|
"message": {
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": [{"type": "text", "text": "hello from runtime"}],
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-11 14:08:24 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}), flush=True)
|
2026-08-11 16:25:59 +08:00
|
|
|
print(json.dumps({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {
|
|
|
|
|
"sessionId": params["sessionId"],
|
|
|
|
|
"event": {
|
|
|
|
|
"type": "turn/end",
|
|
|
|
|
"data": {"turn": 1, "reason": {"kind": "completed"}},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}), flush=True)
|
2026-08-11 16:48:41 +08:00
|
|
|
print(json.dumps({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {
|
|
|
|
|
"sessionId": params["sessionId"],
|
|
|
|
|
"event": {
|
|
|
|
|
"type": "turn/end",
|
|
|
|
|
"data": {"turn": 2, "reason": {"kind": "max-tokens"}},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
print(json.dumps({
|
|
|
|
|
"jsonrpc": "2.0",
|
2026-07-30 17:45:58 +08:00
|
|
|
"method": "session.status",
|
|
|
|
|
"params": {"sessionId": params["sessionId"], "status": "idle"},
|
2026-07-11 14:08:24 +08:00
|
|
|
}), flush=True)
|
|
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with DeepSeekHarness(
|
|
|
|
|
model="deepseek-v4-flash",
|
2026-08-21 03:43:54 +08:00
|
|
|
reasoning_effort="max",
|
2026-07-28 17:36:44 +08:00
|
|
|
max_tokens=4096,
|
2026-07-11 14:08:24 +08:00
|
|
|
cwd=str(tmp_path),
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
env={
|
|
|
|
|
"ENV_DUMP": str(env_dump),
|
2026-07-28 17:36:44 +08:00
|
|
|
"INIT_DUMP": str(init_dump),
|
2026-07-11 14:08:24 +08:00
|
|
|
"DEEPSEEK_API_KEY": "env-key",
|
|
|
|
|
"DEEPSEEK_BASE_URL": "http://127.0.0.1:4321",
|
|
|
|
|
},
|
|
|
|
|
) as harness:
|
|
|
|
|
result = harness.run("say hello", session_id="main")
|
|
|
|
|
|
|
|
|
|
assert result.final_response == "hello from runtime"
|
2026-08-11 16:48:41 +08:00
|
|
|
assert result.finish_reason == "max-tokens"
|
2026-08-11 16:25:59 +08:00
|
|
|
assert result.events[-1]["type"] == "turn/end"
|
2026-07-11 14:08:24 +08:00
|
|
|
dumped_env = json.loads(env_dump.read_text())
|
|
|
|
|
assert dumped_env["DEEPSEEK_API_KEY"] == "env-key"
|
|
|
|
|
assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321"
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert dumped_env["DSH_CWD"] is None
|
|
|
|
|
assert dumped_env["DSH_SESSION_ROOT"] is None
|
|
|
|
|
assert dumped_env["DSH_CORDIS_CONFIG"] is None
|
2026-07-28 17:36:44 +08:00
|
|
|
assert json.loads(init_dump.read_text()) == {
|
|
|
|
|
"cwd": str(tmp_path),
|
2026-07-29 16:36:07 +08:00
|
|
|
"provider": "deepseek-official",
|
2026-07-28 17:36:44 +08:00
|
|
|
"model": "deepseek-v4-flash",
|
2026-08-21 03:43:54 +08:00
|
|
|
"reasoningEffort": "max",
|
2026-07-28 17:36:44 +08:00
|
|
|
"maxTokens": 4096,
|
|
|
|
|
}
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_run_invokes_notification_callback_before_returning(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
2026-07-30 17:45:58 +08:00
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
seen: list[str] = []
|
|
|
|
|
with DeepSeekHarness(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
cwd=str(tmp_path),
|
|
|
|
|
) as harness:
|
|
|
|
|
session = harness.start_session("main")
|
|
|
|
|
result = session.run(
|
|
|
|
|
"spawn a helper",
|
|
|
|
|
on_notification=lambda notification: seen.append(notification.method),
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-30 17:45:58 +08:00
|
|
|
assert seen == ["session.event", "session.status", "subagent.started", "session.status"]
|
2026-08-11 16:25:59 +08:00
|
|
|
assert result.finish_reason is None
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
2026-08-11 16:48:41 +08:00
|
|
|
def test_high_level_sdk_rejects_turn_end_without_reason_kind(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
params = msg.get("params") or {}
|
|
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.event", "params": {"sessionId": params["sessionId"], "event": {"type": "turn/end", "data": {"turn": 1, "reason": {}}}}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
|
|
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with DeepSeekHarness(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-08-11 16:48:41 +08:00
|
|
|
cwd=str(tmp_path),
|
|
|
|
|
) as harness:
|
|
|
|
|
with pytest.raises(
|
|
|
|
|
SdkProtocolError,
|
|
|
|
|
match=r"turn/end event requires a string data\.reason\.kind",
|
|
|
|
|
):
|
|
|
|
|
harness.run("reject malformed turn ending", session_id="main")
|
|
|
|
|
|
|
|
|
|
|
2026-07-13 16:34:03 +08:00
|
|
|
def test_relative_cwd_is_absolute_in_process_environment_and_wire(
|
|
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
script = tmp_path / "capture_cwd.py"
|
|
|
|
|
capture = tmp_path / "cwd.json"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
json.dump({"process": os.getcwd(), "environment": os.environ.get("DSH_CWD"), "wire": msg["params"]["cwd"]}, open(os.environ["CAPTURE"], "w"))
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
|
|
|
|
|
|
with DeepSeekHarness(
|
|
|
|
|
cwd=".",
|
|
|
|
|
runtime_cwd=".",
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-13 16:34:03 +08:00
|
|
|
env={"CAPTURE": str(capture)},
|
|
|
|
|
):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
expected = str(tmp_path.resolve())
|
|
|
|
|
assert json.loads(capture.read_text()) == {
|
|
|
|
|
"process": expected,
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
"environment": None,
|
2026-07-13 16:34:03 +08:00
|
|
|
"wire": expected,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 14:08:24 +08:00
|
|
|
def test_session_run_includes_subagent_finished_for_parent_session(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
2026-07-30 17:45:58 +08:00
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "main", "childSessionId": "child"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "main", "childSessionId": "child", "status": "ok", "stopReason": "completed"}}), flush=True)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "main", "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with DeepSeekHarness(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
cwd=str(tmp_path),
|
|
|
|
|
) as harness:
|
|
|
|
|
result = harness.run("spawn a helper", session_id="main")
|
|
|
|
|
|
|
|
|
|
assert [notification.method for notification in result.notifications] == [
|
2026-07-30 17:45:58 +08:00
|
|
|
"session.event",
|
|
|
|
|
"session.status",
|
2026-07-11 14:08:24 +08:00
|
|
|
"subagent.started",
|
|
|
|
|
"subagent.finished",
|
2026-07-30 17:45:58 +08:00
|
|
|
"session.status",
|
2026-07-11 14:08:24 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 11:23:16 +08:00
|
|
|
def test_session_run_collects_nested_subagent_tree_without_polluting_root_events(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
root = (msg.get("params") or {})["sessionId"]
|
2026-07-30 17:45:58 +08:00
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": root, "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-24 11:23:16 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": root, "childSessionId": "child"}}), flush=True)
|
|
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.started", "params": {"parentSessionId": "child", "childSessionId": "grandchild"}}), flush=True)
|
|
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": "child", "childSessionId": "grandchild", "status": "ok"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "subagent.finished", "params": {"parentSessionId": root, "childSessionId": "child", "status": "ok"}}), flush=True)
|
|
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": root, "status": "idle"}}), flush=True)
|
2026-07-24 11:23:16 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
seen: list[str] = []
|
|
|
|
|
with DeepSeekHarness(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-24 11:23:16 +08:00
|
|
|
cwd=str(tmp_path),
|
|
|
|
|
) as harness:
|
|
|
|
|
result = harness.run(
|
|
|
|
|
"delegate recursively",
|
|
|
|
|
session_id="main",
|
|
|
|
|
on_notification=lambda notification: seen.append(notification.method),
|
|
|
|
|
)
|
|
|
|
|
assert harness.client._notifications.qsize() == 0
|
|
|
|
|
|
|
|
|
|
assert result.final_response == "root response"
|
2026-07-30 17:45:58 +08:00
|
|
|
assert [event["data"]["content"][0]["text"] for event in result.events if event["type"] == "assistant/message"] == ["root response"]
|
2026-07-24 11:23:16 +08:00
|
|
|
assert [notification.method for notification in result.notifications] == [
|
2026-07-30 17:45:58 +08:00
|
|
|
"session.event",
|
|
|
|
|
"session.status",
|
2026-07-24 11:23:16 +08:00
|
|
|
"subagent.started",
|
|
|
|
|
"session.event",
|
|
|
|
|
"subagent.started",
|
|
|
|
|
"session.event",
|
|
|
|
|
"subagent.finished",
|
|
|
|
|
"subagent.finished",
|
|
|
|
|
"session.event",
|
2026-07-30 17:45:58 +08:00
|
|
|
"session.status",
|
2026-07-24 11:23:16 +08:00
|
|
|
]
|
|
|
|
|
assert seen == [notification.method for notification in result.notifications]
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 14:08:24 +08:00
|
|
|
def test_session_run_ignores_notifications_for_other_sessions(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
params = msg.get("params") or {}
|
|
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": "other", "status": "idle"}}), flush=True)
|
|
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with DeepSeekHarness(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
cwd=str(tmp_path),
|
|
|
|
|
) as harness:
|
|
|
|
|
result = harness.run("stay in your lane", session_id="main")
|
|
|
|
|
|
|
|
|
|
assert result.final_response == "right session"
|
2026-07-30 17:45:58 +08:00
|
|
|
assert [notification.payload.get("sessionId") for notification in result.notifications] == ["main"] * 4
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_high_level_session_run_does_not_accumulate_global_notifications(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
params = msg.get("params") or {}
|
2026-07-30 17:45:58 +08:00
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": params["sessionId"], "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
|
2026-07-11 14:08:24 +08:00
|
|
|
result = harness.run("one turn", session_id="main")
|
|
|
|
|
assert harness.client._notifications.qsize() == 0
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 17:45:58 +08:00
|
|
|
def test_session_run_waits_for_late_idle_without_replaying_stale_notifications(tmp_path: Path) -> None:
|
2026-07-11 14:08:24 +08:00
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
turn = 0
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-runtime"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
turn += 1
|
|
|
|
|
params = msg.get("params") or {}
|
|
|
|
|
session_id = params["sessionId"]
|
2026-07-30 17:45:58 +08:00
|
|
|
message_id = f"message-{turn}"
|
|
|
|
|
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)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "running"}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": message_id}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
if turn == 1:
|
|
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
else:
|
|
|
|
|
time.sleep(0.05)
|
|
|
|
|
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)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "session.status", "params": {"sessionId": session_id, "status": "idle"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness:
|
2026-07-11 14:08:24 +08:00
|
|
|
first = harness.run("first turn", session_id="main")
|
|
|
|
|
second = harness.run("second turn", session_id="main")
|
|
|
|
|
|
|
|
|
|
assert first.final_response == "first"
|
|
|
|
|
assert second.final_response == "second"
|
2026-07-30 17:45:58 +08:00
|
|
|
assert [notification.payload.get("sessionId") for notification in second.notifications] == ["main"] * 4
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_starts_subprocess_sends_requests_and_routes_notifications(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
params = msg.get("params") or {}
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "llm/request", "params": {"requestId": "req-1", "sessionId": params["sessionId"], "model": "dsagent", "messages": []}}), flush=True)
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
assert init.serverInfo.name == "fake-dsh"
|
|
|
|
|
|
2026-07-13 16:34:03 +08:00
|
|
|
client.session_prompt("main", [{"type": "text", "text": "fix it"}])
|
2026-07-11 14:08:24 +08:00
|
|
|
notification = client.next_notification()
|
|
|
|
|
assert notification.method == "llm/request"
|
|
|
|
|
assert notification.payload["requestId"] == "req-1"
|
|
|
|
|
assert notification.payload["sessionId"] == "main"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_keeps_unmatched_notifications_available_globally_while_subscribed() -> None:
|
|
|
|
|
client = HarnessClient()
|
|
|
|
|
with client.subscribe_session_notifications("main"):
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {"sessionId": "other", "event": {"type": "assistant/message"}},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert client._notifications.qsize() == 1
|
|
|
|
|
notification = client._notifications.get_nowait()
|
|
|
|
|
assert not isinstance(notification, BaseException)
|
|
|
|
|
assert notification.method == "session.event"
|
|
|
|
|
assert notification.payload["sessionId"] == "other"
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 11:23:16 +08:00
|
|
|
def test_session_subscription_keeps_descendant_relationships_across_subscriptions() -> None:
|
|
|
|
|
client = HarnessClient()
|
|
|
|
|
with client.subscribe_session_notifications("main") as first:
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "subagent.started",
|
|
|
|
|
"params": {"parentSessionId": "main", "childSessionId": "child"},
|
|
|
|
|
})
|
|
|
|
|
assert first.next().payload["childSessionId"] == "child"
|
|
|
|
|
|
|
|
|
|
with client.subscribe_session_notifications("main") as second:
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "subagent.started",
|
|
|
|
|
"params": {"parentSessionId": "child", "childSessionId": "grandchild"},
|
|
|
|
|
})
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {"sessionId": "grandchild", "event": {"type": "assistant/message"}},
|
|
|
|
|
})
|
|
|
|
|
assert second.next().payload["childSessionId"] == "grandchild"
|
|
|
|
|
assert second.next().payload["sessionId"] == "grandchild"
|
|
|
|
|
|
|
|
|
|
assert client._notifications.qsize() == 0
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 12:21:10 +08:00
|
|
|
def test_session_subscription_preserves_reused_child_ancestry_after_late_finish() -> None:
|
|
|
|
|
client = HarnessClient()
|
|
|
|
|
old_seen: list[Notification] = []
|
|
|
|
|
new_seen: list[Notification] = []
|
|
|
|
|
with (
|
|
|
|
|
client.subscribe_session_notifications("old-parent") as old_subscription,
|
|
|
|
|
client.subscribe_session_notifications("new-parent") as new_subscription,
|
|
|
|
|
):
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "subagent.started",
|
|
|
|
|
"params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
|
|
|
|
|
})
|
|
|
|
|
old_subscription.drain(old_seen.append)
|
|
|
|
|
new_subscription.drain(new_seen.append)
|
|
|
|
|
assert [notification.method for notification in old_seen] == ["subagent.started"]
|
|
|
|
|
assert new_seen == []
|
|
|
|
|
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "subagent.started",
|
|
|
|
|
"params": {"parentSessionId": "new-parent", "childSessionId": "reused-child"},
|
|
|
|
|
})
|
|
|
|
|
old_subscription.drain(old_seen.append)
|
|
|
|
|
new_subscription.drain(new_seen.append)
|
|
|
|
|
assert [notification.method for notification in new_seen] == ["subagent.started"]
|
|
|
|
|
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "subagent.finished",
|
|
|
|
|
"params": {"parentSessionId": "old-parent", "childSessionId": "reused-child"},
|
|
|
|
|
})
|
|
|
|
|
old_subscription.drain(old_seen.append)
|
|
|
|
|
new_subscription.drain(new_seen.append)
|
|
|
|
|
assert [notification.method for notification in old_seen] == [
|
|
|
|
|
"subagent.started",
|
|
|
|
|
"subagent.finished",
|
|
|
|
|
]
|
|
|
|
|
assert [notification.method for notification in new_seen] == ["subagent.started"]
|
|
|
|
|
|
|
|
|
|
client._handle_message({
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": "session.event",
|
|
|
|
|
"params": {"sessionId": "reused-child", "event": {"type": "assistant/message"}},
|
|
|
|
|
})
|
|
|
|
|
old_subscription.drain(old_seen.append)
|
|
|
|
|
new_subscription.drain(new_seen.append)
|
|
|
|
|
|
|
|
|
|
assert [notification.method for notification in old_seen] == [
|
|
|
|
|
"subagent.started",
|
|
|
|
|
"subagent.finished",
|
|
|
|
|
]
|
|
|
|
|
assert [notification.method for notification in new_seen] == [
|
|
|
|
|
"subagent.started",
|
|
|
|
|
"session.event",
|
|
|
|
|
]
|
|
|
|
|
assert client._notifications.qsize() == 0
|
|
|
|
|
|
|
|
|
|
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
def test_client_contains_notification_filter_failure_to_its_subscription(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif method in {"emit-first", "emit-second"}:
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "tick", "params": {"source": method}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
2026-07-30 17:45:58 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"messageId": "message-1"}}), flush=True)
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def broken_filter(_notification: object) -> bool:
|
|
|
|
|
raise RuntimeError("bad notification filter")
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy
runtime process explicit. Document the context-manager/close contract and
update every construction example to use a context manager so repeated runs
remain valid without encouraging leaked subprocesses.
Contain notification predicate failures at the subscription boundary. Remove
only the subscriber whose callback raised, deliver that exception through its
queue, and continue dispatching to healthy subscribers so arbitrary callback
code cannot terminate the shared reader thread or strand later requests.
Enforce one in-flight prompt per server session with an atomic activePrompt
guard. Route overlap through the existing -32603 handler-error response and
clear the guard in finally, preserving parallel prompts across sessions and
sequential reuse without changing JSON-RPC request or notification shapes.
Use StringDecoder for line framing so a UTF-8 code point split across Buffer
chunks is not corrupted. Add a queued-write flush barrier, and make memoized
shutdown await it before disposal and exit while retaining exactly-once
cleanup when shutdown calls race or flushing fails.
Cover callback isolation, same-session exclusion, cross-session concurrency,
split multibyte input, delayed writes, racing shutdown, and flush failure with
deterministic tests.
2026-07-13 20:53:20 +08:00
|
|
|
with (
|
|
|
|
|
client.subscribe_notifications(broken_filter) as broken,
|
|
|
|
|
client.subscribe_notifications(lambda notification: notification.method == "tick") as healthy,
|
|
|
|
|
):
|
|
|
|
|
client.notify("emit-first")
|
|
|
|
|
with pytest.raises(RuntimeError, match="bad notification filter"):
|
|
|
|
|
broken.next()
|
|
|
|
|
assert healthy.next().payload == {"source": "emit-first"}
|
|
|
|
|
assert client._notifications.qsize() == 0
|
|
|
|
|
|
|
|
|
|
client.session_prompt("main", [{"type": "text", "text": "reader still works"}])
|
|
|
|
|
client.notify("emit-second")
|
|
|
|
|
assert healthy.next().payload == {"source": "emit-second"}
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 14:08:24 +08:00
|
|
|
def test_client_rejects_unaccepted_session_prompt_response(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif method == "session/prompt":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"accepted": False}}), flush=True)
|
|
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
with pytest.raises(ValueError):
|
2026-07-13 16:34:03 +08:00
|
|
|
client.session_prompt("main", [{"type": "text", "text": "fix it"}])
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_routes_bridge_requests_and_sends_responses(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
method = msg.get("method")
|
|
|
|
|
if method == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": "bridge-req-1", "method": "llm.request", "params": {"requestId": "req-1", "sessionId": "main", "model": "dsagent", "messages": []}}), flush=True)
|
|
|
|
|
elif "id" in msg and "method" not in msg:
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "method": "response/seen", "params": {"result": msg.get("result")}}), flush=True)
|
|
|
|
|
elif method == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
request = client.next_request()
|
|
|
|
|
assert request.id == "bridge-req-1"
|
|
|
|
|
assert request.method == "llm.request"
|
|
|
|
|
assert request.payload["requestId"] == "req-1"
|
|
|
|
|
|
|
|
|
|
client.respond(request.id, {"content_blocks": [{"type": "text", "text": "done"}]})
|
|
|
|
|
notification = client.next_notification()
|
|
|
|
|
assert notification.method == "response/seen"
|
|
|
|
|
assert notification.payload["result"]["content_blocks"][0]["text"] == "done"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_ignores_non_json_stdout_lines(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
print("node warning: experimental loader", flush=True)
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
with HarnessClient(_launch_args=(sys.executable, str(script))) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
assert init.serverInfo.name == "fake-dsh"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_request_times_out_when_bridge_does_not_respond(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
2026-08-11 14:27:59 +08:00
|
|
|
import sys
|
2026-07-11 14:08:24 +08:00
|
|
|
import time
|
|
|
|
|
|
2026-08-11 14:27:59 +08:00
|
|
|
print("bridge is still starting", file=sys.stderr, flush=True)
|
2026-07-11 14:08:24 +08:00
|
|
|
time.sleep(60)
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with HarnessClient(
|
|
|
|
|
HarnessConfig(
|
2026-08-23 19:04:19 +08:00
|
|
|
profile="web",
|
|
|
|
|
initialize_timeout_seconds=0.1,
|
2026-08-25 00:10:14 +08:00
|
|
|
),
|
|
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
) as client:
|
|
|
|
|
start = time.monotonic()
|
|
|
|
|
try:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-08-11 14:27:59 +08:00
|
|
|
except TimeoutError as exc:
|
2026-07-11 14:08:24 +08:00
|
|
|
assert time.monotonic() - start < 2
|
2026-08-11 14:27:59 +08:00
|
|
|
assert "bridge is still starting" in str(exc)
|
2026-08-23 19:04:19 +08:00
|
|
|
assert "profile 'web'" in str(exc)
|
2026-07-11 14:08:24 +08:00
|
|
|
else:
|
|
|
|
|
raise AssertionError("initialize should time out")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_close_times_out_when_shutdown_does_not_respond(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
2026-07-13 16:34:03 +08:00
|
|
|
import signal
|
2026-07-11 14:08:24 +08:00
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
|
2026-07-13 16:34:03 +08:00
|
|
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
|
|
|
|
|
2026-07-11 14:08:24 +08:00
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
time.sleep(60)
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client = HarnessClient(
|
|
|
|
|
HarnessConfig(
|
|
|
|
|
shutdown_timeout_seconds=0.1,
|
2026-08-25 00:10:14 +08:00
|
|
|
),
|
|
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
)
|
|
|
|
|
client.start()
|
2026-07-13 16:34:03 +08:00
|
|
|
proc = client._proc
|
|
|
|
|
assert proc is not None
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
start = time.monotonic()
|
|
|
|
|
client.close()
|
|
|
|
|
assert time.monotonic() - start < 2
|
2026-07-13 16:34:03 +08:00
|
|
|
assert proc.poll() is not None
|
|
|
|
|
assert client._proc is None
|
|
|
|
|
|
|
|
|
|
|
feat(python-runtime): package the Windows x64 dsh executable
Add node24-win-x64 as the only supported Windows runtime target and publish it as a py3-none-win_amd64 wheel containing the conventional dsh and ripgrep .exe payload names. Keep Windows ARM64 rejected explicitly so Python cannot claim a carrier that CI and release automation do not build.
Teach the pkg builder to require a native x64 Windows host, validate both node-pty ConPTY addons, copy @vscode's win32 ripgrep executable, and recognize pkg's .exe output. Extend runtime resolution, wheel staging, payload validation, and the preset closure check so the Windows-specific PowerShell plugins and sidecars fail loud when omitted.
The sidecar resolver now maps a packaged main.exe to main-rg.exe; focused TypeScript and Python tests cover that name, the win_amd64 manifest, x64-only host selection, complete wheel payload, ConPTY inventory, and platform-conditioned plugin closure.
2026-08-23 15:39:18 +08:00
|
|
|
def test_client_close_allows_eof_quiescence_after_shutdown_response(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_runtime.py"
|
|
|
|
|
marker = tmp_path / "quiesced.txt"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
|
|
|
|
|
time.sleep(0.05)
|
|
|
|
|
Path(os.environ["QUIESCED_MARKER"]).write_text("quiesced")
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client = HarnessClient(
|
|
|
|
|
HarnessConfig(
|
|
|
|
|
env={"QUIESCED_MARKER": str(marker)},
|
|
|
|
|
shutdown_timeout_seconds=1,
|
2026-08-25 00:10:14 +08:00
|
|
|
),
|
|
|
|
|
_launch_args=(sys.executable, str(script)),
|
feat(python-runtime): package the Windows x64 dsh executable
Add node24-win-x64 as the only supported Windows runtime target and publish it as a py3-none-win_amd64 wheel containing the conventional dsh and ripgrep .exe payload names. Keep Windows ARM64 rejected explicitly so Python cannot claim a carrier that CI and release automation do not build.
Teach the pkg builder to require a native x64 Windows host, validate both node-pty ConPTY addons, copy @vscode's win32 ripgrep executable, and recognize pkg's .exe output. Extend runtime resolution, wheel staging, payload validation, and the preset closure check so the Windows-specific PowerShell plugins and sidecars fail loud when omitted.
The sidecar resolver now maps a packaged main.exe to main-rg.exe; focused TypeScript and Python tests cover that name, the win_amd64 manifest, x64-only host selection, complete wheel payload, ConPTY inventory, and platform-conditioned plugin closure.
2026-08-23 15:39:18 +08:00
|
|
|
)
|
|
|
|
|
client.start()
|
|
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
assert marker.read_text() == "quiesced"
|
|
|
|
|
|
|
|
|
|
|
2026-07-13 16:34:03 +08:00
|
|
|
def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "rejecting_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
print("initialize diagnostic", file=sys.stderr, flush=True)
|
2026-07-13 16:34:03 +08:00
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
client = HarnessClient(_launch_args=(sys.executable, str(script)))
|
2026-07-13 16:34:03 +08:00
|
|
|
client.start()
|
|
|
|
|
proc = client._proc
|
|
|
|
|
assert proc is not None
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
with pytest.raises(JsonRpcError, match="bad initialize") as excinfo:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd=".", model="dsagent")
|
2026-07-13 16:34:03 +08:00
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert excinfo.value.code == -32000
|
|
|
|
|
assert "initialize diagnostic" in str(excinfo.value)
|
2026-07-13 16:34:03 +08:00
|
|
|
assert proc.wait(timeout=1) is not None
|
|
|
|
|
assert client._proc is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_public_signatures_omit_unsupported_wire_parameters() -> None:
|
|
|
|
|
from deepseek_harness import DeepSeekHarnessConfig, Session
|
|
|
|
|
|
|
|
|
|
assert "session_root" not in inspect.signature(HarnessClient.initialize).parameters
|
|
|
|
|
assert "system_prompt" not in inspect.signature(HarnessClient.initialize).parameters
|
|
|
|
|
assert "profile" not in inspect.signature(HarnessClient.session_prompt).parameters
|
|
|
|
|
assert "profile" not in inspect.signature(DeepSeekHarness.run).parameters
|
|
|
|
|
assert "profile" not in inspect.signature(Session.run).parameters
|
|
|
|
|
assert "system_prompt" not in DeepSeekHarnessConfig.__dataclass_fields__
|
2026-07-28 17:36:44 +08:00
|
|
|
assert "max_tokens" in DeepSeekHarnessConfig.__dataclass_fields__
|
2026-08-21 03:43:54 +08:00
|
|
|
assert "reasoning_effort" in DeepSeekHarnessConfig.__dataclass_fields__
|
2026-07-28 17:36:44 +08:00
|
|
|
assert "max_tokens" in inspect.signature(HarnessClient.initialize).parameters
|
2026-08-21 03:43:54 +08:00
|
|
|
assert "reasoning_effort" in inspect.signature(HarnessClient.initialize).parameters
|
2026-07-13 16:34:03 +08:00
|
|
|
assert "client_name" not in HarnessConfig.__dataclass_fields__
|
|
|
|
|
assert "client_version" not in HarnessConfig.__dataclass_fields__
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set(
|
|
|
|
|
DeepSeekHarnessConfig.__dataclass_fields__
|
|
|
|
|
)
|
|
|
|
|
assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set(
|
|
|
|
|
HarnessConfig.__dataclass_fields__
|
|
|
|
|
)
|
2026-08-23 19:04:19 +08:00
|
|
|
assert "initialize_timeout_seconds" in DeepSeekHarnessConfig.__dataclass_fields__
|
|
|
|
|
assert "initialize_timeout_seconds" in HarnessConfig.__dataclass_fields__
|
2026-08-24 19:07:21 +08:00
|
|
|
assert DeepSeekHarnessConfig().initialize_timeout_seconds == 30.0
|
|
|
|
|
assert HarnessConfig().initialize_timeout_seconds == 30.0
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"):
|
|
|
|
|
assert removed not in DeepSeekHarnessConfig.__dataclass_fields__
|
|
|
|
|
assert removed not in HarnessConfig.__dataclass_fields__
|
2026-08-25 00:10:14 +08:00
|
|
|
assert "_launch_args" not in HarnessConfig.__dataclass_fields__
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert "session_root" not in RunResult.__dataclass_fields__
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None:
|
|
|
|
|
HarnessClient().close()
|
|
|
|
|
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-25 00:10:14 +08:00
|
|
|
client = HarnessClient(_launch_args=(sys.executable, str(script)))
|
2026-07-11 14:08:24 +08:00
|
|
|
client.start()
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
client.close()
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_runtime_closed_error_includes_stderr_tail(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "crashing_runtime.py"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
print("fatal bridge exploded", file=sys.stderr, flush=True)
|
|
|
|
|
sys.exit(42)
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with HarnessClient(
|
|
|
|
|
HarnessConfig(
|
|
|
|
|
request_timeout_seconds=2,
|
2026-08-25 00:10:14 +08:00
|
|
|
),
|
|
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
) as client:
|
|
|
|
|
with pytest.raises(Exception, match="fatal bridge exploded"):
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_serializes_concurrent_writes(tmp_path: Path) -> None:
|
|
|
|
|
script = tmp_path / "fake_bridge.py"
|
|
|
|
|
output = tmp_path / "seen.jsonl"
|
|
|
|
|
script.write_text(
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
with open(os.environ["SEEN"], "w") as seen:
|
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
seen.write(line)
|
|
|
|
|
seen.flush()
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if "id" in msg and msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
|
|
|
|
elif "id" in msg and msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with HarnessClient(
|
|
|
|
|
HarnessConfig(
|
|
|
|
|
env={"SEEN": str(output)},
|
2026-08-25 00:10:14 +08:00
|
|
|
),
|
|
|
|
|
_launch_args=(sys.executable, str(script)),
|
2026-07-11 14:08:24 +08:00
|
|
|
) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
2026-07-11 14:08:24 +08:00
|
|
|
threads = [
|
|
|
|
|
threading.Thread(target=client.notify, args=(f"notice-{index}", {"index": index}))
|
|
|
|
|
for index in range(50)
|
|
|
|
|
]
|
|
|
|
|
for thread in threads:
|
|
|
|
|
thread.start()
|
|
|
|
|
for thread in threads:
|
|
|
|
|
thread.join()
|
|
|
|
|
|
|
|
|
|
for line in output.read_text().splitlines():
|
|
|
|
|
json.loads(line)
|
|
|
|
|
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
def _install_fake_bundled_dsh(
|
2026-07-11 22:54:42 +08:00
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
) -> None:
|
|
|
|
|
"""Install a fake runtime package that records dsh argv and serves lifecycle calls."""
|
|
|
|
|
runtime = tmp_path / "dsh.py"
|
2026-07-11 14:08:24 +08:00
|
|
|
runtime.write_text(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
"""
|
2026-07-11 14:08:24 +08:00
|
|
|
import json
|
2026-07-11 22:54:42 +08:00
|
|
|
import os
|
2026-07-11 14:08:24 +08:00
|
|
|
import sys
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
json.dump({
|
|
|
|
|
"argv": sys.argv[1:],
|
|
|
|
|
"DSH_HOME": os.environ.get("DSH_HOME"),
|
|
|
|
|
"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"),
|
|
|
|
|
}, open(os.environ["ENV_DUMP"], "w"))
|
2026-07-11 14:08:24 +08:00
|
|
|
for line in sys.stdin:
|
|
|
|
|
msg = json.loads(line)
|
|
|
|
|
if msg.get("method") == "initialize":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "bundled-runtime"}}}), flush=True)
|
|
|
|
|
elif msg.get("method") == "shutdown":
|
|
|
|
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
|
|
|
|
break
|
|
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
module_dir = tmp_path / "deepseek_harness_runtime"
|
|
|
|
|
module_dir.mkdir()
|
|
|
|
|
(module_dir / "__init__.py").write_text(
|
|
|
|
|
f"""
|
|
|
|
|
def resolve_bundled_launch_args(mode=None):
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
return ({sys.executable!r}, {str(runtime)!r})
|
2026-07-11 14:08:24 +08:00
|
|
|
""".strip()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.syspath_prepend(str(tmp_path))
|
|
|
|
|
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
|
2026-07-11 22:54:42 +08:00
|
|
|
|
2026-07-11 14:08:24 +08:00
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
def test_client_default_launch_uses_bundled_dsh_sdk_profile_and_explicit_home(
|
|
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
2026-07-11 22:54:42 +08:00
|
|
|
) -> None:
|
|
|
|
|
env_dump = tmp_path / "env.json"
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
home = tmp_path / "home"
|
|
|
|
|
patch = tmp_path / "sdk.patch.yml"
|
|
|
|
|
patch.write_text("[]\n")
|
|
|
|
|
_install_fake_bundled_dsh(tmp_path, monkeypatch)
|
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
|
monkeypatch.setenv("DSH_HOME", str(tmp_path / "ambient-home"))
|
|
|
|
|
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
|
2026-07-11 22:54:42 +08:00
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
with HarnessClient(HarnessConfig(
|
|
|
|
|
profile="sdk",
|
|
|
|
|
patches=("sdk.patch.yml",),
|
|
|
|
|
dsh_home=str(home),
|
|
|
|
|
env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(tmp_path / "env-home")},
|
|
|
|
|
)) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro")
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
assert init.serverInfo.name == "bundled-runtime"
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert json.loads(env_dump.read_text()) == {
|
|
|
|
|
"argv": ["--profile", "sdk", "--patch", str(patch)],
|
|
|
|
|
"DSH_HOME": str(home),
|
|
|
|
|
"DSH_CORDIS_CONFIG": None,
|
|
|
|
|
}
|
2026-07-11 22:54:42 +08:00
|
|
|
|
|
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
def test_client_accepts_explicit_environment_dsh_home(
|
2026-07-11 22:54:42 +08:00
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
env_dump = tmp_path / "env.json"
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
home = tmp_path / "environment-home"
|
|
|
|
|
_install_fake_bundled_dsh(tmp_path, monkeypatch)
|
2026-07-11 22:54:42 +08:00
|
|
|
|
|
|
|
|
with HarnessClient(
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
HarnessConfig(profile="custom", env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(home)})
|
2026-07-11 22:54:42 +08:00
|
|
|
) as client:
|
2026-07-29 16:36:07 +08:00
|
|
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro")
|
2026-07-11 22:54:42 +08:00
|
|
|
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
assert json.loads(env_dump.read_text()) == {
|
|
|
|
|
"argv": ["--profile", "custom"],
|
|
|
|
|
"DSH_HOME": str(home),
|
|
|
|
|
"DSH_CORDIS_CONFIG": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_rejects_an_implicit_default_dsh_home(
|
|
|
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
_install_fake_bundled_dsh(tmp_path, monkeypatch)
|
|
|
|
|
monkeypatch.delenv("DSH_HOME", raising=False)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match="explicit dsh_home or non-empty DSH_HOME"):
|
|
|
|
|
HarnessClient(HarnessConfig(env={})).start()
|
2026-07-11 14:08:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
|
|
|
|
|
monkeypatch.setattr(sys, "path", [])
|
|
|
|
|
|
|
|
|
|
with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"):
|
feat(python-sdk): launch dsh profiles from explicit homes
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
2026-08-23 14:56:16 +08:00
|
|
|
HarnessClient(HarnessConfig(dsh_home="/explicit/home")).start()
|