136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
"""Tests for scripts/cycle_retro.py issue auto-detection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
# Import the module under test — it's a script so we import the helpers directly
|
|
import importlib
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent / "scripts"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _add_scripts_to_path(monkeypatch):
|
|
monkeypatch.syspath_prepend(str(SCRIPTS_DIR))
|
|
|
|
|
|
@pytest.fixture()
|
|
def mod():
|
|
"""Import cycle_retro as a module."""
|
|
return importlib.import_module("cycle_retro")
|
|
|
|
|
|
class TestDetectIssueFromBranch:
|
|
def test_kimi_issue_branch(self, mod):
|
|
with patch.object(subprocess, "check_output", return_value="kimi/issue-492\n"):
|
|
assert mod.detect_issue_from_branch() == 492
|
|
|
|
def test_plain_issue_branch(self, mod):
|
|
with patch.object(subprocess, "check_output", return_value="issue-123\n"):
|
|
assert mod.detect_issue_from_branch() == 123
|
|
|
|
def test_issue_slash_number(self, mod):
|
|
with patch.object(subprocess, "check_output", return_value="fix/issue/55\n"):
|
|
assert mod.detect_issue_from_branch() == 55
|
|
|
|
def test_no_issue_in_branch(self, mod):
|
|
with patch.object(subprocess, "check_output", return_value="main\n"):
|
|
assert mod.detect_issue_from_branch() is None
|
|
|
|
def test_feature_branch(self, mod):
|
|
with patch.object(subprocess, "check_output", return_value="feature/add-widget\n"):
|
|
assert mod.detect_issue_from_branch() is None
|
|
|
|
def test_git_not_available(self, mod):
|
|
with patch.object(subprocess, "check_output", side_effect=FileNotFoundError):
|
|
assert mod.detect_issue_from_branch() is None
|
|
|
|
def test_git_fails(self, mod):
|
|
with patch.object(
|
|
subprocess,
|
|
"check_output",
|
|
side_effect=subprocess.CalledProcessError(1, "git"),
|
|
):
|
|
assert mod.detect_issue_from_branch() is None
|
|
|
|
|
|
class TestConsumeOnce:
|
|
"""cycle_result.json must be deleted after reading."""
|
|
|
|
def test_cycle_result_deleted_after_read(self, mod, tmp_path):
|
|
"""After _load_cycle_result() data is consumed in main(), the file is deleted."""
|
|
result_file = tmp_path / "cycle_result.json"
|
|
result_file.write_text('{"issue": 42, "type": "bug"}')
|
|
|
|
with (
|
|
patch.object(mod, "CYCLE_RESULT_FILE", result_file),
|
|
patch.object(mod, "RETRO_FILE", tmp_path / "retro" / "cycles.jsonl"),
|
|
patch.object(mod, "SUMMARY_FILE", tmp_path / "retro" / "summary.json"),
|
|
patch.object(mod, "EPOCH_COUNTER_FILE", tmp_path / "retro" / ".epoch_counter"),
|
|
patch(
|
|
"sys.argv",
|
|
["cycle_retro", "--cycle", "1", "--success", "--main-green", "--duration", "60"],
|
|
),
|
|
):
|
|
mod.main()
|
|
|
|
assert not result_file.exists(), "cycle_result.json should be deleted after consumption"
|
|
|
|
def test_cycle_result_not_deleted_when_empty(self, mod, tmp_path):
|
|
"""If cycle_result.json doesn't exist, no error occurs."""
|
|
result_file = tmp_path / "nonexistent_result.json"
|
|
|
|
with (
|
|
patch.object(mod, "CYCLE_RESULT_FILE", result_file),
|
|
patch.object(mod, "RETRO_FILE", tmp_path / "retro" / "cycles.jsonl"),
|
|
patch.object(mod, "SUMMARY_FILE", tmp_path / "retro" / "summary.json"),
|
|
patch.object(mod, "EPOCH_COUNTER_FILE", tmp_path / "retro" / ".epoch_counter"),
|
|
patch(
|
|
"sys.argv",
|
|
[
|
|
"cycle_retro",
|
|
"--cycle",
|
|
"1",
|
|
"--success",
|
|
"--main-green",
|
|
"--duration",
|
|
"60",
|
|
"--issue",
|
|
"10",
|
|
],
|
|
),
|
|
):
|
|
mod.main() # Should not raise
|
|
|
|
|
|
class TestBackfillExtractIssueNumber:
|
|
"""Tests for backfill_retro.extract_issue_number PR-number filtering."""
|
|
|
|
@pytest.fixture()
|
|
def backfill(self):
|
|
return importlib.import_module("backfill_retro")
|
|
|
|
def test_body_has_issue(self, backfill):
|
|
assert backfill.extract_issue_number("fix: foo (#491)", "Fixes #490", pr_number=491) == 490
|
|
|
|
def test_title_skips_pr_number(self, backfill):
|
|
assert backfill.extract_issue_number("fix: foo (#491)", "", pr_number=491) is None
|
|
|
|
def test_title_with_issue_and_pr(self, backfill):
|
|
# [loop-cycle-538] refactor: ... (#459) (#481)
|
|
assert (
|
|
backfill.extract_issue_number(
|
|
"[loop-cycle-538] refactor: remove dead airllm (#459) (#481)",
|
|
"",
|
|
pr_number=481,
|
|
)
|
|
== 459
|
|
)
|
|
|
|
def test_no_pr_number_provided(self, backfill):
|
|
assert backfill.extract_issue_number("fix: foo (#491)", "") == 491
|