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.
204 lines
6.1 KiB
Python
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"
|