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>
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 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"
|