154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _load_utils_module():
|
|
module_path = Path(__file__).resolve().parents[2] / "strix" / "interface" / "utils.py"
|
|
spec = importlib.util.spec_from_file_location("strix_interface_utils_test", module_path)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError("Failed to load strix.interface.utils for tests")
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
utils = _load_utils_module()
|
|
|
|
|
|
def test_parse_name_status_uses_rename_destination_path() -> None:
|
|
raw = (
|
|
b"R100\x00old/path.py\x00new/path.py\x00"
|
|
b"R75\x00legacy/module.py\x00modern/module.py\x00"
|
|
b"M\x00src/app.py\x00"
|
|
b"A\x00src/new_file.py\x00"
|
|
b"D\x00src/deleted.py\x00"
|
|
)
|
|
|
|
entries = utils._parse_name_status_z(raw)
|
|
classified = utils._classify_diff_entries(entries)
|
|
|
|
assert "new/path.py" in classified["analyzable_files"]
|
|
assert "old/path.py" not in classified["analyzable_files"]
|
|
assert "modern/module.py" in classified["analyzable_files"]
|
|
assert classified["renamed_files"][0]["old_path"] == "old/path.py"
|
|
assert classified["renamed_files"][0]["new_path"] == "new/path.py"
|
|
assert "src/deleted.py" in classified["deleted_files"]
|
|
assert "src/deleted.py" not in classified["analyzable_files"]
|
|
|
|
|
|
def test_build_diff_scope_instruction_includes_added_modified_and_deleted_guidance() -> None:
|
|
scope = utils.RepoDiffScope(
|
|
source_path="/tmp/repo",
|
|
workspace_subdir="repo",
|
|
base_ref="refs/remotes/origin/main",
|
|
merge_base="abc123",
|
|
added_files=["src/added.py"],
|
|
modified_files=["src/changed.py"],
|
|
renamed_files=[{"old_path": "src/old.py", "new_path": "src/new.py", "similarity": 90}],
|
|
deleted_files=["src/deleted.py"],
|
|
analyzable_files=["src/added.py", "src/changed.py", "src/new.py"],
|
|
)
|
|
|
|
instruction = utils.build_diff_scope_instruction([scope])
|
|
|
|
assert "For Added files, review the entire file content." in instruction
|
|
assert "For Modified files, focus primarily on the changed areas." in instruction
|
|
assert "Note: These files were deleted" in instruction
|
|
assert "src/deleted.py" in instruction
|
|
assert "src/old.py -> src/new.py" in instruction
|
|
|
|
|
|
def test_resolve_base_ref_prefers_github_base_ref(monkeypatch) -> None:
|
|
calls: list[str] = []
|
|
|
|
def fake_ref_exists(_repo_path: Path, ref: str) -> bool:
|
|
calls.append(ref)
|
|
return ref == "refs/remotes/origin/release-2026"
|
|
|
|
monkeypatch.setattr(utils, "_git_ref_exists", fake_ref_exists)
|
|
monkeypatch.setattr(utils, "_extract_github_base_sha", lambda _env: None)
|
|
monkeypatch.setattr(utils, "_resolve_origin_head_ref", lambda _repo_path: None)
|
|
|
|
base_ref = utils._resolve_base_ref(
|
|
Path("/tmp/repo"),
|
|
diff_base=None,
|
|
env={"GITHUB_BASE_REF": "release-2026"},
|
|
)
|
|
|
|
assert base_ref == "refs/remotes/origin/release-2026"
|
|
assert calls[0] == "refs/remotes/origin/release-2026"
|
|
|
|
|
|
def test_resolve_base_ref_falls_back_to_remote_main(monkeypatch) -> None:
|
|
calls: list[str] = []
|
|
|
|
def fake_ref_exists(_repo_path: Path, ref: str) -> bool:
|
|
calls.append(ref)
|
|
return ref == "refs/remotes/origin/main"
|
|
|
|
monkeypatch.setattr(utils, "_git_ref_exists", fake_ref_exists)
|
|
monkeypatch.setattr(utils, "_extract_github_base_sha", lambda _env: None)
|
|
monkeypatch.setattr(utils, "_resolve_origin_head_ref", lambda _repo_path: None)
|
|
|
|
base_ref = utils._resolve_base_ref(Path("/tmp/repo"), diff_base=None, env={})
|
|
|
|
assert base_ref == "refs/remotes/origin/main"
|
|
assert "refs/remotes/origin/main" in calls
|
|
assert "origin/main" not in calls
|
|
|
|
|
|
def test_resolve_diff_scope_context_auto_degrades_when_repo_scope_resolution_fails(
|
|
monkeypatch,
|
|
) -> None:
|
|
source = {"source_path": "/tmp/repo", "workspace_subdir": "repo"}
|
|
|
|
monkeypatch.setattr(utils, "_should_activate_auto_scope", lambda *_args, **_kwargs: True)
|
|
monkeypatch.setattr(utils, "_is_git_repo", lambda _repo_path: True)
|
|
monkeypatch.setattr(
|
|
utils,
|
|
"_resolve_repo_diff_scope",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(ValueError("shallow history")),
|
|
)
|
|
|
|
result = utils.resolve_diff_scope_context(
|
|
local_sources=[source],
|
|
scope_mode="auto",
|
|
diff_base=None,
|
|
non_interactive=True,
|
|
env={},
|
|
)
|
|
|
|
assert result.active is False
|
|
assert result.mode == "auto"
|
|
assert result.metadata["active"] is False
|
|
assert result.metadata["mode"] == "auto"
|
|
assert "skipped_diff_scope_sources" in result.metadata
|
|
assert result.metadata["skipped_diff_scope_sources"] == [
|
|
"/tmp/repo (diff-scope skipped: shallow history)"
|
|
]
|
|
|
|
|
|
def test_resolve_diff_scope_context_diff_mode_still_raises_on_repo_scope_resolution_failure(
|
|
monkeypatch,
|
|
) -> None:
|
|
source = {"source_path": "/tmp/repo", "workspace_subdir": "repo"}
|
|
|
|
monkeypatch.setattr(utils, "_is_git_repo", lambda _repo_path: True)
|
|
monkeypatch.setattr(
|
|
utils,
|
|
"_resolve_repo_diff_scope",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(ValueError("shallow history")),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="shallow history"):
|
|
utils.resolve_diff_scope_context(
|
|
local_sources=[source],
|
|
scope_mode="diff",
|
|
diff_base=None,
|
|
non_interactive=True,
|
|
env={},
|
|
)
|