fix: download optional Python runtime resources explicitly

This commit is contained in:
yudshj committed 2026-10-09 12:06:50 +08:00
1 parent 15c10e0e87
commit 89f35fe2e9
34 files changed
+1159 -259

No files matched your search

+3
View File
@@ -5,6 +5,9 @@
/deepseek-harness-python-sdk:
en: 43f7de064985e731
zh: 9ad59629660b1f13
/deepseek-harness-python-sdk/optional-office-operations:
en: ddf4dabea526cef1
zh: 6e31cab10d2f3ed8
/deepseek-harness-python-sdk/start-a-runtime:
en: 9a069c1a7cbb0779
zh: 7f625a802cd1e9ea
+18
View File
@@ -8,6 +8,24 @@ Python subprocess SDK for driving DeepSeek Harness over newline-delimited JSON-R
python -m pip install deepseek-harness-sdk
```
## Optional Office operations
Download the Office runtime explicitly, then call the local API without an agent, Session, model request, API key, or Harness home:
```py
from deepseek_harness_runtime import download_office
from deepseek_harness.office import convert, recalculate, render_images
download_office()
pdf = convert("report.docx", "report.pdf")
workbook = recalculate("forecast.xlsx", "calculated.xlsx")
preview = render_images("report.docx", "report-preview", pages=[1])
```
The output file or image directory must not exist. Successful outputs belong to the caller; Kit removes failed outputs. `OfficeOptions` supplies operation limits and font preferences. Results expose typed paths, missing fonts, and image metadata. `OfficeError` retains CLI failure codes; malformed results raise `SdkProtocolError`. Recalculation does not verify business logic or Excel fidelity. Ctrl+C waits for CLI cleanup; a second interrupt forces termination and may leave partial output. Windows requires an absolute `SystemRoot` and a shared console for Ctrl+C delivery.
The authoring environment is independently optional through `deepseek_harness_runtime.download_primary_runtime()`. See the [runtime reference](../sdk-runtime/README.md) for cache selection, resource activation, and download behavior. SDK startup and Office operations never acquire missing resources automatically.
## Start a runtime
The Python SDK has no separate application entrypoint. It launches the bundled `dsh` CLI with `--profile sdk`; the selected profile owns the JSON-RPC server, agent composition, credentials, persistence, tools, and shutdown behavior.
+18
View File
@@ -8,6 +8,24 @@
python -m pip install deepseek-harness-sdk
```
## 可选 Office 操作
显式下载 Office 运行时后,即可调用本地 API,无需 agent、Session、模型请求、API key 或 Harness home:
```py
from deepseek_harness_runtime import download_office
from deepseek_harness.office import convert, recalculate, render_images
download_office()
pdf = convert("report.docx", "report.pdf")
workbook = recalculate("forecast.xlsx", "calculated.xlsx")
preview = render_images("report.docx", "report-preview", pages=[1])
```
输出文件或图片目录必须不存在。调用方拥有成功输出;Kit 删除失败输出。`OfficeOptions` 提供操作限制和字体偏好。结果包含类型化路径、缺失字体和图片元数据。`OfficeError` 保留 CLI 失败代码;无效结果抛出 `SdkProtocolError`。重算不验证业务逻辑或 Excel 兼容性。Ctrl+C 会等待 CLI 清理;再次中断会强制终止,可能留下未完成输出。Windows 要求绝对 `SystemRoot` 路径,并通过共享控制台传递 Ctrl+C。
创作环境可通过 `deepseek_harness_runtime.download_primary_runtime()` 独立选择。缓存选择、资源启用与下载行为见[运行时参考](../sdk-runtime/README.zh.md)。SDK 启动和 Office 操作都不会自动获取缺失资源。
## 启动运行时
Python SDK 没有独立的应用入口。它以 `--profile sdk` 启动内置的 `dsh` CLI(命令行界面);所选 profile 负责 JSON-RPC 服务器、agent(智能体)组合、凭据、持久化、工具和关闭流程。
+220
View File
@@ -0,0 +1,220 @@
"""Local Office operations through the runtime wheel's dsoffice CLI.
Calls are synchronous and do not create agents, sessions, or model requests.
The CLI owns conversion deadlines, engine termination, and failed-output cleanup.
Console Ctrl+C waits for CLI cleanup before raising KeyboardInterrupt; a second
interrupt forces termination and can leave partial output. On Windows, callers
must share the CLI's console for Ctrl+C delivery.
"""
from __future__ import annotations
import json
import os
from collections.abc import Sequence
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Literal, TypeVar
import deepseek_harness_runtime
from deepseek_harness_runtime._resources import run_office_process
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from .errors import HarnessError, SdkProtocolError
class OfficeError(HarnessError):
"""A failed Office operation, including its CLI error code and exit status."""
def __init__(self, code: str, message: str, exit_code: int) -> None:
super().__init__(message)
self.code = code
self.exit_code = exit_code
class _OfficeModel(BaseModel):
model_config = ConfigDict(strict=True, frozen=True)
class ConversionResult(_OfficeModel):
"""Saved conversion or recalculation output and missing declared fonts."""
backend: Literal["native", "wasm"]
output_path: Path = Field(alias="outputPath")
missing_fonts: list[str] = Field(alias="missingFonts")
class ImageRectangle(_OfficeModel):
"""Source rectangle in CSS pixels at 96 DPI, before raster scaling."""
x: float
y: float
width: float
height: float
class RenderedImage(_OfficeModel):
"""One saved PNG and its source page, worksheet, or worksheet fragment."""
index: int
path: Path
width: int
height: int
rectangle: ImageRectangle
byte_length: int = Field(alias="byteLength")
page: int | None = None
sheet: str | None = None
range: str | None = None
rtl: bool | None = None
class RenderImagesResult(_OfficeModel):
"""Image manifest for one saved input; page_count includes unselected pages."""
schema_version: Literal[1] = Field(alias="schemaVersion")
backend: Literal["native", "wasm"]
raster_engine: Literal["libreoffice", "pdfium"] = Field(alias="rasterEngine")
source: Literal["saved"]
input_path: Path = Field(alias="inputPath")
source_sha256: str = Field(alias="sourceSha256")
dpi: float
page_count: int = Field(alias="pageCount")
images: list[RenderedImage]
missing_fonts: list[str] = Field(alias="missingFonts")
@dataclass(frozen=True, slots=True)
class OfficeOptions:
"""Kit limits and fonts; omitted values retain the installed kit defaults.
timeout_ms bounds the engine operation, including font work and output,
after explicit resource download. The call waits for engine exit and cleanup.
Font paths may be relative to cwd; fallback groups replace the kit defaults.
"""
timeout_ms: int | None = None
max_input_bytes: int | None = None
max_output_bytes: int | None = None
max_image_resolution: int | None = None
max_archive_entries: int | None = None
max_uncompressed_bytes: int | None = None
max_font_files: int | None = None
max_font_file_bytes: int | None = None
max_loaded_font_bytes: int | None = None
font_directories: Sequence[str | os.PathLike[str]] | None = None
initial_font_families: Sequence[str] | None = None
font_fallbacks: Sequence[Sequence[str]] | None = None
def _arguments(self) -> list[str]:
arguments: list[str] = []
for field in fields(self):
value = getattr(self, field.name)
if value is None:
continue
if field.name == "font_directories":
for path in value:
arguments.extend(("--font-directory", os.fspath(path)))
elif field.name == "initial_font_families":
for family in value:
arguments.extend(("--initial-font-family", family))
elif field.name == "font_fallbacks":
arguments.extend(("--font-fallbacks", json.dumps(value)))
else:
arguments.extend(("--" + field.name.replace("_", "-"), str(value)))
return arguments
class _CliFailure(_OfficeModel):
code: str
error: str
_Result = TypeVar("_Result", bound=_OfficeModel)
def _run(arguments: list[str], options: OfficeOptions | None, result_type: type[_Result]) -> _Result:
process = run_office_process(
[*deepseek_harness_runtime.resolve_office_launch_args(), *arguments,
*(options._arguments() if options is not None else [])],
capture_output=True,
)
stdout, stderr = process.stdout, process.stderr
if process.returncode != 0:
try:
failure = _CliFailure.model_validate_json(stderr)
except ValidationError:
raise OfficeError("failed", stderr.decode("utf-8", errors="replace").strip()
or "Office CLI exited without a diagnostic.",
process.returncode) from None
raise OfficeError(failure.code, failure.error, process.returncode)
try:
return result_type.model_validate_json(stdout)
except ValidationError as error:
raise SdkProtocolError("Office CLI returned an invalid result.") from error
def render_images(
input_path: str | os.PathLike[str],
output_dir: str | os.PathLike[str],
*,
pages: Literal["all"] | Sequence[int] | None = None,
sheet: str | None = None,
range: str | None = None,
dpi: int | None = None,
max_pages: int | None = None,
max_pixels: int | None = None,
max_dimension: int | None = None,
options: OfficeOptions | None = None,
) -> RenderImagesResult:
"""Render Office or PDF to PNG files and manifest.json in a fresh directory.
Pages are one-based document pages or slides. Worksheets use an exact
sheet name and optional A1 range instead. Paths may be relative to cwd.
The caller owns successful output; failed batches remove the new directory.
Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON.
"""
arguments = ["render", "--input", os.fspath(input_path), "--output-dir", os.fspath(output_dir)]
selections = {
"pages": pages if isinstance(pages, str) or pages is None else ",".join(map(str, pages)),
"sheet": sheet, "range": range, "dpi": dpi, "max-pages": max_pages,
"max-pixels": max_pixels, "max-dimension": max_dimension,
}
for name, value in selections.items():
if value is not None:
arguments.extend(("--" + name, str(value)))
return _run(arguments, options, RenderImagesResult)
def convert(
input_path: str | os.PathLike[str],
output_path: str | os.PathLike[str],
*,
sheet: str | None = None,
options: OfficeOptions | None = None,
) -> ConversionResult:
"""Export Office to the format named by a fresh output file's extension.
Paths may be relative to cwd. CSV export requires an exact sheet name for
multi-sheet input. Existing output is rejected; failed output is removed.
Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON.
"""
arguments = ["convert", "--input", os.fspath(input_path), "--output", os.fspath(output_path)]
if sheet is not None:
arguments.extend(("--sheet", sheet))
return _run(arguments, options, ConversionResult)
def recalculate(
input_path: str | os.PathLike[str],
output_path: str | os.PathLike[str],
*,
options: OfficeOptions | None = None,
) -> ConversionResult:
"""Refresh workbook formula caches into a distinct, fresh XLSX or ODS file.
Input accepts XLS, XLSX, or ODS. Paths may be relative to cwd. Formulas
survive, but recalculation does not verify business logic or Excel fidelity.
Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON.
"""
return _run(["recalculate", "--input", os.fspath(input_path), "--output", os.fspath(output_path)],
options, ConversionResult)
+48
View File
@@ -0,0 +1,48 @@
"""Office JSON subprocess peer; engine behavior belongs to the real kit smoke."""
import json
import sys
from pathlib import Path
receipt = Path(sys.argv[1])
arguments = sys.argv[2:]
receipt.write_text(json.dumps(arguments), encoding="utf-8")
command = arguments[0]
values = dict(zip(arguments[1::2], arguments[2::2]))
source = Path(values["--input"])
if source.name == "error.docx":
print(json.dumps({"code": "invalid-document", "error": "Document cannot be loaded."}), file=sys.stderr)
sys.exit(1)
if source.name == "crash.docx":
print("engine process unavailable", file=sys.stderr)
sys.exit(7)
if source.name == "invalid.docx":
print(json.dumps({"backend": "native", "outputPath": 3, "missingFonts": []}))
sys.exit(0)
if source.name == "invalid-bytes.docx":
sys.stdout.buffer.write(b"\xff")
sys.exit(0)
if source.name == "timeout.docx":
output = Path(values["--output"])
output.write_text("partial output", encoding="utf-8")
output.unlink()
print(json.dumps({"code": "timeout", "error": "Conversion timed out."}), file=sys.stderr)
sys.exit(1)
if command == "render":
output = Path(values["--output-dir"])
output.mkdir()
image = output / "page-1.png"
image.write_bytes(b"png fixture")
result = {
"schemaVersion": 1, "backend": "native", "rasterEngine": "libreoffice", "source": "saved",
"inputPath": str(source.resolve()), "sourceSha256": "abc123", "dpi": 144, "pageCount": 3,
"images": [{"index": 0, "page": 1, "path": str(image.resolve()), "width": 64, "height": 48,
"rectangle": {"x": 0, "y": 0, "width": 32, "height": 24}, "byteLength": 11}],
"missingFonts": ["Fixture Sans"],
}
(output / "manifest.json").write_text(json.dumps(result), encoding="utf-8")
else:
output = Path(values["--output"])
output.write_text("converted fixture", encoding="utf-8")
result = {"backend": "wasm", "outputPath": str(output.resolve()), "missingFonts": []}
print(json.dumps(result))
+101
View File
@@ -0,0 +1,101 @@
"""Exercise Office argv and JSON through an actual subprocess without a model."""
from __future__ import annotations
import json
import sys
from collections.abc import Callable
from pathlib import Path
import deepseek_harness_runtime
import pytest
from deepseek_harness.errors import SdkProtocolError
from deepseek_harness.office import ConversionResult, OfficeError, OfficeOptions, convert, recalculate, render_images
@pytest.fixture
def office_cli(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
receipt = tmp_path / "arguments.json"
script = Path(__file__).parent / "fixtures" / "office_cli.py"
monkeypatch.setattr(deepseek_harness_runtime, "resolve_office_launch_args",
lambda: (sys.executable, str(script), str(receipt)))
return receipt
@pytest.mark.parametrize("operation", [convert, recalculate])
def test_saved_output_and_literal_path_arguments(
operation: Callable[..., ConversionResult], office_cli: Path, tmp_path: Path,
) -> None:
source = tmp_path / "工作簿 $name with spaces.xlsx"
output = tmp_path / "saved workbook.xlsx"
result = operation(source, output)
assert result.output_path == output
assert output.read_text() == "converted fixture"
assert result.backend == "wasm"
assert result.missing_fonts == []
assert json.loads(office_cli.read_text()) == [operation.__name__, "--input", str(source), "--output", str(output)]
def test_render_images_reads_the_saved_manifest(office_cli: Path, tmp_path: Path) -> None:
output = tmp_path / "preview"
result = render_images(tmp_path / "deck.pptx", output, pages=[1, 3], dpi=144, max_pages=10,
max_pixels=1_000_000, max_dimension=4096)
assert result.page_count == 3
assert result.images[0].path.read_bytes() == b"png fixture"
assert result.images[0].rectangle.width == 32
assert result.images[0].byte_length == 11
assert result.missing_fonts == ["Fixture Sans"]
assert result.input_path == tmp_path / "deck.pptx"
assert json.loads(office_cli.read_text())[5:] == [
"--pages", "1,3", "--dpi", "144", "--max-pages", "10", "--max-pixels", "1000000", "--max-dimension", "4096",
]
def test_worksheet_selection_and_csv_sheet_keep_exact_names(office_cli: Path, tmp_path: Path) -> None:
sheet = "收入 $Values"
render_images("workbook.xlsx", tmp_path / "preview", sheet=sheet, range="A1:E9")
assert json.loads(office_cli.read_text())[5:] == ["--sheet", sheet, "--range", "A1:E9"]
convert("workbook.xlsx", tmp_path / "data.csv", sheet=sheet)
assert json.loads(office_cli.read_text())[5:] == ["--sheet", sheet]
def test_limits_and_fonts_reach_the_cli(office_cli: Path, tmp_path: Path) -> None:
convert("report.docx", tmp_path / "report.pdf", options=OfficeOptions(
timeout_ms=4567, max_input_bytes=1024, max_output_bytes=2048, max_image_resolution=72,
max_archive_entries=10, max_uncompressed_bytes=4096, max_font_files=20,
max_font_file_bytes=8192, max_loaded_font_bytes=16384,
font_directories=[tmp_path / "Fonts One", tmp_path / "Fonts Two"],
initial_font_families=["Fixture Sans", "中文字体"], font_fallbacks=[["Preferred", "Fallback"]],
))
assert json.loads(office_cli.read_text())[5:] == [
"--timeout-ms", "4567", "--max-input-bytes", "1024", "--max-output-bytes", "2048",
"--max-image-resolution", "72", "--max-archive-entries", "10", "--max-uncompressed-bytes", "4096",
"--max-font-files", "20", "--max-font-file-bytes", "8192", "--max-loaded-font-bytes", "16384",
"--font-directory", str(tmp_path / "Fonts One"), "--font-directory", str(tmp_path / "Fonts Two"),
"--initial-font-family", "Fixture Sans", "--initial-font-family", "中文字体",
"--font-fallbacks", '[["Preferred", "Fallback"]]',
]
@pytest.mark.parametrize(("filename", "code", "exit_code", "message"), [
("error.docx", "invalid-document", 1, "Document cannot be loaded"),
("crash.docx", "failed", 7, "engine process unavailable"),
("timeout.docx", "timeout", 1, "Conversion timed out"),
])
def test_cli_failures_preserve_diagnostics_after_exit(
office_cli: Path, tmp_path: Path, filename: str, code: str, exit_code: int, message: str,
) -> None:
output = tmp_path / "result.pdf"
with pytest.raises(OfficeError, match=message) as failure:
convert(filename, output, options=OfficeOptions(timeout_ms=100))
assert failure.value.code == code
assert failure.value.exit_code == exit_code
assert not output.exists()
assert json.loads(office_cli.read_text())[-2:] == ["--timeout-ms", "100"]
@pytest.mark.parametrize("filename", ["invalid.docx", "invalid-bytes.docx"])
def test_invalid_cli_result_fails_at_the_json_reader(office_cli: Path, tmp_path: Path, filename: str) -> None:
with pytest.raises(SdkProtocolError, match="Office CLI returned an invalid result"):
convert(filename, tmp_path / "result.pdf")
+8 -42
View File
@@ -145,7 +145,7 @@ def test_stage_runtime_copies_platform_payload(
spawn_helper.write_bytes(b"helper")
spawn_helper.chmod(0o755)
expected[spawn_helper.name] = b"helper"
office = executable.parent / build_python_release.office_sidecar_name(executable.name)
office = executable.parent / f"{executable.name.removesuffix('.exe')}-office"
office_asset = office / "node_modules" / "@deepseek-ai" / "libreoffice-kit-wasm" / "assets" / "soffice.data"
office_asset.parent.mkdir(parents=True)
office_asset.write_bytes(b"office data")
@@ -163,7 +163,7 @@ def test_stage_runtime_copies_platform_payload(
for path in runtime_dir.glob("deepseek-harness-sdk-runtime-*")
if path.is_file()
} == expected
assert (runtime_dir / office.name / office_asset.relative_to(office)).read_bytes() == b"office data"
assert not (runtime_dir / office.name).exists()
assert (runtime_dir / resources.name / resource.relative_to(resources)).read_text() == "checker"
pyproject = (destination / "pyproject.toml").read_text()
assert 'license = "MIT"' in pyproject
@@ -191,43 +191,9 @@ def test_stage_runtime_rejects_a_noncanonical_executable_name(tmp_path: Path) ->
)
@pytest.mark.parametrize("platform_tag,selected", [
("manylinux_2_28_x86_64", "wasm"), ("macosx_14_0_arm64", "darwin-arm64"),
("macosx_14_0_x86_64", "darwin-x64"), ("win_amd64", "win32-x64"),
("manylinux_2_28_x86_64", "linux-x64"), ("macosx_14_0_arm64", "wasm"),
])
@pytest.mark.parametrize("invalid", [None, "asset", "missing-engine", "foreign-engine", "helper-mode"])
def test_office_wheel_requires_only_target_engine(
tmp_path: Path, platform_tag: str, selected: str, invalid: str | None,
) -> None:
root = "runtime-office/node_modules"
wheel = tmp_path / "office.zip"
native = selected != "wasm"
if invalid == "helper-mode" and (not native or platform_tag == "win_amd64"):
pytest.skip("executable mode applies to POSIX native helpers")
engine = ({"kind": "native", "executable": "bin/helper"} if native else
{"kind": "wasm", "loader": "loader.cjs", "wasm": "engine.wasm", "data": "engine.data", "metadata": "fonts.json"})
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit/package.json", json.dumps({
"optionalDependencies": {f"@deepseek-ai/libreoffice-kit-{selected}": "0.0.1"},
}))
base = f"{root}/@deepseek-ai/libreoffice-kit-{selected}"
if invalid != "missing-engine":
archive.writestr(f"{base}/prebuilds.json", json.dumps({"engine": engine}))
for field in (("executable",) if native else ("loader", "wasm", "data", "metadata")):
if invalid == "asset" and field == ("executable" if native else "data"):
continue
asset = zipfile.ZipInfo(f"{base}/{engine[field]}")
asset.external_attr = (0o644 if invalid == "helper-mode" else 0o755) << 16
archive.writestr(asset, b"payload")
if invalid == "foreign-engine":
foreign = "wasm" if native else "darwin-arm64"
archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit-{foreign}/prebuilds.json", "{}")
with zipfile.ZipFile(wheel) as archive:
if invalid is None:
build_python_release.verify_office_payload(archive, root, platform_tag)
else:
message = {"asset": "asset is missing", "missing-engine": "dependency is missing",
"foreign-engine": "Unexpected Office engine", "helper-mode": "executable bit"}[invalid]
with pytest.raises(RuntimeError, match=message):
build_python_release.verify_office_payload(archive, root, platform_tag)
def test_wheel_verification_rejects_oversized_artifacts(tmp_path: Path) -> None:
wheel = tmp_path / "oversized.whl"
with wheel.open("wb") as output:
output.truncate(100_000_001)
with pytest.raises(RuntimeError, match="PyPI single-file limit"):
build_python_release.verify_wheel(wheel, "runtime", "1.2.3", ("win_amd64", "runtime.exe"))
+50 -7
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import json
import os
import subprocess
import shutil
import sys
import zipfile
from pathlib import Path
@@ -36,6 +37,11 @@ def _resource_sidecars(executable: Path, native_targets: tuple[str, ...] = ("dar
f"@deepseek-ai/libreoffice-kit-{target}": "0.0.1" for target in (*native_targets, "wasm")
}}), encoding="utf-8")
resources = executable.with_name(tag)
resources.mkdir(parents=True, exist_ok=True)
(resources / "downloads.json").write_text(json.dumps({
"target": tag.replace("macos-", "mac-"), "identity": "a" * 64,
"office": [{"name": "fixture"}], "pnpm": {},
}))
manifest = resources / "primary-runtime/runtime.json"
manifest.parent.mkdir(parents=True)
manifest.write_text(json.dumps({"platform": native.rsplit("-", 1)[0], "arch": tag.rsplit("-", 1)[1],
@@ -195,19 +201,20 @@ def test_runtime_requires_complete_resource_sidecars(
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
with pytest.raises(FileNotFoundError, match="Office sidecar"):
with pytest.raises(FileNotFoundError, match="downloads.json"):
runtime.bundled_runtime_path()
office = _resource_sidecars(executable)
assert runtime.bundled_runtime_path() == executable
(office / "node_modules/@deepseek-ai/libreoffice-kit-wasm/prebuilds.json").unlink()
with pytest.raises(FileNotFoundError, match="Office sidecar"):
runtime.bundled_runtime_path()
assert runtime.bundled_runtime_path() == executable
shutil.rmtree(executable.with_name("linux-x64") / "primary-runtime")
assert runtime.bundled_runtime_path() == executable
def test_node_mode_runs_the_deployed_dsh_cli(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
bin_js = tmp_path / "runtime" / "node" / "node_modules" / "@deepseek-ai" / "dsh" / "lib" / "bin.js"
bin_js = tmp_path / "runtime" / "node" / "runtime-bootstrap.mjs"
bin_js.parent.mkdir(parents=True)
bin_js.touch()
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
@@ -303,7 +310,7 @@ def test_windows_console_branch_preserves_real_child_io_and_completion(tmp_path:
("macos-arm64", ("darwin-arm64",)), ("macos-x64", ("darwin-x64",)), ("win-x64", ("win32-x64",)),
("linux-x64", ("linux-x64",)), ("macos-arm64", ()),
])
def test_runtime_requires_its_platform_office_engine(
def test_runtime_starts_independently_of_office_engine(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, target: str, native_targets: tuple[str, ...],
) -> None:
extension = ".exe" if target.startswith("win-") else ""
@@ -322,5 +329,41 @@ def test_runtime_requires_its_platform_office_engine(
foreign = office / "node_modules/@deepseek-ai" / ("libreoffice-kit-darwin-arm64" if engine.parent.name == "libreoffice-kit-wasm" else "libreoffice-kit-wasm") / "prebuilds.json"
foreign.parent.mkdir(parents=True, exist_ok=True)
foreign.write_text("{}")
with pytest.raises(FileNotFoundError, match="Office sidecar"):
runtime.bundled_runtime_path()
assert runtime.bundled_runtime_path() == executable
@pytest.mark.parametrize("function,flag", [
(runtime.download_office, "--download-office"),
(runtime.download_primary_runtime, "--download-primary-runtime"),
])
def test_explicit_download_uses_runtime_without_a_harness_home(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, function, flag: str,
) -> None:
script = tmp_path / "download.py"
destination = tmp_path / "downloaded"
script.write_text("import json,sys\n" + f"assert sys.argv[1] == {flag!r}\nprint(json.dumps({str(destination)!r}))\n")
monkeypatch.delenv("DSH_HOME", raising=False)
monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: (sys.executable, str(script)))
assert function() == destination
script.write_text("import sys\nprint('checksum mismatch', file=sys.stderr)\nsys.exit(2)\n")
with pytest.raises(RuntimeError, match="checksum mismatch"):
function()
def test_office_resolution_requires_explicit_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from deepseek_harness_runtime._resources import office_launch_args
executable = tmp_path / "deepseek-harness-sdk-runtime-linux-x64"
_resource_sidecars(executable)
cache = tmp_path / "cache"
monkeypatch.setenv("DSH_RESOURCE_CACHE", str(cache))
monkeypatch.delenv("DSH_OFFICE_SIDECAR", raising=False)
with pytest.raises(FileNotFoundError, match=r"download_office\(\)"):
office_launch_args(tmp_path / "linux-x64", "linux-x64")
office = cache / ("a" * 64) / "office"
node = office / "node/bin/node"
cli = office / "node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js"
for path in (node, cli):
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
assert office_launch_args(tmp_path / "linux-x64", "linux-x64") == (str(node), str(cli))