rein-aharness/tests/test_intake.py
tegwick 66ccd4fa01 feat: issue-core task intake poll/claim/run (HARNESS-WP-0001-T03)
Add intake client mapping emissions to TaskSpec, CLI poll and
run --from-issue-core, keep --task-file for local dev. Completes
HARNESS-WP-0001 workplan.
2026-07-18 11:30:40 +02:00

204 lines
6.1 KiB
Python

from __future__ import annotations
import json
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from agent_harness.intake import (
EmittedIssue,
IntakeConfig,
IssueCoreClient,
infer_agent_and_event,
issue_to_taskspec,
poll_next,
resolve_target_repo,
)
from agent_harness.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="agent-harness",
)
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": "agent-harness"}
close_body = {**claim_body, "state": "closed"}
with patch("agent_harness.intake.httpx.get") as get_mock, patch(
"agent_harness.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"