* 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>
132 lines
4.6 KiB
Python
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
|