generated from coulomb/repo-seed
Implement discover_kaizen_scheduled_repos and discover_kaizen_projects per kaizen-agentic ADR-005 contract: State Hub roster, roster.yaml filter, schedule validation, and prepare_command emission. Register kaizen/resolver/shell source types with unit tests and runbook dry-run instructions.
195 lines
5.8 KiB
Python
195 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
import yaml
|
|
|
|
from activity_core.context_resolvers.kaizen import (
|
|
KaizenContextResolver,
|
|
discover_kaizen_scheduled_repos,
|
|
)
|
|
|
|
|
|
class DummyResponse:
|
|
def __init__(self, payload: Any, status_error: Exception | None = None) -> None:
|
|
self.payload = payload
|
|
self.status_error = status_error
|
|
|
|
def raise_for_status(self) -> None:
|
|
if self.status_error is not None:
|
|
raise self.status_error
|
|
|
|
def json(self) -> Any:
|
|
return self.payload
|
|
|
|
|
|
def _write_schedule(path: Path, agents: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(
|
|
yaml.safe_dump(
|
|
{"version": "1", "timezone": "Europe/Berlin", "agents": agents},
|
|
sort_keys=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def test_discover_scheduled_repos_emits_enabled_coach(tmp_path, monkeypatch) -> None:
|
|
repo_root = tmp_path / "pilot-repo"
|
|
repo_root.mkdir()
|
|
_write_schedule(
|
|
repo_root / ".kaizen" / "schedule.yml",
|
|
{"coach": {"cadence": "daily", "cron": "15 * * * *", "enabled": True}},
|
|
)
|
|
|
|
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
|
return DummyResponse(
|
|
[
|
|
{
|
|
"slug": "pilot-repo",
|
|
"domain_slug": "custodian",
|
|
"host_paths": {"testhost": str(repo_root)},
|
|
}
|
|
]
|
|
)
|
|
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
monkeypatch.setenv("KAIZEN_RUNNER_HOST", "testhost")
|
|
monkeypatch.setattr(httpx, "get", fake_get)
|
|
|
|
result = discover_kaizen_scheduled_repos({})
|
|
|
|
assert len(result["scheduled_runs"]) == 1
|
|
run = result["scheduled_runs"][0]
|
|
assert run["repo"] == "pilot-repo"
|
|
assert run["agent"] == "coach"
|
|
assert run["enabled"] is True
|
|
assert "schedule prepare coach" in run["prepare_command"]
|
|
|
|
|
|
def test_discover_scheduled_repos_skips_disabled_coach(tmp_path, monkeypatch) -> None:
|
|
repo_root = tmp_path / "pilot-repo"
|
|
repo_root.mkdir()
|
|
_write_schedule(
|
|
repo_root / ".kaizen" / "schedule.yml",
|
|
{"coach": {"cadence": "daily", "enabled": False}},
|
|
)
|
|
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
monkeypatch.setenv("KAIZEN_RUNNER_HOST", "testhost")
|
|
monkeypatch.setattr(
|
|
httpx,
|
|
"get",
|
|
lambda url, **kwargs: DummyResponse(
|
|
[{"slug": "pilot-repo", "host_paths": {"testhost": str(repo_root)}}]
|
|
),
|
|
)
|
|
|
|
result = discover_kaizen_scheduled_repos({})
|
|
assert result["scheduled_runs"] == []
|
|
|
|
|
|
def test_discover_scheduled_repos_skips_missing_schedule(tmp_path, monkeypatch) -> None:
|
|
repo_root = tmp_path / "no-schedule"
|
|
repo_root.mkdir()
|
|
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
monkeypatch.setenv("KAIZEN_RUNNER_HOST", "testhost")
|
|
monkeypatch.setattr(
|
|
httpx,
|
|
"get",
|
|
lambda url, **kwargs: DummyResponse(
|
|
[{"slug": "no-schedule", "host_paths": {"testhost": str(repo_root)}}]
|
|
),
|
|
)
|
|
|
|
result = discover_kaizen_scheduled_repos({})
|
|
assert result["scheduled_runs"] == []
|
|
|
|
|
|
def test_discover_scheduled_repos_skips_invalid_schedule(tmp_path, monkeypatch) -> None:
|
|
repo_root = tmp_path / "bad-schedule"
|
|
schedule = repo_root / ".kaizen" / "schedule.yml"
|
|
schedule.parent.mkdir(parents=True)
|
|
schedule.write_text("version: '2'\nagents: {}\n", encoding="utf-8")
|
|
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
monkeypatch.setenv("KAIZEN_RUNNER_HOST", "testhost")
|
|
monkeypatch.setattr(
|
|
httpx,
|
|
"get",
|
|
lambda url, **kwargs: DummyResponse(
|
|
[{"slug": "bad-schedule", "host_paths": {"testhost": str(repo_root)}}]
|
|
),
|
|
)
|
|
|
|
result = discover_kaizen_scheduled_repos({})
|
|
assert result["scheduled_runs"] == []
|
|
|
|
|
|
def test_discover_scheduled_repos_filters_by_roster_and_cadence(
|
|
tmp_path, monkeypatch
|
|
) -> None:
|
|
repo_a = tmp_path / "kaizen-agentic"
|
|
repo_b = tmp_path / "other-repo"
|
|
for root in (repo_a, repo_b):
|
|
_write_schedule(
|
|
root / ".kaizen" / "schedule.yml",
|
|
{
|
|
"coach": {"cadence": "daily", "enabled": True},
|
|
"optimization": {"cadence": "weekly", "enabled": True},
|
|
},
|
|
)
|
|
|
|
roster = tmp_path / "roster.yaml"
|
|
roster.write_text(
|
|
yaml.safe_dump(
|
|
{
|
|
"active": [
|
|
{"slug": "kaizen-agentic", "agents": ["coach"], "status": "active"}
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
monkeypatch.setenv("KAIZEN_RUNNER_HOST", "testhost")
|
|
monkeypatch.setattr(
|
|
httpx,
|
|
"get",
|
|
lambda url, **kwargs: DummyResponse(
|
|
[
|
|
{"slug": "kaizen-agentic", "host_paths": {"testhost": str(repo_a)}},
|
|
{"slug": "other-repo", "host_paths": {"testhost": str(repo_b)}},
|
|
]
|
|
),
|
|
)
|
|
|
|
result = discover_kaizen_scheduled_repos(
|
|
{"roster": str(roster), "cadence": "daily"}
|
|
)
|
|
agents = {r["agent"] for r in result["scheduled_runs"]}
|
|
repos = {r["repo"] for r in result["scheduled_runs"]}
|
|
assert repos == {"kaizen-agentic"}
|
|
assert agents == {"coach"}
|
|
|
|
|
|
def test_hub_unreachable_raises(monkeypatch) -> None:
|
|
monkeypatch.setenv("STATE_HUB_URL", "http://hub.test")
|
|
|
|
def fail_get(url: str, **kwargs: Any) -> DummyResponse:
|
|
raise httpx.ConnectError("down")
|
|
|
|
monkeypatch.setattr(httpx, "get", fail_get)
|
|
|
|
with pytest.raises(RuntimeError, match="State Hub unreachable"):
|
|
discover_kaizen_scheduled_repos({})
|
|
|
|
|
|
def test_resolver_registry_alias() -> None:
|
|
resolver = KaizenContextResolver()
|
|
assert resolver.resolve("unknown_query", None, {}) == {} |