opencode-config/tests/test_draw_image_tool.py
Sergey 56b67d2d5c
feat(draw-image): SVG template renderer for on-brand covers (#134)
* feat(draw-image): add SVG template render engine

* feat(draw-image): add opencode plugin and tool tests

* test(draw-image): add vitest unit integration e2e suite

* chore(draw-image): wire CI dependabot docs and ADR

* docs(handoff): set PR number

* docs(handoff): fix PR number placeholder in frontmatter

* fix(ci): use tempfile and trailing newline in draw-image tests

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-29 23:42:50 +03:00

132 lines
4.6 KiB
Python

"""Tests for .opencode/tools/draw-image.ts — the draw-image custom tool.
Mirrors tests/test_commit_tool.py: exercises the tool's ``execute()`` function
via ``tests/_ts_loader.mjs`` using the ``exec_stub_json`` mode (multi-arg tools).
The loader is parameterized via the ``TS_FILE`` env var. These tests set
``TS_FILE=.opencode/tools/draw-image.ts``.
Modes used:
- ``load`` — sanity-check that the tool loads and declares args.
- ``exec_stub_json`` — call execute with a stubbed spawnSync to verify:
(a) success path: valid args + spawnSync exit 0 → JSON result string,
(b) failure path: spawnSync exit 1 → error string with "draw-image failed",
(c) cwd propagation: spawnSync opts contain cwd=context.worktree.
draw-image.ts makes 1 spawnSync call (node cli.ts render ...). The stub
sequencer returns responses in order per call.
"""
import json
import os
import subprocess
import tempfile
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
LOADER = REPO_ROOT / "tests" / "_ts_loader.mjs"
TS_FILE = REPO_ROOT / ".opencode" / "tools" / "draw-image.ts"
TS_FILE_REL = ".opencode/tools/draw-image.ts"
TMP_OUT = os.path.join(tempfile.gettempdir(), "out.png")
TMP_CUSTOM = os.path.join(tempfile.gettempdir(), "custom.png")
OK_RESPONSE = {
"status": 0,
"stdout": '{"path":"/tmp/out.png","hash":"abc123","status":"rendered"}',
"stderr": "",
}
FAIL_RESPONSE = {"status": 1, "stdout": "", "stderr": "render failed"}
def _run_loader(*args: str) -> dict:
"""Invoke the loader with TS_FILE env set to draw-image.ts and parse JSON stdout."""
env = {**os.environ, "TS_FILE": TS_FILE_REL}
proc = subprocess.run(
["node", str(LOADER), *args],
capture_output=True,
text=True,
check=False,
cwd=str(REPO_ROOT),
timeout=60,
env=env,
)
if proc.returncode != 0:
raise RuntimeError(
f"_ts_loader.mjs {' '.join(args)} failed (exit {proc.returncode}):\n"
f"stdout: {proc.stdout}\nstderr: {proc.stderr}"
)
return json.loads(proc.stdout)
def _run_exec(args: dict, responses: list[dict]) -> dict:
"""Helper: exec_stub_json mode with JSON args + sequential stub responses."""
return _run_loader("exec_stub_json", json.dumps(args), json.dumps(responses))
def test_loader_can_load_tool():
"""Sanity: draw-image.ts loads and declares the expected arguments."""
if not TS_FILE.exists():
pytest.skip("draw-image.ts not present")
out = _run_loader("load")
assert "description" in out
assert "template" in out["args"]
assert "title" in out["args"]
assert "subtitle" in out["args"]
assert "slots" in out["args"]
assert "out" in out["args"]
def test_valid_render():
"""execute() with valid args + spawnSync exit 0 returns JSON result string.
draw-image.ts makes 1 spawnSync call: node cli.ts render ...
"""
out = _run_exec({"template": "cover", "title": "Test"}, [OK_RESPONSE])
result = out["result"]
assert "rendered" in result, f"expected rendered status, got: {result!r}"
assert TMP_OUT in result, f"expected path in result, got: {result!r}"
def test_render_failure():
"""execute() with spawnSync exit 1 returns error string."""
out = _run_exec({"template": "cover", "title": "Test"}, [FAIL_RESPONSE])
result = out["result"]
assert "draw-image failed" in result, f"expected failure error, got: {result!r}"
def test_execute_uses_cwd_from_context():
"""execute passes cwd=context.worktree to spawnSync (ADR-023 pattern)."""
out = _run_exec({"template": "cover", "title": "Test"}, [OK_RESPONSE])
calls = out["calls"]
assert len(calls) >= 1, f"expected >=1 spawnSync call, got {len(calls)}"
opts = calls[0]["opts"]
assert opts is not None, "spawnSync called without opts — expected cwd kwarg"
assert "cwd" in opts, f"opts missing 'cwd' key — got: {opts}"
assert opts["cwd"] == str(REPO_ROOT), (
f"cwd must equal context.worktree ({REPO_ROOT}), got: {opts['cwd']!r}"
)
def test_passes_all_args_to_cli():
"""execute builds CLI args from all provided fields."""
args = {
"template": "cover",
"title": "My Title",
"subtitle": "Sub",
"slots": "icon=mic",
"out": TMP_CUSTOM,
}
out = _run_exec(args, [OK_RESPONSE])
calls = out["calls"]
assert len(calls) >= 1
cli_args = calls[0]["args"]
cli_args_str = " ".join(cli_args)
assert "render" in cli_args_str
assert "cover" in cli_args_str
assert "My Title" in cli_args_str
assert "Sub" in cli_args_str
assert "icon=mic" in cli_args_str
assert TMP_CUSTOM in cli_args_str