Files
ar9av__obsidian-wiki/tests/test_code_understand_cli.py
Arnav ea1633a8d4 fix(tests): isolate CLI subprocess tests, and actually run the suite in CI (#174)
* fix(tests): isolate CLI subprocess tests, and run the suite in CI

12 tests failed on a clean checkout. Neither was a product bug — both were
test-harness isolation gaps, and CI never caught them because the suite was
never run there.

1. Wrong package under test. The 18 test files that spawn
   `python -m obsidian_wiki.cli` each build their own env from
   os.environ.copy() and never set PYTHONPATH, so sys.path[0] was the
   subprocess cwd. Any test running from a temp dir imported whatever
   obsidian_wiki happened to be pip-installed instead of the working tree —
   here a stale build predating `code-understand`, so all 8
   test_code_understand_cli tests died at argparse with "invalid choice",
   plus 4 in test_context_pack_cli. One autouse session fixture in
   conftest.py now pins PYTHONPATH for every helper at once.

2. Developer .env leaking in. test_lint's `_run` and test_context_pack's
   `run_cli` inherited pytest's cwd (the repo), so config resolution walked
   up and found a developer's own untracked .env, pointing the run at their
   real vault instead of the fixture. Both now run from the fake HOME.

Verified the fix isn't masking anything: sabotaging the config resolver makes
the relevant tests fail, and the suite passes in a venv with no obsidian_wiki
installed at all — so PYTHONPATH is genuinely doing the work.

Adds .github/workflows/tests.yml to run pytest on PRs and main. The workflows
only ran setup.sh, wheel publish, and the README-sync checker, so nothing
executed tests/ — the real reason this drifted.

Also drops the stale "RED by design / command does not exist yet" docstring
in test_code_understand_cli.py; the wiring landed in fd30720.

566 passed, 1 skipped (was 554 passed / 12 failed).

* fix(tests): make three test modules importable on Python 3.9

test_batch, test_graph_analysis and test_graphrag annotate helpers with
`int | None` / `list[str] | None` but lack `from __future__ import
annotations`, so 3.9 evaluates the union at import time and collection dies
with "unsupported operand type(s) for |". Every module under obsidian_wiki/
already has the future import; only these three tests were missing it.

Surfaced by the new pytest CI job — the 3.9 leg (the requires-python floor)
failed at collection while 3.12 passed.
2026-08-16 21:14:55 -07:00

190 lines
5.1 KiB
Python

"""Subprocess tests for the `code-understand` CLI command (issue #167).
These were written RED-first, before the subcommand existed; the wiring landed
in fd30720 and they are green now. They pin the exact CLI contract:
obsidian-wiki code-understand [--project <dir>] [--backend auto|builtin|codegraph]
[--changed <file> ...] [--since <sha>] [--max-symbols N] [--pretty]
Every invocation is a real subprocess (`python -m obsidian_wiki.cli`) via the
conftest `_run_cli` helper, so these tests lock the observable contract — exit
codes, JSON shape on stdout, and stderr messages — not any internal function.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from conftest import (
_run_cli,
make_fake_codegraph_bin,
make_project,
make_repo_two_commits,
)
# Minimal PATH so the code-understanding provider can never find a real
# codegraph on the machine running the tests. /usr/bin:/bin keeps `git` (and
# the absolute sys.executable used to launch the CLI) working while excluding
# any user/brew/.omo install of codegraph. `which codegraph` fails on the dev
# machine (vendored bin lives under ~/.omo but not on PATH), so this is belt
# and suspenders for CI machines that do have codegraph on PATH.
_NO_CODEGRAPH_PATH = "/usr/bin:/bin"
def _no_codegraph_env() -> dict[str, str]:
return {"PATH": _NO_CODEGRAPH_PATH, "CODE_UNDERSTANDING_CODEGRAPH_BIN": ""}
def _mini_project(tmp_path: Path) -> Path:
return make_project(tmp_path, {"src/foo.py": "class Foo:\n pass\n"})
def test_code_understand_builtin_json_shape(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
proc = _run_cli(
project, "code-understand", "--backend", "builtin", "--project", str(project)
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert data["backend"] == "builtin"
assert isinstance(data["focus_map"], list)
for item in data["focus_map"]:
assert "file" in item
assert "lines" in item
def test_code_understand_accepts_changed_files(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--backend",
"builtin",
"--project",
str(project),
"--changed",
"src/foo.py",
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert "src/foo.py" in data["seed_files"]
def test_code_understand_since_delta(tmp_path: Path) -> None:
project, sha_first = make_repo_two_commits(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--backend",
"builtin",
"--project",
str(project),
"--since",
sha_first,
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
# Only src/foo.py (modified) and src/bar.py (added) change after sha_first.
assert set(data["seed_files"]) == {"src/foo.py", "src/bar.py"}
def test_code_understand_explicit_codegraph_broken_returns_1(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--project",
str(project),
env_overrides={
"CODE_UNDERSTANDING_BACKEND": "codegraph",
**_no_codegraph_env(),
},
)
assert proc.returncode == 1
assert "codegraph" in proc.stderr.lower()
def test_code_understand_auto_falls_back_when_codegraph_absent(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--project",
str(project),
env_overrides={
"CODE_UNDERSTANDING_BACKEND": "auto",
**_no_codegraph_env(),
},
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert data["backend"] == "builtin"
assert data["warnings"]
def test_code_understand_codegraph_via_bin_env(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
bin_path = make_fake_codegraph_bin(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--project",
str(project),
env_overrides={
**_no_codegraph_env(),
"CODE_UNDERSTANDING_BACKEND": "codegraph",
"CODE_UNDERSTANDING_CODEGRAPH_BIN": str(bin_path),
},
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert data["backend"] == "codegraph"
def test_code_understand_bad_project_returns_1(tmp_path: Path) -> None:
proc = _run_cli(
tmp_path,
"code-understand",
"--backend",
"builtin",
"--project",
"/nonexistent/xyz",
)
assert proc.returncode == 1
assert "project" in proc.stderr.lower()
def test_code_understand_pretty_flag(tmp_path: Path) -> None:
project = _mini_project(tmp_path)
proc = _run_cli(
project,
"code-understand",
"--backend",
"builtin",
"--project",
str(project),
"--pretty",
)
assert proc.returncode == 0
with pytest.raises(json.JSONDecodeError):
json.loads(proc.stdout)
assert "backend" in proc.stdout