test_runtime_resolution.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. """Keyless runtime-resolution tests; launch coverage lives in test_bundled_runtime.py."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import subprocess
  6. import sys
  7. from pathlib import Path
  8. from types import SimpleNamespace
  9. import deepseek_harness_runtime as runtime
  10. import pytest
  11. from deepseek_harness_runtime import (
  12. RUNTIME_MODE_ENV_VAR,
  13. bundled_package_dir,
  14. main,
  15. resolve_bundled_launch_args,
  16. )
  17. def _office_sidecar(executable: Path, native_targets: tuple[str, ...] = ("darwin-arm64", "darwin-x64", "win32-x64")) -> Path:
  18. office = executable.with_name(f"{executable.name.removesuffix('.exe')}-office")
  19. tag = executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
  20. native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
  21. engine = native if native in native_targets else "wasm"
  22. for required in ("@deepseek-ai/libreoffice-kit/package.json", f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json"):
  23. path = office / "node_modules" / required
  24. path.parent.mkdir(parents=True, exist_ok=True)
  25. path.write_text("{}")
  26. adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
  27. adapter.write_text(json.dumps({"optionalDependencies": {
  28. f"@deepseek-ai/libreoffice-kit-{target}": "0.0.1" for target in (*native_targets, "wasm")
  29. }}), encoding="utf-8")
  30. return office
  31. def test_unknown_explicit_mode_fails_loud() -> None:
  32. with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
  33. resolve_bundled_launch_args("bogus")
  34. def test_unknown_env_mode_fails_loud(monkeypatch: pytest.MonkeyPatch) -> None:
  35. monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
  36. with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
  37. resolve_bundled_launch_args()
  38. def test_explicit_mode_wins_over_env_mode(monkeypatch: pytest.MonkeyPatch) -> None:
  39. monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
  40. try:
  41. args = resolve_bundled_launch_args("exe")
  42. except FileNotFoundError:
  43. return # explicit 'exe' was honored; only the artifact is missing
  44. assert args[0].endswith(("-x64", "-arm64"))
  45. def test_runtime_requires_spawn_helper_only_on_macos(
  46. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  47. ) -> None:
  48. runtime_dir = tmp_path / "runtime"
  49. runtime_dir.mkdir()
  50. linux = runtime_dir / "deepseek-harness-sdk-runtime-linux-x64"
  51. linux.touch()
  52. Path(f"{linux}-rg").touch()
  53. _office_sidecar(linux)
  54. macos = runtime_dir / "deepseek-harness-sdk-runtime-macos-arm64"
  55. macos.touch()
  56. Path(f"{macos}-rg").touch()
  57. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  58. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "macos-arm64")
  59. with pytest.raises(FileNotFoundError, match="node-pty spawn helper"):
  60. runtime.bundled_runtime_path()
  61. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
  62. assert runtime.bundled_runtime_path() == linux
  63. def test_windows_runtime_uses_exe_payload_and_exe_sidecar(
  64. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  65. ) -> None:
  66. runtime_dir = tmp_path / "runtime"
  67. runtime_dir.mkdir()
  68. executable = runtime_dir / "deepseek-harness-sdk-runtime-win-x64.exe"
  69. executable.touch()
  70. (runtime_dir / "deepseek-harness-sdk-runtime-win-x64-rg.exe").touch()
  71. _office_sidecar(executable)
  72. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  73. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "win-x64")
  74. assert runtime.bundled_runtime_path() == executable
  75. def test_current_platform_supports_windows_x64_only(monkeypatch: pytest.MonkeyPatch) -> None:
  76. monkeypatch.setattr(runtime.sys, "platform", "win32")
  77. monkeypatch.setattr(runtime.platform, "machine", lambda: "AMD64")
  78. assert runtime._current_platform_tag() == "win-x64"
  79. monkeypatch.setattr(runtime.platform, "machine", lambda: "ARM64")
  80. with pytest.raises(FileNotFoundError, match="Windows x64"):
  81. runtime._current_platform_tag()
  82. def test_current_platform_supports_macos_x64(monkeypatch: pytest.MonkeyPatch) -> None:
  83. monkeypatch.setattr(runtime.sys, "platform", "darwin")
  84. monkeypatch.setattr(runtime.platform, "machine", lambda: "x86_64")
  85. assert runtime._current_platform_tag() == "macos-x64"
  86. def test_runtime_requires_ripgrep_sidecar(
  87. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  88. ) -> None:
  89. runtime_dir = tmp_path / "runtime"
  90. runtime_dir.mkdir()
  91. (runtime_dir / "deepseek-harness-sdk-runtime-linux-x64").touch()
  92. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  93. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
  94. with pytest.raises(FileNotFoundError, match="ripgrep sidecar"):
  95. runtime.bundled_runtime_path()
  96. def test_runtime_requires_complete_office_sidecar(
  97. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  98. ) -> None:
  99. executable = tmp_path / "runtime" / "deepseek-harness-sdk-runtime-linux-x64"
  100. executable.parent.mkdir()
  101. executable.touch()
  102. Path(f"{executable}-rg").touch()
  103. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  104. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
  105. with pytest.raises(FileNotFoundError, match="Office sidecar"):
  106. runtime.bundled_runtime_path()
  107. office = _office_sidecar(executable)
  108. assert runtime.bundled_runtime_path() == executable
  109. (office / "node_modules/@deepseek-ai/libreoffice-kit-wasm/prebuilds.json").unlink()
  110. with pytest.raises(FileNotFoundError, match="Office sidecar"):
  111. runtime.bundled_runtime_path()
  112. def test_node_mode_runs_the_deployed_dsh_cli(
  113. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  114. ) -> None:
  115. bin_js = tmp_path / "runtime" / "node" / "node_modules" / "@deepseek-ai" / "dsh" / "lib" / "bin.js"
  116. bin_js.parent.mkdir(parents=True)
  117. bin_js.touch()
  118. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  119. monkeypatch.setattr(runtime.shutil, "which", lambda _name: "/node")
  120. assert resolve_bundled_launch_args("node") == ("/node", str(bin_js))
  121. def test_python_dsh_command_requires_explicit_home(
  122. monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
  123. ) -> None:
  124. monkeypatch.delenv("DSH_HOME", raising=False)
  125. with pytest.raises(SystemExit) as excinfo:
  126. main()
  127. assert excinfo.value.code == 2
  128. assert "explicit DSH_HOME" in capsys.readouterr().err
  129. def test_python_dsh_command_executes_the_bundled_cli(
  130. monkeypatch: pytest.MonkeyPatch
  131. ) -> None:
  132. called: dict[str, object] = {}
  133. monkeypatch.setenv("DSH_HOME", "/explicit/home")
  134. monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("/runtime",))
  135. monkeypatch.setattr(runtime, "sys", SimpleNamespace(platform="linux", argv=["dsh", "plugin", "--profile", "sdk", "list"]))
  136. def execvpe(file: str, args: tuple[str, ...], env: dict[str, str]) -> None:
  137. called.update(file=file, args=args, home=env.get("DSH_HOME"))
  138. monkeypatch.setattr(runtime.os, "execvpe", execvpe)
  139. main()
  140. assert called == {
  141. "file": "/runtime",
  142. "args": ("/runtime", "plugin", "--profile", "sdk", "list"),
  143. "home": "/explicit/home",
  144. }
  145. @pytest.mark.parametrize("returncode", [0, 37, 513])
  146. def test_windows_console_waits_and_forwards_runtime_status(monkeypatch: pytest.MonkeyPatch, returncode: int) -> None:
  147. monkeypatch.setenv("DSH_HOME", "/explicit/home")
  148. monkeypatch.setattr(runtime, "sys", SimpleNamespace(platform="win32", argv=["dsh", "plugin", "argument with spaces", "中文"]))
  149. monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("runtime.exe",))
  150. called = []
  151. def run(args: tuple[str, ...], **kwargs: object) -> subprocess.CompletedProcess[str]:
  152. called.append((args, kwargs))
  153. return subprocess.CompletedProcess(args, returncode)
  154. def forbidden_exec(*args: object) -> None:
  155. pytest.fail("Windows console must wait instead of entering CRT exec")
  156. monkeypatch.setattr(subprocess, "run", run)
  157. monkeypatch.setattr(runtime.os, "execvpe", forbidden_exec)
  158. with pytest.raises(SystemExit) as result:
  159. main()
  160. assert result.value.code == returncode
  161. assert called == [(("runtime.exe", "plugin", "argument with spaces", "中文"), {"env": os.environ})]
  162. @pytest.mark.parametrize("returncode", [0, 37, pytest.param(513, marks=pytest.mark.skipif(sys.platform != "win32", reason="POSIX truncates process exit codes to eight bits"))])
  163. def test_windows_console_branch_preserves_real_child_io_and_completion(tmp_path: Path, returncode: int) -> None:
  164. child = tmp_path / "child with spaces.py"
  165. sentinel = tmp_path / "finished"
  166. child.write_text(
  167. "import pathlib,sys\n"
  168. "assert sys.argv[1] == 'argument with spaces'\n"
  169. "assert sys.argv[2] == '中文'\n"
  170. "print('stdout-中文', flush=True)\n"
  171. "print('stderr-中文', file=sys.stderr, flush=True)\n"
  172. f"pathlib.Path({str(sentinel)!r}).write_text('done')\n"
  173. f"raise SystemExit({returncode})\n", encoding="utf-8",
  174. )
  175. driver = (
  176. "import deepseek_harness_runtime as runtime; from types import SimpleNamespace; "
  177. f"runtime.sys = SimpleNamespace(platform='win32', argv=['dsh', 'argument with spaces', '中文']); "
  178. f"runtime.resolve_bundled_launch_args = lambda: ({sys.executable!r}, {str(child)!r}); runtime.main()"
  179. )
  180. result = subprocess.run([sys.executable, "-c", driver], capture_output=True, text=True, encoding="utf-8",
  181. env={**os.environ, "DSH_HOME": str(tmp_path), "PYTHONIOENCODING": "utf-8"}, timeout=15)
  182. assert result.returncode == returncode, result.stderr
  183. assert result.stdout == "stdout-中文\n"
  184. assert result.stderr == "stderr-中文\n"
  185. assert sentinel.read_text() == "done"
  186. @pytest.mark.parametrize("target,native_targets", [
  187. ("linux-x64", ()), ("linux-arm64", ()),
  188. ("macos-arm64", ("darwin-arm64",)), ("macos-x64", ("darwin-x64",)), ("win-x64", ("win32-x64",)),
  189. ("linux-x64", ("linux-x64",)), ("macos-arm64", ()),
  190. ])
  191. def test_runtime_requires_its_platform_office_engine(
  192. tmp_path: Path, monkeypatch: pytest.MonkeyPatch, target: str, native_targets: tuple[str, ...],
  193. ) -> None:
  194. extension = ".exe" if target.startswith("win-") else ""
  195. executable = tmp_path / "runtime" / f"deepseek-harness-sdk-runtime-{target}{extension}"
  196. executable.parent.mkdir()
  197. executable.touch()
  198. executable.with_name(f"{executable.stem}-rg{extension}").touch()
  199. if target.startswith("macos-"):
  200. Path(f"{executable}-spawn-helper").touch()
  201. office = _office_sidecar(executable, native_targets)
  202. monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
  203. monkeypatch.setattr(runtime, "_current_platform_tag", lambda: target)
  204. assert runtime.bundled_runtime_path() == executable
  205. engine = next(office.glob("node_modules/@deepseek-ai/libreoffice-kit-*/prebuilds.json"))
  206. engine.unlink()
  207. foreign = office / "node_modules/@deepseek-ai" / ("libreoffice-kit-darwin-arm64" if engine.parent.name == "libreoffice-kit-wasm" else "libreoffice-kit-wasm") / "prebuilds.json"
  208. foreign.parent.mkdir(parents=True, exist_ok=True)
  209. foreign.write_text("{}")
  210. with pytest.raises(FileNotFoundError, match="Office sidecar"):
  211. runtime.bundled_runtime_path()