"""Tests for .opencode/tools/project-status.ts — the project_status custom tool. Mirrors ``tests/test_spec_status_tool.py``: exercises the tool's ``execute()`` function via ``tests/_ts_loader.mjs`` (a node CommonJS sandbox that strips TS-only syntax, stubs ``@opencode-ai/plugin``, and replaces ``import.meta.dir`` with the real ``.opencode/tools`` directory). The loader is parameterized via the ``TS_FILE`` env var. These tests set ``TS_FILE=.opencode/tools/project-status.ts``. ``buildExecArgs`` was extended in ``_ts_loader.mjs`` to support the multi-arg boolean tool (``check`` + ``fast``): the raw value is split on ``|`` and each part is mapped to a boolean via ``/^true$/i``. Modes used: - ``load`` — sanity-check that the tool loads and has ``check`` + ``fast`` args. - ``exec_stub`` — call execute with a stubbed spawnSync to verify: (a) ``--check`` / ``--fast`` flags added to argv correctly, (b) stdout is trimmed on success, (c) non-zero exit without ``check`` returns an actionable error message, (d) non-zero exit WITH ``check`` returns the trimmed stdout (strict mode), (e) ``cwd`` is propagated from ``context.worktree`` (ADR-023). - ``exec_real`` — call execute against the real project-status.py (integration test, non-blocking so always exit 0 in this repo). """ import json import os import subprocess 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" / "project-status.ts" TS_FILE_REL = ".opencode/tools/project-status.ts" def _run_loader(*args: str, stdin: str | None = None) -> dict: """Invoke the loader with ``TS_FILE`` env set to project-status.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), input=stdin, 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 test_loader_can_load_tool(): """Sanity: project-status.ts loads and has the ``check`` + ``fast`` args (optional).""" if not TS_FILE.exists(): pytest.skip("project-status.ts not present") out = _run_loader("load") assert "description" in out args = out["args"] assert "check" in args, f"expected 'check' arg, got: {args}" assert "fast" in args, f"expected 'fast' arg, got: {args}" def test_execute_no_flags(): """execute() with no flags calls spawnSync WITHOUT ``--check`` / ``--fast``. rawValue="false|false" → both check and fast are false → cmdArgs = []. """ out = _run_loader("exec_stub", "false|false", "0", "project output", "") calls = out["calls"] assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}" call = calls[0] assert call["cmd"] == "python3" assert call["args"][0] == str(REPO_ROOT / ".opencode" / "scripts" / "project-status.py") assert "--check" not in call["args"], f"unexpected --check: {call['args']}" assert "--fast" not in call["args"], f"unexpected --fast: {call['args']}" def test_execute_passes_check_flag(): """execute() with ``check: true`` adds ``--check`` to spawnSync argv.""" out = _run_loader("exec_stub", "true|false", "0", "strict output", "") calls = out["calls"] assert len(calls) == 1 call = calls[0] assert "--check" in call["args"], f"expected --check in args: {call['args']}" assert "--fast" not in call["args"], f"unexpected --fast: {call['args']}" def test_execute_passes_fast_flag(): """execute() with ``fast: true`` adds ``--fast`` to spawnSync argv.""" out = _run_loader("exec_stub", "false|true", "0", "fast output", "") calls = out["calls"] assert len(calls) == 1 call = calls[0] assert "--fast" in call["args"], f"expected --fast in args: {call['args']}" assert "--check" not in call["args"], f"unexpected --check: {call['args']}" def test_execute_passes_both_flags(): """execute() with ``check: true`` + ``fast: true`` adds both flags.""" out = _run_loader("exec_stub", "true|true", "0", "both output", "") calls = out["calls"] assert len(calls) == 1 call = calls[0] assert "--check" in call["args"] assert "--fast" in call["args"] def test_execute_trims_stdout(): """execute trims leading/trailing whitespace from the script stdout.""" raw_stdout = " trimmed-output \n" out = _run_loader("exec_stub", "false|false", "0", raw_stdout, "") result = out["result"] assert result == "trimmed-output", f"expected trimmed output, got: {result!r}" def test_execute_nonzero_exit_without_check_returns_error(): """execute returns an actionable error message on non-zero exit WITHOUT --check.""" out = _run_loader("exec_stub", "false|false", "1", "", "some stderr from project-status") result = out["result"] assert "project_status failed" in result assert "exit 1" in result assert "some stderr from project-status" in result def test_execute_nonzero_exit_with_check_returns_stdout(): """execute with ``check: true`` returns stdout even on non-zero exit (strict).""" out = _run_loader("exec_stub", "true|false", "1", "FAIL report here", "ignored stderr") result = out["result"] assert result == "FAIL report here", f"strict mode should return stdout, got: {result!r}" def test_execute_uses_cwd_from_context(): """execute passes ``cwd=context.worktree`` to spawnSync (ADR-023).""" out = _run_loader("exec_stub", "false|false", "0", "ok", "") calls = out["calls"] assert len(calls) == 1 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_execute_real_project_status(): """Integration: execute() returns the real project-status.py output (non-blocking).""" if not (REPO_ROOT / ".opencode" / "scripts" / "project-status.py").exists(): pytest.skip("project-status.py not present") out = _run_loader("exec_real", "false|false") if out.get("error"): pytest.fail(f"execute raised: {out['error']}") result = out["result"] assert "Project:" in result, f"expected 'Project:' in output, got: {result[:200]!r}" assert "Итог:" in result, f"expected 'Итог:' in output, got: {result[:200]!r}"