"""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}"