from __future__ import annotations import json import subprocess from pathlib import Path from unittest.mock import MagicMock, patch import pytest from rein_aharness.intake import ( EmittedIssue, IntakeConfig, IssueCoreClient, infer_agent_and_event, issue_to_taskspec, poll_next, resolve_target_repo, ) from rein_aharness.taskspec import TaskSpecError def _make_repo(tmp_path: Path, name: str = "binky-control") -> Path: repo = tmp_path / name repo.mkdir() subprocess.run(["git", "init", "-q"], cwd=repo, check=True) (repo / "README.md").write_text("repo\n") subprocess.run(["git", "add", "."], cwd=repo, check=True) subprocess.run( ["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "init"], cwd=repo, check=True, ) return repo def test_resolve_target_repo_by_slug(tmp_path: Path) -> None: repo = _make_repo(tmp_path, "binky-control") found = resolve_target_repo( "binky-control", repo_roots=(str(tmp_path),), ) assert found == repo.resolve() def test_resolve_target_repo_map(tmp_path: Path) -> None: repo = _make_repo(tmp_path, "sandbox") found = resolve_target_repo( "coulomb/other-name", repo_map={"other-name": str(repo)}, ) assert found == repo.resolve() def test_infer_agent_from_definition() -> None: issue = EmittedIssue( issue_id="1", title="x", description="y", labels=["binky", "automated"], activity_definition_id="binky-weekly-mail-intake", ) agent, event = infer_agent_and_event(issue) assert agent == "mail-triage" assert event == "binky_mail_intake" def test_infer_agent_from_labels() -> None: issue = EmittedIssue( issue_id="1", title="x", description="y", labels=["rhythm", "automated"], ) agent, event = infer_agent_and_event(issue) assert agent == "coach" assert event == "binky_daily_brief" def test_issue_to_taskspec(tmp_path: Path) -> None: repo = _make_repo(tmp_path) issue = EmittedIssue( issue_id="abc", title="Run daily", description="do the rhythm", labels=["binky", "rhythm", "automated"], target_repo="binky-control", activity_definition_id="binky-daily-rhythm", ) cfg = IntakeConfig(repo_roots=(str(tmp_path),)) spec = issue_to_taskspec(issue, cfg) assert spec.title == "Run daily" assert spec.agent == "coach" assert spec.completion_event_type == "binky_daily_brief" assert spec.target_repo == repo.resolve() def test_issue_to_taskspec_missing_repo_raises(tmp_path: Path) -> None: issue = EmittedIssue( issue_id="1", title="x", description="y", target_repo="no-such-repo", ) with pytest.raises(TaskSpecError): issue_to_taskspec(issue, IntakeConfig(repo_roots=(str(tmp_path),))) def test_client_list_claim_close() -> None: cfg = IntakeConfig( base_url="http://issue-core.test", api_key="k", require_labels=("automated",), assignee="rein-aharness", ) client = IssueCoreClient(cfg) list_body = [ { "issue_id": "id-1", "number": 3, "title": "Run Binky daily rhythm", "description": "hygiene + brief", "state": "open", "labels": ["binky", "rhythm", "automated"], "target_repo": "binky-control", "activity_definition_id": "binky-daily-rhythm", } ] claim_body = {**list_body[0], "state": "in_progress", "assignee": "rein-aharness"} close_body = {**claim_body, "state": "closed"} with patch("rein_aharness.intake.httpx.get") as get_mock, patch( "rein_aharness.intake.httpx.patch" ) as patch_mock: get_resp = MagicMock() get_resp.raise_for_status = MagicMock() get_resp.json.return_value = list_body get_mock.return_value = get_resp issues = client.list_open() assert len(issues) == 1 assert issues[0].issue_id == "id-1" get_mock.assert_called_once() params = get_mock.call_args.kwargs.get("params") or get_mock.call_args[1].get( "params" ) assert ("label", "automated") in params patch_resp = MagicMock() patch_resp.raise_for_status = MagicMock() patch_resp.json.return_value = claim_body patch_mock.return_value = patch_resp claimed = client.claim("id-1") assert claimed.state == "in_progress" patch_resp.json.return_value = close_body closed = client.close("id-1") assert closed.state == "closed" def test_poll_next_empty() -> None: client = IssueCoreClient(IntakeConfig(base_url="http://x", api_key="k")) with patch.object(client, "list_open", return_value=[]): assert poll_next(client) is None def test_poll_next_claims_and_maps(tmp_path: Path) -> None: _make_repo(tmp_path) client = IssueCoreClient( IntakeConfig( base_url="http://x", api_key="k", repo_roots=(str(tmp_path),), ) ) open_issue = EmittedIssue( issue_id="id-9", title="Mail intake", description="triage", labels=["mail-intake", "automated"], target_repo="binky-control", activity_definition_id="binky-weekly-mail-intake", number=9, ) claimed = EmittedIssue( issue_id="id-9", title="Mail intake", description="triage", labels=["mail-intake", "automated"], target_repo="binky-control", activity_definition_id="binky-weekly-mail-intake", number=9, state="in_progress", ) with patch.object(client, "list_open", return_value=[open_issue]), patch.object( client, "claim", return_value=claimed ) as claim: result = poll_next(client, claim=True) assert result is not None issue, spec = result claim.assert_called_once_with("id-9") assert issue.state == "in_progress" assert spec.agent == "mail-triage" assert spec.completion_event_type == "binky_mail_intake"