"""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, "harness_profile_ref": "harness.agent-dev-local@1.0.0", "execution_refs": { "correlation_id": "corr-1", "goal_refs": ["goal:42@1"], }, } ) assert row.target_repo == "freedom-intelligence" assert "research-brief" in row.labels assert row.harness_profile_ref == "harness.agent-dev-local@1.0.0" assert row.execution_refs == { "correlation_id": "corr-1", "goal_refs": ["goal:42@1"], } 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())