chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:42:18 +08:00
commit 05f60106aa
288 changed files with 76871 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
"""Tests for CLI helpers and MCP serve command wiring."""
import json
import logging
import sys
from importlib.metadata import PackageNotFoundError
from unittest.mock import MagicMock, patch
from code_review_graph import cli
def test_get_version_falls_back_to_package_attr_when_metadata_missing(
monkeypatch, caplog,
):
"""When importlib.metadata can't find the dist, fall back to __version__.
This matters on filesystems where iCloud / OneDrive leave orphan
dist-info dirs that confuse the metadata lookup. Before v2.3.5 the
fallback returned the literal string "dev", which produced confusing
output for installed users whose lookup happened to fail.
"""
def _raise_package_not_found(_dist_name: str) -> str:
raise PackageNotFoundError("code-review-graph")
monkeypatch.setattr(cli, "pkg_version", _raise_package_not_found)
with caplog.at_level(logging.DEBUG, logger="code_review_graph.cli"):
version = cli._get_version()
# Falls back to the package's __version__, not "dev"
from code_review_graph import __version__ as expected
assert version == expected
assert "Package metadata unavailable" in caplog.text
def test_get_version_returns_dev_when_both_sources_fail(monkeypatch, caplog):
"""The literal "dev" fallback still fires when __version__ also fails."""
def _raise_package_not_found(_dist_name: str) -> str:
raise PackageNotFoundError("code-review-graph")
monkeypatch.setattr(cli, "pkg_version", _raise_package_not_found)
import code_review_graph
monkeypatch.delattr(code_review_graph, "__version__", raising=False)
with caplog.at_level(logging.DEBUG, logger="code_review_graph.cli"):
version = cli._get_version()
assert version == "dev"
class TestServeCommand:
def test_serve_passes_auto_watch_flag(self):
argv = [
"code-review-graph",
"serve",
"--repo",
"repo-root",
"--auto-watch",
]
with patch.object(sys, "argv", argv):
with patch("code_review_graph.main.main") as mock_serve:
cli.main()
mock_serve.assert_called_once_with(
repo_root="repo-root",
auto_watch=True,
tools=None,
)
def test_mcp_alias_maps_to_serve(self):
argv = [
"code-review-graph",
"mcp",
"--repo",
"repo-root",
]
with patch.object(sys, "argv", argv):
with patch("code_review_graph.main.main") as mock_serve:
cli.main()
mock_serve.assert_called_once_with(
repo_root="repo-root",
auto_watch=False,
)
class TestWatchInteraction:
def test_watch_exits_when_lock_is_held(self):
argv = ["code-review-graph", "watch", "--repo", "repo-root"]
with patch.object(sys, "argv", argv):
with patch("code_review_graph.graph.GraphStore") as mock_store:
mock_store.return_value = MagicMock()
with patch("code_review_graph.incremental.get_db_path") as mock_db:
mock_db.return_value = MagicMock()
with patch("code_review_graph.incremental.watch") as mock_watch:
mock_watch.side_effect = RuntimeError("watcher already running")
try:
cli.main()
assert False, "Expected SystemExit"
except SystemExit as exc:
assert exc.code == 1
class TestBuildUpdateCommands:
def test_build_skip_postprocess_does_not_run_extra_cli_postprocess(self):
argv = [
"code-review-graph",
"build",
"--skip-postprocess",
"--repo",
"repo-root",
]
result = {
"files_parsed": 1,
"total_nodes": 2,
"total_edges": 1,
"postprocess_level": "none",
}
with patch.object(sys, "argv", argv):
with patch("code_review_graph.graph.GraphStore") as mock_store:
mock_store.return_value = MagicMock()
with patch("code_review_graph.incremental.get_db_path") as mock_db:
mock_db.return_value = MagicMock()
with patch(
"code_review_graph.tools.build.build_or_update_graph",
return_value=result,
) as mock_build:
with patch(
"code_review_graph.postprocessing.run_post_processing",
) as mock_postprocess:
cli.main()
mock_build.assert_called_once_with(
full_rebuild=True,
repo_root="repo-root",
postprocess="none",
)
mock_postprocess.assert_not_called()
def test_update_skip_flows_does_not_run_extra_cli_postprocess(self):
argv = [
"code-review-graph",
"update",
"--skip-flows",
"--repo",
"repo-root",
]
result = {
"files_updated": 1,
"total_nodes": 2,
"total_edges": 1,
"postprocess_level": "minimal",
}
with patch.object(sys, "argv", argv):
with patch("code_review_graph.graph.GraphStore") as mock_store:
mock_store.return_value = MagicMock()
with patch("code_review_graph.incremental.get_db_path") as mock_db:
mock_db.return_value = MagicMock()
with patch(
"code_review_graph.tools.build.build_or_update_graph",
return_value=result,
) as mock_build:
with patch(
"code_review_graph.postprocessing.run_post_processing",
) as mock_postprocess:
cli.main()
mock_build.assert_called_once_with(
full_rebuild=False,
repo_root="repo-root",
base="HEAD~1",
postprocess="minimal",
)
mock_postprocess.assert_not_called()
class TestDetectChangesCommand:
def test_brief_output_includes_token_savings_panel(self, tmp_path, capsys):
"""v2.3.5: --brief output renders a boxed Token Savings panel.
Replaces the v2.3.4 one-line `Estimated context saved: …` format.
The panel must include the title, the saved-tokens line, the
percent suffix, and box borders.
"""
repo = tmp_path / "repo"
repo.mkdir()
(repo / ".git").mkdir()
(repo / "app.py").write_text("x" * 2000, encoding="utf-8")
argv = [
"code-review-graph",
"detect-changes",
"--repo",
str(repo),
"--brief",
]
with patch.object(sys, "argv", argv):
with patch("code_review_graph.graph.GraphStore") as mock_store:
mock_store.return_value = MagicMock()
with patch("code_review_graph.incremental.get_db_path") as mock_db:
mock_db.return_value = MagicMock()
with patch(
"code_review_graph.incremental.get_changed_files",
return_value=["app.py"],
):
with patch(
"code_review_graph.changes.analyze_changes",
return_value={"summary": "summary only"},
):
cli.main()
output = capsys.readouterr().out
assert "summary only" in output
# Panel structure: title, the three core rows, and box borders.
assert "Token Savings" in output
assert "Full context would be:" in output
assert "Graph context used:" in output
assert "Saved:" in output
# Box drawing characters from format_context_savings_panel
assert "" in output and "" in output
def test_json_output_includes_compact_savings_metadata(self, tmp_path, capsys):
repo = tmp_path / "repo"
repo.mkdir()
(repo / ".git").mkdir()
(repo / "app.py").write_text("x" * 2000, encoding="utf-8")
argv = [
"code-review-graph",
"detect-changes",
"--repo",
str(repo),
]
with patch.object(sys, "argv", argv):
with patch("code_review_graph.graph.GraphStore") as mock_store:
mock_store.return_value = MagicMock()
with patch("code_review_graph.incremental.get_db_path") as mock_db:
mock_db.return_value = MagicMock()
with patch(
"code_review_graph.incremental.get_changed_files",
return_value=["app.py"],
):
with patch(
"code_review_graph.changes.analyze_changes",
return_value={"summary": "json summary"},
):
cli.main()
result = json.loads(capsys.readouterr().out)
assert set(result["context_savings"]) == {
"estimated",
"saved_tokens",
"saved_percent",
}
class TestDetectChangesEndToEnd:
"""Regression test for #528: CLI detect-changes mapped 0 functions.
The graph stores absolute native paths, but the CLI path let
analyze_changes parse the diff internally, producing forward-slash
relative keys that never matched on Windows. This exercises the full
pipeline on a real tmp git repo with a committed change.
"""
@staticmethod
def _git(repo, *args):
import subprocess
subprocess.run(
["git", "-C", str(repo), "-c", "user.email=t@example.com",
"-c", "user.name=Test", "-c", "commit.gpgsign=false", *args],
check=True,
capture_output=True,
stdin=subprocess.DEVNULL,
timeout=30,
)
def test_detect_changes_maps_committed_change_to_functions(
self, tmp_path, capsys, monkeypatch,
):
monkeypatch.delenv("CRG_DATA_DIR", raising=False)
monkeypatch.delenv("CRG_REPO_ROOT", raising=False)
repo = tmp_path / "repo"
src = repo / "src"
src.mkdir(parents=True)
app = src / "app.py"
app.write_text(
"def greet(name):\n"
" message = 'hello ' + name\n"
" return message\n"
"\n"
"def farewell(name):\n"
" return 'bye ' + name\n",
encoding="utf-8",
)
self._git(repo, "init", "-q")
self._git(repo, "add", ".")
self._git(repo, "commit", "-q", "-m", "initial")
# Commit a change inside greet() so HEAD~1..HEAD touches lines 1-3.
app.write_text(
"def greet(name):\n"
" message = 'hi there ' + name\n"
" return message.upper()\n"
"\n"
"def farewell(name):\n"
" return 'bye ' + name\n",
encoding="utf-8",
)
self._git(repo, "add", ".")
self._git(repo, "commit", "-q", "-m", "change greet")
# Build the graph after the change (stores absolute native paths).
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import full_build, get_db_path
store = GraphStore(get_db_path(repo))
try:
full_build(repo, store)
finally:
store.close()
argv = ["code-review-graph", "detect-changes", "--repo", str(repo)]
with patch.object(sys, "argv", argv):
cli.main()
out = capsys.readouterr().out
result = json.loads(out[out.index("{"):])
# The diff must map to >0 functions — not silently come up empty.
names = {f["name"] for f in result["changed_functions"]}
assert "greet" in names
# The token-savings metadata must not be the misleading
# 100%-on-empty case (tiny response because nothing was mapped).
savings = result["context_savings"]
assert result["changed_functions"]
assert savings["saved_percent"] < 100