139 lines
6.6 KiB
Python
139 lines
6.6 KiB
Python
"""Explicit command transport, results, and ordinary prompt compatibility."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from deepseek_harness import CommandErrorResult, CommandExecution, CommandSuccessResult, DeepSeekHarness
|
|
from deepseek_harness.errors import JsonRpcError, SdkProtocolError
|
|
|
|
|
|
@pytest.fixture
|
|
def peer(tmp_path: Path) -> Path:
|
|
script = tmp_path / "commands.py"
|
|
script.write_text('''import json
|
|
import os
|
|
import sys
|
|
|
|
def send(frame):
|
|
print(json.dumps({"jsonrpc": "2.0", **frame}), flush=True)
|
|
|
|
for line in sys.stdin:
|
|
frame = json.loads(line)
|
|
with open(os.environ["REQUEST_LOG"], "a", encoding="utf-8") as log:
|
|
log.write(line)
|
|
method = frame["method"]
|
|
params = frame.get("params") or {}
|
|
if method == "initialize":
|
|
send({"id": frame["id"], "result": {"serverInfo": {"name": "command-peer"}}})
|
|
elif method == "session/command":
|
|
for event_type in ("command/run", "command/done"):
|
|
send({"method": "session.event", "params": {"sessionId": params["sessionId"],
|
|
"event": {"type": event_type, "data": {"commandId": "command-1"}}}})
|
|
send({"id": frame["id"], **json.loads(os.environ["COMMAND_RESPONSE"])})
|
|
elif method == "session/prompt":
|
|
session = params["sessionId"]
|
|
send({"id": frame["id"], "result": {"messageId": "input-1"}})
|
|
events = [
|
|
{"type": "agent/inbox/spliced", "data": {"inserted": [{"id": "input-1"}]}},
|
|
{"type": "assistant/message", "data": {"message": {"content": [{"type": "text", "text": "ordinary prompt"}]}}},
|
|
{"type": "turn/end", "data": {"turn": 1, "reason": {"kind": "completed"}}},
|
|
]
|
|
for event in events:
|
|
send({"method": "session.event", "params": {"sessionId": session, "event": event}})
|
|
send({"method": "session.status", "params": {"sessionId": session, "status": "idle"}})
|
|
elif method == "shutdown":
|
|
send({"id": frame["id"], "result": {}})
|
|
break
|
|
else:
|
|
raise AssertionError(method)
|
|
''', encoding="utf-8")
|
|
return script
|
|
|
|
|
|
def harness(peer: Path, tmp_path: Path, response: dict) -> DeepSeekHarness:
|
|
return DeepSeekHarness(
|
|
cwd=str(tmp_path),
|
|
dsh_home=str(tmp_path / "home"),
|
|
request_timeout_seconds=5,
|
|
_launch_args=(sys.executable, str(peer)),
|
|
env={"REQUEST_LOG": str(tmp_path / "requests.jsonl"), "COMMAND_RESPONSE": json.dumps(response)},
|
|
)
|
|
|
|
|
|
def frames(tmp_path: Path) -> list[dict]:
|
|
return [json.loads(line) for line in (tmp_path / "requests.jsonl").read_text(encoding="utf-8").splitlines()]
|
|
|
|
|
|
@pytest.mark.parametrize("result", [
|
|
{"kind": "success"},
|
|
{"kind": "success", "text": "Compacted.", "sourceEventSeq": 0},
|
|
{"kind": "success", "sourceEventSeq": 2**53 - 1},
|
|
{"kind": "error", "text": "The agent is busy."},
|
|
{"kind": "error", "text": " The agent is busy.\n"},
|
|
])
|
|
def test_command_returns_without_turn_or_idle_and_preserves_events(peer: Path, tmp_path: Path, result: dict) -> None:
|
|
response = {"commandId": "command-1", "result": result}
|
|
with harness(peer, tmp_path, {"result": response}) as sdk:
|
|
session = sdk.start_session("main")
|
|
with sdk.client.subscribe_session_notifications(session.id) as subscription:
|
|
execution = sdk.client.session_command(session.id, "/compact")
|
|
notifications = []
|
|
subscription.drain(notifications.append)
|
|
assert isinstance(execution, CommandExecution)
|
|
assert execution.command_id == "command-1"
|
|
assert isinstance(execution.result, CommandSuccessResult if result["kind"] == "success" else CommandErrorResult)
|
|
assert execution.model_dump(by_alias=True, exclude_none=True) == response
|
|
assert [item.payload["event"]["type"] for item in notifications] == ["command/run", "command/done"]
|
|
sent = frames(tmp_path)
|
|
assert [frame["method"] for frame in sent] == ["initialize", "session/command", "shutdown"]
|
|
assert sent[1]["params"] == {"sessionId": "main", "line": "/compact"}
|
|
|
|
|
|
def test_low_level_command_preserves_the_line(peer: Path, tmp_path: Path) -> None:
|
|
with harness(peer, tmp_path, {"result": {"commandId": "command-1", "result": {"kind": "success"}}}) as sdk:
|
|
assert sdk.client.session_command("other", "/compact \t").result.kind == "success"
|
|
assert frames(tmp_path)[1]["params"] == {"sessionId": "other", "line": "/compact \t"}
|
|
|
|
|
|
def test_command_rpc_rejection_does_not_fall_back_to_prompt(peer: Path, tmp_path: Path) -> None:
|
|
with harness(peer, tmp_path, {"error": {"code": -32602, "message": "unsupported command"}}) as sdk:
|
|
with pytest.raises(JsonRpcError, match="unsupported command") as raised:
|
|
sdk.client.session_command("main", "/model")
|
|
assert raised.value.code == -32602
|
|
assert [frame["method"] for frame in frames(tmp_path)] == ["initialize", "session/command", "shutdown"]
|
|
|
|
|
|
@pytest.mark.parametrize("response", [
|
|
{},
|
|
{"commandId": "command-1"},
|
|
{"commandId": "", "result": {"kind": "success"}},
|
|
{"commandId": "command-1", "result": {"kind": "unknown"}},
|
|
{"commandId": "command-1", "result": {"kind": "error"}},
|
|
{"commandId": "command-1", "result": {"kind": "error", "text": ""}},
|
|
{"commandId": "command-1", "result": {"kind": "error", "text": " \t\n"}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "text": None}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "sourceEventSeq": None}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "sourceEventSeq": "4"}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "sourceEventSeq": True}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "sourceEventSeq": -1}},
|
|
{"commandId": "command-1", "result": {"kind": "success", "sourceEventSeq": 2**53}},
|
|
])
|
|
def test_command_rejects_malformed_results(peer: Path, tmp_path: Path, response: dict) -> None:
|
|
with harness(peer, tmp_path, {"result": response}) as sdk:
|
|
with pytest.raises(SdkProtocolError) as raised:
|
|
sdk.client.session_command("main", "/compact")
|
|
assert isinstance(raised.value.__cause__, ValidationError)
|
|
|
|
|
|
def test_low_level_prompt_keeps_slash_text_as_an_ordinary_prompt(peer: Path, tmp_path: Path) -> None:
|
|
with harness(peer, tmp_path, {"result": {}}) as sdk:
|
|
assert sdk.client.session_prompt("main", [{"type": "text", "text": "/compact"}]) == "input-1"
|
|
sent = frames(tmp_path)
|
|
assert [frame["method"] for frame in sent] == ["initialize", "session/prompt", "shutdown"]
|
|
assert sent[1]["params"] == {"sessionId": "main", "contentBlocks": [{"type": "text", "text": "/compact"}]}
|