manual_sdk_agent_smoke.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """Manual keyless smoke: drive the repo-source jsonrpc-agent bin (node + tsx).
  2. Runs the SDK against `packages/ui/jsonrpc-agent/src/bin.ts` executed from the
  3. repo checkout (requires `pnpm install`; no build, no API key — the model
  4. endpoint is a local mock SSE server). The bin only boots the supplied
  5. cordis.yml — the stdio JSON-RPC server itself comes from the config's
  6. `@deepseek-ai/dsh-jsonrpc` entry — so the runtime package's default cordis.yml
  7. is passed explicitly. Not collected by pytest; run it directly:
  8. `python tests/manual_sdk_agent_smoke.py`.
  9. """
  10. from __future__ import annotations
  11. import argparse
  12. import json
  13. import shutil
  14. import tempfile
  15. import threading
  16. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  17. from pathlib import Path
  18. from typing import Any
  19. from deepseek_harness import DeepSeekHarness
  20. from deepseek_harness_runtime import bundled_default_config_path
  21. class MockCompletionHandler(BaseHTTPRequestHandler):
  22. requests: list[dict[str, Any]] = []
  23. def do_POST(self) -> None:
  24. length = int(self.headers.get("content-length", "0"))
  25. body = self.rfile.read(length).decode("utf-8")
  26. self.requests.append({
  27. "path": self.path,
  28. "authorization": self.headers.get("authorization"),
  29. "body": json.loads(body),
  30. })
  31. self.send_response(200)
  32. self.send_header("content-type", "text/event-stream")
  33. self.end_headers()
  34. self.wfile.write(b'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}\n\n')
  35. self.wfile.write(b'data: {"choices":[{"delta":{"content":"SDK runtime reached the configured HTTP model endpoint."}}]}\n\n')
  36. self.wfile.write(b'data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":9}}\n\n')
  37. self.wfile.write(b"data: [DONE]\n\n")
  38. def log_message(self, _format: str, *_args: object) -> None:
  39. return
  40. def run_smoke(repo_root: Path, keep_sessions: bool) -> None:
  41. session_root = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-sessions-"))
  42. runtime_entry = repo_root / "packages/ui/jsonrpc-agent/src/bin.ts"
  43. server = ThreadingHTTPServer(("127.0.0.1", 0), MockCompletionHandler)
  44. thread = threading.Thread(target=server.serve_forever, name="mock-openai-compatible-server", daemon=True)
  45. thread.start()
  46. base_url = f"http://127.0.0.1:{server.server_address[1]}"
  47. print(f"repo_root={repo_root}")
  48. print(f"session_root={session_root}")
  49. print(f"mock_base_url={base_url}")
  50. try:
  51. with DeepSeekHarness(
  52. model="sdk-smoke-model",
  53. cwd=str(repo_root / "python/sdk"),
  54. runtime_cwd=str(repo_root),
  55. session_root=str(session_root),
  56. cordis=str(bundled_default_config_path()),
  57. launch_args_override=("node", "--import", "tsx", str(runtime_entry)),
  58. env={
  59. "DEEPSEEK_BASE_URL": base_url,
  60. "DEEPSEEK_API_KEY": "sdk-smoke-key",
  61. },
  62. request_timeout_seconds=20,
  63. shutdown_timeout_seconds=2,
  64. ) as harness:
  65. result = harness.run(
  66. "Please reply with a short confirmation and do not call tools.",
  67. session_id="sdk-smoke-main",
  68. )
  69. print(f"turn_status={result.status}")
  70. print(f"final_response={result.final_response}")
  71. assert result.status == "ok", result
  72. assert "configured HTTP model endpoint" in result.final_response
  73. assert len(MockCompletionHandler.requests) == 1
  74. request = MockCompletionHandler.requests[0]
  75. print(json.dumps(request, ensure_ascii=False, indent=2)[:4000])
  76. assert request["authorization"] == "Bearer sdk-smoke-key"
  77. assert request["body"]["model"] == "sdk-smoke-model"
  78. jsonl_files = sorted(session_root.rglob("*.jsonl"))
  79. assert jsonl_files, f"no jsonl sessions were written under {session_root}"
  80. print("session_jsonl_files:")
  81. for path in jsonl_files:
  82. print(f" {path} bytes={path.stat().st_size}")
  83. with path.open("r", encoding="utf-8") as handle:
  84. first_line = handle.readline().strip()
  85. if first_line:
  86. print(f" first_line={first_line[:500]}")
  87. finally:
  88. server.shutdown()
  89. server.server_close()
  90. if keep_sessions:
  91. print(f"kept_session_root={session_root}")
  92. else:
  93. shutil.rmtree(session_root)
  94. print("removed temporary session root")
  95. def main() -> None:
  96. parser = argparse.ArgumentParser()
  97. parser.add_argument(
  98. "--repo-root",
  99. type=Path,
  100. default=Path(__file__).resolve().parents[3],
  101. help="Path to the deepseek-harness checkout.",
  102. )
  103. parser.add_argument("--keep-sessions", action="store_true")
  104. args = parser.parse_args()
  105. run_smoke(args.repo_root.resolve(), args.keep_sessions)
  106. if __name__ == "__main__":
  107. main()