rein-aharness/tests/test_intake.py
tegwick f6930ad115 Rename package, CLI, and deploy artifacts to rein-aharness (HARNESS-WP-0002-T02)
agent_harness -> rein_aharness (package + all imports), CLI command
agent-harness -> rein-aharness, Docker image tag, k8s namespace/labels/
names, Makefile targets, deploy script env var/paths. In-repo identity
strings (hub event source, metrics harness field, default assignee,
argparse prog name, commit author identity) updated to match.

Historical documents left untouched on purpose: docs/adr/ADR-001-agent-harness-architecture.md,
docs/architecture.md (dated v0.1 snapshot), workplans/HARNESS-WP-0001
(completed under the old name), and the SSH host alias
"forgejo-agent-harness" (external ~/.ssh/config entry, not owned here).

Verified: 47/47 tests pass, CLI runs correctly from a fresh venv,
`make image` builds and the resulting container runs correctly.

deploy/README.md gained an explicit rename cutover checklist for what
this session cannot safely do unattended -- moving the host-side
secrets dir and checkout on railiance01, and not deleting the old k8s
namespace until the new one is confirmed working. The actual live
cutover (running that checklist against the real Railiance deployment)
is not attempted here -- real production surgery on binky-control's
live automation, needs the operator present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:22:18 +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 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"