Files
deepseek-harness/python/sdk/tests/test_queries.py
T

392 lines
14 KiB
Python

"""Split query submission and per-request response observation."""
from __future__ import annotations
import sys
import threading
from pathlib import Path
import pytest
from pydantic import BaseModel, ValidationError
from deepseek_harness import CommandExecution, DeepSeekHarness, Notification, RunResult, SdkProtocolError
from deepseek_harness.errors import JsonRpcError, TransportClosedError
class Ack(BaseModel):
pass
@pytest.fixture
def peer(tmp_path: Path) -> Path:
script = tmp_path / "queries.py"
script.write_text('''import json
import sys
serial = 0
pending_commands = []
pending_prompts = []
def send(frame):
print(json.dumps({"jsonrpc": "2.0", **frame}), flush=True)
def event(session, type, data):
send({"method": "session.event", "params": {"sessionId": session, "event": {"type": type, "data": data}}})
def command(frame, number):
session = frame["params"]["sessionId"]
identity = "command-" + str(number)
event(session, "command/run", {"commandId": identity})
event(session, "command/done", {"commandId": identity, "kind": "success"})
send({"id": frame["id"], "result": {"kind": "command", "execution": {
"commandId": identity, "result": {"kind": "success", "text": "compacted"}}}})
def receipt(frame, number):
session = frame["params"]["sessionId"]
send({"method": "session.status", "params": {"sessionId": session, "status": "idle"}})
event(session, "agent/inbox/spliced", {"inserted": [{"id": "input-" + str(number)}]})
def finish(frame):
session = frame["params"]["sessionId"]
event(session, "assistant/message", {"message": {"content": [
{"type": "text", "text": "answer for " + frame["params"]["text"]}]}})
event(session, "turn/end", {"turn": 1, "reason": {"kind": "completed"}})
send({"method": "session.status", "params": {"sessionId": session, "status": "idle"}})
for line in sys.stdin:
frame = json.loads(line)
method = frame["method"]
if method == "initialize":
send({"id": frame["id"], "result": {"serverInfo": {"name": "query-peer"}}})
elif method == "session/query":
serial += 1
params = frame["params"]
assert set(params) == {"sessionId", "text", "delivery"}
assert params["delivery"] in ("queue", "steer")
text = params["text"]
if text == "hold-command":
pending_commands.append((frame, serial))
elif text == "/compact":
command(frame, serial)
elif text == "silent-command":
send({"id": frame["id"], "result": {"kind": "command", "execution": {
"commandId": "silent", "result": {"kind": "success"}}}})
elif text == "rpc-error":
send({"id": frame["id"], "error": {"code": -32602, "message": "query rejected"}})
elif text == "bad-route":
send({"id": frame["id"], "result": {"kind": "unknown"}})
elif text == "bad-execution":
send({"id": frame["id"], "result": {"kind": "command", "execution": {"commandId": "c"}}})
else:
result = {"kind": "prompt", "messageId": "input-" + str(serial)}
if text == "fast-prompt":
receipt(frame, serial)
finish(frame)
send({"id": frame["id"], "result": result})
else:
send({"id": frame["id"], "result": result})
receipt(frame, serial)
if text == "hold-prompt":
pending_prompts.append(frame)
else:
finish(frame)
elif method == "release":
for original, number in pending_commands:
command(original, number)
pending_commands.clear()
for original in pending_prompts:
finish(original)
pending_prompts.clear()
send({"id": frame["id"], "result": {}})
elif method == "barrier":
send({"id": frame["id"], "result": {}})
elif method == "disconnect":
send({"id": frame["id"], "result": {}})
break
elif method == "shutdown":
send({"id": frame["id"], "result": {}})
break
else:
raise AssertionError(method)
''', encoding="utf-8")
return script
@pytest.fixture
def sdk(peer: Path, tmp_path: Path):
with DeepSeekHarness(
cwd=str(tmp_path), dsh_home=str(tmp_path / "home"), request_timeout_seconds=5,
_launch_args=(sys.executable, str(peer)),
) as harness:
yield harness
def release(sdk: DeepSeekHarness) -> None:
sdk.client.request("release", {}, response_model=Ack, timeout_seconds=5)
def assert_released(sdk: DeepSeekHarness) -> None:
assert sdk.client._responses == {}
assert sdk.client._notification_subscribers == {}
def test_query_only_sends_then_receive_returns_command(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
assert session.query("hold-command") is None
assert len(sdk.client._responses) == 1
release(sdk)
responses = list(session.receive_response())
assert [item.payload["event"]["type"] for item in responses[:-1]] == ["command/run", "command/done"]
assert isinstance(responses[-1], CommandExecution)
assert responses[-1].command_id == "command-1"
assert_released(sdk)
@pytest.mark.parametrize("text", ["ordinary", "fast-prompt", "/model", "/permission"])
@pytest.mark.parametrize("delivery", ["queue", "steer"])
def test_prompt_yields_receipt_through_idle_then_run_result(sdk: DeepSeekHarness, text: str, delivery: str) -> None:
session = sdk.start_session("main")
session.query(text, delivery=delivery)
responses = list(session.receive_response())
assert all(isinstance(item, Notification) for item in responses[:-1])
assert responses[0].payload["event"]["type"] == "agent/inbox/spliced"
result = responses[-1]
assert isinstance(result, RunResult)
assert result.final_response == "answer for " + text
assert result.finish_reason == "completed"
assert result.notifications == responses[:-1]
assert len(result.events) == 3
assert_released(sdk)
def test_late_receive_uses_an_arrived_response_before_expired_deadline(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
sdk.client.config.request_timeout_seconds = 0
session.query("/compact")
sdk.client.request("barrier", {}, response_model=Ack, timeout_seconds=5)
assert isinstance(list(session.receive_response())[-1], CommandExecution)
assert_released(sdk)
def test_pending_response_timeout_releases_only_its_observer(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
sdk.client.config.request_timeout_seconds = 0
session.query("hold-command")
with pytest.raises(TimeoutError, match="session/query timed out"):
list(session.receive_response())
assert_released(sdk)
release(sdk)
@pytest.mark.parametrize("text,error", [
("rpc-error", JsonRpcError), ("bad-route", SdkProtocolError), ("bad-execution", ValidationError),
])
def test_query_errors_arrive_during_receive_and_leave_later_queries_usable(sdk: DeepSeekHarness, text: str, error: type) -> None:
session = sdk.start_session("main")
session.query(text)
with pytest.raises(error):
list(session.receive_response())
assert_released(sdk)
session.query("/compact")
assert isinstance(list(session.receive_response())[-1], CommandExecution)
def test_receivers_claim_fifo_even_when_responses_arrive_out_of_order(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("hold-command")
session.query("/compact")
release(sdk)
first = session.receive_response()
second = session.receive_response()
assert list(second)[-1].command_id == "command-2"
assert list(first)[-1].command_id == "command-1"
assert_released(sdk)
with pytest.raises(RuntimeError, match="preceding query"):
session.receive_response()
def test_command_receiver_stops_at_its_done_before_a_later_prompt(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("/compact")
session.query("ordinary")
sdk.client.request("barrier", {}, response_model=Ack)
command = list(session.receive_response())
assert [item.payload["event"]["type"] for item in command[:-1]] == ["command/run", "command/done"]
assert command[-1].command_id == "command-1"
prompt = list(session.receive_response())
assert isinstance(prompt[-1], RunResult)
assert prompt[-1].final_response == "answer for ordinary"
assert_released(sdk)
def test_command_result_without_notifications_does_not_wait_for_done(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("silent-command")
responses = list(session.receive_response())
assert len(responses) == 1
assert responses[0].command_id == "silent"
assert_released(sdk)
@pytest.mark.parametrize("text", ["/compact", "silent-command"])
def test_late_command_receive_preserves_completed_result_after_eof(sdk: DeepSeekHarness, text: str) -> None:
session = sdk.start_session("main")
session.query(text)
sdk.client.request("disconnect", {}, response_model=Ack)
sdk.client._reader_thread.join(5)
assert not sdk.client._reader_thread.is_alive()
responses = list(session.receive_response())
assert isinstance(responses[-1], CommandExecution)
assert responses[-1].result.kind == "success"
if text == "/compact":
assert [item.payload["event"]["type"] for item in responses[:-1]] == ["command/run", "command/done"]
else:
assert len(responses) == 1
assert_released(sdk)
def test_command_receive_does_not_hide_other_subscription_errors(sdk: DeepSeekHarness, monkeypatch) -> None:
session = sdk.start_session("main")
session.query("/compact")
sdk.client.request("barrier", {}, response_model=Ack)
receiver = session.receive_response()
def fail():
raise SdkProtocolError("invalid notification")
monkeypatch.setattr(receiver._subscription, "_next_queued", fail)
with pytest.raises(SdkProtocolError, match="invalid notification"):
list(receiver)
assert_released(sdk)
def test_query_and_another_receiver_can_run_while_a_prompt_receiver_waits(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("hold-prompt")
receiver = session.receive_response()
entered = threading.Event()
responses = []
errors = []
def receive() -> None:
try:
for item in receiver:
responses.append(item)
entered.set()
except BaseException as error:
errors.append(error)
thread = threading.Thread(target=receive)
thread.start()
try:
assert entered.wait(5)
session.query("/compact")
assert list(session.receive_response())[-1].command_id == "command-2"
assert thread.is_alive()
release(sdk)
thread.join(5)
assert not thread.is_alive()
assert not errors
assert isinstance(responses[-1], RunResult)
assert responses[-1].final_response == "answer for hold-prompt"
assert_released(sdk)
finally:
receiver.close()
thread.join(5)
def test_iterator_close_before_first_next_releases_request_and_subscription(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("hold-command")
receiver = session.receive_response()
receiver.close()
receiver.close()
assert list(receiver) == []
assert_released(sdk)
release(sdk)
session.query("/compact")
assert list(session.receive_response())[-1].command_id == "command-2"
def test_iterator_close_after_a_prompt_notification_releases_its_subscription(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("hold-prompt")
receiver = session.receive_response()
assert isinstance(next(receiver), Notification)
receiver.close()
assert list(receiver) == []
assert_released(sdk)
release(sdk)
def test_iterator_close_wakes_its_blocked_request_receiver(sdk: DeepSeekHarness, monkeypatch) -> None:
session = sdk.start_session("main")
session.query("hold-command")
receiver = session.receive_response()
entered = threading.Event()
errors = []
original = sdk.client._wait_request
def wait(request, **kwargs):
entered.set()
return original(request, **kwargs)
monkeypatch.setattr(sdk.client, "_wait_request", wait)
def receive() -> None:
try:
assert list(receiver) == []
except BaseException as error:
errors.append(error)
thread = threading.Thread(target=receive)
thread.start()
try:
assert entered.wait(5)
receiver.close()
thread.join(5)
assert not thread.is_alive()
assert not errors
assert_released(sdk)
finally:
receiver.close()
thread.join(5)
def test_terminal_result_releases_resources_before_caller_sends_another_query(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("/compact")
receiver = session.receive_response()
for item in receiver:
if isinstance(item, CommandExecution):
assert_released(sdk)
session.query("/compact")
receiver.close()
assert list(session.receive_response())[-1].command_id == "command-2"
assert_released(sdk)
def test_runtime_close_settles_unreceived_queries(sdk: DeepSeekHarness) -> None:
session = sdk.start_session("main")
session.query("hold-command")
sdk.close()
with pytest.raises(TransportClosedError):
list(session.receive_response())
assert_released(sdk)
def test_send_failure_releases_subscription_without_queueing_a_receiver(sdk: DeepSeekHarness, monkeypatch) -> None:
session = sdk.start_session("main")
original = sdk.client._write_message
def write(message):
if message.get("method") == "session/query":
raise TransportClosedError("write failed")
original(message)
monkeypatch.setattr(sdk.client, "_write_message", write)
with pytest.raises(TransportClosedError, match="write failed"):
session.query("ordinary")
assert_released(sdk)
with pytest.raises(RuntimeError, match="preceding query"):
session.receive_response()