Implement REIN-A-0002 ops_run claim loop and approach registry.
Add activity-core ops_run client, approach selection (FI/Binky/mail/agent), claim-loop worker with lease heartbeat, CLI run --from-ops-run and claim-loop, install units, and docs demoting issue-core to legacy external tickets. T05 timer cutover remains operator after five clean cycles.
This commit is contained in:
parent
9644202eb2
commit
8200a672ea
15 changed files with 1807 additions and 73 deletions
168
tests/test_ops_run_client.py
Normal file
168
tests/test_ops_run_client.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"""Tests for activity-core ops_run client (REIN-A-0002-T01)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from rein_aharness.ops_run_client import (
|
||||
ActivityCoreOpsClient,
|
||||
OpsRun,
|
||||
OpsRunConfig,
|
||||
OpsRunError,
|
||||
ops_run_to_taskspec,
|
||||
)
|
||||
from rein_aharness.taskspec import TaskSpecError
|
||||
|
||||
|
||||
def test_ops_run_from_api() -> None:
|
||||
row = OpsRun.from_api(
|
||||
{
|
||||
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"activity_definition_id": "11111111-2222-3333-4444-555555555555",
|
||||
"idempotency_key": "k",
|
||||
"target_repo": "freedom-intelligence",
|
||||
"title": "FI daily",
|
||||
"description": "d",
|
||||
"labels": ["automated", "research-brief"],
|
||||
"state": "open",
|
||||
"attempt": 0,
|
||||
}
|
||||
)
|
||||
assert row.target_repo == "freedom-intelligence"
|
||||
assert "research-brief" in row.labels
|
||||
|
||||
|
||||
def test_config_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("ACTIVITY_CORE_URL", "http://actcore:8010/")
|
||||
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "tok")
|
||||
monkeypatch.setenv("AGENT_HARNESS_WORKER_ID", "w@host")
|
||||
monkeypatch.setenv("AGENT_HARNESS_OPS_LABELS", "automated,research-brief")
|
||||
monkeypatch.setenv(
|
||||
"AGENT_HARNESS_REPO_MAP",
|
||||
json.dumps({"freedom-intelligence": "/tmp/fi"}),
|
||||
)
|
||||
cfg = OpsRunConfig.from_env()
|
||||
assert cfg.base_url == "http://actcore:8010"
|
||||
assert cfg.worker_token == "tok"
|
||||
assert cfg.worker_id == "w@host"
|
||||
assert "research-brief" in cfg.claim_labels
|
||||
assert cfg.repo_map["freedom-intelligence"] == "/tmp/fi"
|
||||
|
||||
|
||||
def test_claim_posts_body() -> None:
|
||||
cfg = OpsRunConfig(
|
||||
base_url="http://example.test",
|
||||
worker_token="secret",
|
||||
worker_id="worker-1",
|
||||
claim_labels=("automated",),
|
||||
lease_seconds=120,
|
||||
)
|
||||
client = ActivityCoreOpsClient(cfg)
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = {
|
||||
"items": [
|
||||
{
|
||||
"id": "r1",
|
||||
"activity_definition_id": "d1",
|
||||
"idempotency_key": "k",
|
||||
"target_repo": "freedom-intelligence",
|
||||
"title": "t",
|
||||
"description": "",
|
||||
"labels": ["automated", "research-brief"],
|
||||
"state": "claimed",
|
||||
"claim_owner": "worker-1",
|
||||
"attempt": 1,
|
||||
}
|
||||
]
|
||||
}
|
||||
with patch("rein_aharness.ops_run_client.httpx.post", return_value=mock_resp) as post:
|
||||
claimed = client.claim(limit=1)
|
||||
assert len(claimed) == 1
|
||||
assert claimed[0].id == "r1"
|
||||
assert claimed[0].state == "claimed"
|
||||
kwargs = post.call_args.kwargs
|
||||
assert kwargs["json"]["worker_id"] == "worker-1"
|
||||
assert kwargs["json"]["labels"] == ["automated"]
|
||||
assert kwargs["headers"]["X-Worker-Token"] == "secret"
|
||||
|
||||
|
||||
def test_complete_and_fail() -> None:
|
||||
cfg = OpsRunConfig(base_url="http://example.test", worker_id="w")
|
||||
client = ActivityCoreOpsClient(cfg)
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = {
|
||||
"id": "r1",
|
||||
"activity_definition_id": "d",
|
||||
"idempotency_key": "k",
|
||||
"title": "t",
|
||||
"description": "",
|
||||
"state": "succeeded",
|
||||
"labels": [],
|
||||
}
|
||||
with patch("rein_aharness.ops_run_client.httpx.post", return_value=mock_resp):
|
||||
out = client.complete("r1", result={"path": "x"})
|
||||
assert out.state == "succeeded"
|
||||
|
||||
mock_resp.json.return_value = {**mock_resp.json.return_value, "state": "open"}
|
||||
with patch("rein_aharness.ops_run_client.httpx.post", return_value=mock_resp) as post:
|
||||
out = client.fail("r1", error="timeout", reopen=True)
|
||||
assert post.call_args.kwargs["json"]["reopen"] is True
|
||||
|
||||
|
||||
def test_claim_http_error() -> None:
|
||||
cfg = OpsRunConfig(base_url="http://example.test", worker_id="w")
|
||||
client = ActivityCoreOpsClient(cfg)
|
||||
with patch(
|
||||
"rein_aharness.ops_run_client.httpx.post",
|
||||
side_effect=httpx.ConnectError("down"),
|
||||
):
|
||||
with pytest.raises(OpsRunError, match="claim failed"):
|
||||
client.claim()
|
||||
|
||||
|
||||
def test_ops_run_to_taskspec(tmp_path: Path) -> None:
|
||||
import subprocess
|
||||
|
||||
repo = tmp_path / "freedom-intelligence"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
(repo / "README.md").write_text("x\n")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "i"],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
run = OpsRun(
|
||||
id="r1",
|
||||
activity_definition_id="d",
|
||||
idempotency_key="k",
|
||||
target_repo="freedom-intelligence",
|
||||
title="FI",
|
||||
description="do",
|
||||
labels=["automated"],
|
||||
)
|
||||
cfg = OpsRunConfig(repo_roots=(str(tmp_path),))
|
||||
spec = ops_run_to_taskspec(run, cfg)
|
||||
assert spec.target_repo == repo.resolve()
|
||||
assert spec.hub_task_id == "r1"
|
||||
|
||||
|
||||
def test_ops_run_to_taskspec_missing_repo() -> None:
|
||||
run = OpsRun(
|
||||
id="r1",
|
||||
activity_definition_id="d",
|
||||
idempotency_key="k",
|
||||
target_repo=None,
|
||||
title="t",
|
||||
description="",
|
||||
)
|
||||
with pytest.raises(TaskSpecError):
|
||||
ops_run_to_taskspec(run, OpsRunConfig())
|
||||
Loading…
Add table
Add a link
Reference in a new issue