opencode-config/tests/test_draw_image_tool.py
Sergey 0d16b028fa
fix(draw-image): optional badge/subtitle + self-healing docker entrypoint (#144)
* fix(draw-image): conditional slot bg + optional subtitle in buildSvg

* test(draw-image): optional badge/subtitle unit+integration+e2e

* feat(docker): self-healing entrypoint shim + .dockerignore

* test(draw-image): pytest assert --subtitle omitted when absent

* docs(handoff): optional badge/subtitle + docker entrypoint handoff+ADR

* docs(handoff): set PR number 144

* docs(project-map): fix PR refs 143 to 144

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-30 01:23:25 +03:00

141 lines
5 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
def test_omits_subtitle_flag_when_not_provided():
"""execute does not pass --subtitle when subtitle arg is absent (optional)."""
out = _run_exec({"template": "cover", "title": "Test"}, [OK_RESPONSE])
calls = out["calls"]
assert len(calls) >= 1
cli_args = calls[0]["args"]
assert "--subtitle" not in cli_args, f"unexpected --subtitle flag, got: {cli_args}"