"""Tests for async StagingStore protocol and async JsonFileStagingStore. Phase 5 - Step 1: All StagingStore methods become async def. Written FIRST (TDD RED phase). """ import json from datetime import date from pathlib import Path from unittest.mock import AsyncMock import pytest from release_agent.graph.dependencies import JsonFileStagingStore, StagingStore, ToolClients from release_agent.models.release import StagingRelease from release_agent.models.ticket import TicketEntry # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_ticket(ticket_id: str = "ALLPOST-1") -> TicketEntry: return TicketEntry( id=ticket_id, summary="Fix something", pr_id="42", pr_url="https://dev.azure.com/org/proj/_git/repo/pullrequest/42", pr_title="Fix: something", branch=f"feature/{ticket_id}-fix", merged_at=date(2025, 1, 15), ) def _make_staging(repo: str = "my-repo", version: str = "v1.0.0") -> StagingRelease: return StagingRelease( version=version, repo=repo, started_at=date(2025, 1, 1), tickets=[], ) # --------------------------------------------------------------------------- # Protocol: all methods must be async # --------------------------------------------------------------------------- class TestStagingStoreProtocolIsAsync: """Verify that StagingStore protocol methods are async-compatible.""" def test_protocol_has_load_method(self) -> None: assert hasattr(StagingStore, "load") def test_protocol_has_save_method(self) -> None: assert hasattr(StagingStore, "save") def test_protocol_has_archive_method(self) -> None: assert hasattr(StagingStore, "archive") def test_protocol_has_list_versions_method(self) -> None: assert hasattr(StagingStore, "list_versions") # --------------------------------------------------------------------------- # JsonFileStagingStore async interface # --------------------------------------------------------------------------- class TestJsonFileStagingStoreAsync: """Verify that JsonFileStagingStore methods are awaitable (async def).""" async def test_load_is_awaitable(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) result = await store.load("nonexistent-repo") assert result is None async def test_load_returns_staging_release_after_save(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging() await store.save(staging) loaded = await store.load("my-repo") assert loaded is not None assert loaded.version == "v1.0.0" assert loaded.repo == "my-repo" async def test_load_returns_staging_with_tickets(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging().add_ticket(_make_ticket("BILL-10")) await store.save(staging) loaded = await store.load("my-repo") assert loaded is not None assert len(loaded.tickets) == 1 assert loaded.tickets[0].id == "BILL-10" async def test_load_returns_fresh_objects(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging() await store.save(staging) loaded1 = await store.load("my-repo") loaded2 = await store.load("my-repo") assert loaded1 is not loaded2 async def test_save_is_awaitable(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging(repo="api-service") await store.save(staging) expected_path = tmp_path / "api-service.json" assert expected_path.exists() async def test_save_overwrites_existing_file(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) await store.save(_make_staging(version="v1.0.0")) await store.save(_make_staging(version="v1.0.1")) loaded = await store.load("my-repo") assert loaded is not None assert loaded.version == "v1.0.1" async def test_save_writes_valid_json(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging() await store.save(staging) raw = (tmp_path / "my-repo.json").read_text() data = json.loads(raw) assert data["version"] == "v1.0.0" assert data["repo"] == "my-repo" async def test_archive_is_awaitable(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging() await store.save(staging) await store.archive(staging, date(2025, 6, 1)) assert await store.load("my-repo") is None async def test_archive_creates_archive_file(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging(repo="my-repo", version="v1.0.0") await store.save(staging) await store.archive(staging, date(2025, 6, 1)) archive_path = tmp_path / "my-repo_v1.0.0_2025-06-01.json" assert archive_path.exists() async def test_archive_file_contains_released_at(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging() await store.save(staging) await store.archive(staging, date(2025, 6, 1)) archive_path = tmp_path / "my-repo_v1.0.0_2025-06-01.json" data = json.loads(archive_path.read_text()) assert data["released_at"] == "2025-06-01" async def test_list_versions_is_awaitable(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) versions = await store.list_versions("my-repo") assert versions == [] async def test_list_versions_returns_staging_version(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) await store.save(_make_staging(version="v2.1.0")) versions = await store.list_versions("my-repo") assert "v2.1.0" in versions async def test_list_versions_includes_archived(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) staging = _make_staging(version="v1.5.0") await store.save(staging) await store.archive(staging, date(2025, 3, 1)) await store.save(_make_staging(version="v1.6.0")) versions = await store.list_versions("my-repo") assert "v1.5.0" in versions assert "v1.6.0" in versions async def test_list_versions_only_for_given_repo(self, tmp_path: Path) -> None: store = JsonFileStagingStore(directory=tmp_path) await store.save(_make_staging(repo="repo-a", version="v1.0.0")) await store.save(_make_staging(repo="repo-b", version="v2.0.0")) versions_a = await store.list_versions("repo-a") assert "v1.0.0" in versions_a assert "v2.0.0" not in versions_a