"""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