Prepare State Hub retirement baseline
This commit is contained in:
parent
2217bdd9f5
commit
5927591be8
46 changed files with 32583 additions and 62 deletions
40
tests/test_codex_mcp_launcher.py
Normal file
40
tests/test_codex_mcp_launcher.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LAUNCHER = ROOT / "scripts" / "codex-state-hub-mcp.sh"
|
||||
|
||||
|
||||
def test_codex_mcp_launcher_uses_configured_python(tmp_path: Path) -> None:
|
||||
fake_python = tmp_path / "python"
|
||||
fake_python.write_text(
|
||||
"#!/usr/bin/env bash\n"
|
||||
"printf 'python=%s\\nscript=%s\\ntransport=%s\\n' "
|
||||
'"$0" "$1" "$MCP_TRANSPORT"\n'
|
||||
)
|
||||
fake_python.chmod(0o755)
|
||||
env = os.environ.copy()
|
||||
env["STATE_HUB_PYTHON"] = str(fake_python)
|
||||
env["API_BASE"] = "http://127.0.0.1:8000"
|
||||
|
||||
result = subprocess.run(
|
||||
[str(LAUNCHER)],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
assert f"python={fake_python}" in result.stdout
|
||||
assert "script=mcp_server/codex_server.py" in result.stdout
|
||||
assert "transport=stdio" in result.stdout
|
||||
|
||||
|
||||
def test_codex_mcp_launcher_has_no_uv_runtime_dependency() -> None:
|
||||
launcher = LAUNCHER.read_text()
|
||||
assert "uv run" not in launcher
|
||||
assert ".venv/bin/python" in launcher
|
||||
19
tests/test_codex_mcp_server.py
Normal file
19
tests/test_codex_mcp_server.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import asyncio
|
||||
|
||||
from mcp_server import codex_server
|
||||
|
||||
|
||||
def test_codex_server_has_only_repository_coordination_tools() -> None:
|
||||
tools = asyncio.run(codex_server.mcp.list_tools())
|
||||
assert {tool.name for tool in tools} == {
|
||||
"get_domain_summary",
|
||||
"get_messages",
|
||||
"mark_message_read",
|
||||
"add_progress_event",
|
||||
"record_decision",
|
||||
"update_task_status",
|
||||
}
|
||||
|
||||
|
||||
def test_codex_server_uses_distinct_server_identity() -> None:
|
||||
assert codex_server.mcp.name == "dev-hub-codex"
|
||||
92
tests/test_configure_codex.py
Normal file
92
tests/test_configure_codex.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import os
|
||||
import subprocess
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "configure-codex.sh"
|
||||
|
||||
|
||||
def run_configure(codex_home: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["CODEX_HOME"] = str(codex_home)
|
||||
return subprocess.run(
|
||||
[str(SCRIPT), *args],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def test_configure_codex_creates_network_setting(tmp_path: Path) -> None:
|
||||
codex_home = tmp_path / ".codex"
|
||||
|
||||
run_configure(codex_home, "--skip-verify", "--skip-mcp")
|
||||
|
||||
config = tomllib.loads((codex_home / "config.toml").read_text())
|
||||
assert config["sandbox_workspace_write"]["network_access"] is True
|
||||
|
||||
|
||||
def test_configure_codex_preserves_existing_settings_and_is_idempotent(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
codex_home = tmp_path / ".codex"
|
||||
codex_home.mkdir()
|
||||
config_path = codex_home / "config.toml"
|
||||
config_path.write_text(
|
||||
'model = "gpt-test"\n\n'
|
||||
"[sandbox_workspace_write]\n"
|
||||
"network_access = false\n"
|
||||
'writable_roots = ["/tmp/example"]\n'
|
||||
)
|
||||
|
||||
run_configure(codex_home, "--skip-verify", "--skip-mcp")
|
||||
first = config_path.read_text()
|
||||
run_configure(codex_home, "--skip-verify", "--skip-mcp")
|
||||
|
||||
assert config_path.read_text() == first
|
||||
config = tomllib.loads(first)
|
||||
assert config["model"] == "gpt-test"
|
||||
assert config["sandbox_workspace_write"] == {
|
||||
"network_access": True,
|
||||
"writable_roots": ["/tmp/example"],
|
||||
}
|
||||
|
||||
|
||||
def test_configure_codex_dry_run_does_not_write(tmp_path: Path) -> None:
|
||||
codex_home = tmp_path / ".codex"
|
||||
|
||||
result = run_configure(codex_home, "--dry-run", "--skip-mcp")
|
||||
|
||||
assert "DRY-RUN" in result.stdout
|
||||
assert not (codex_home / "config.toml").exists()
|
||||
|
||||
|
||||
def test_configure_codex_removes_dev_hub_registration_by_default(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
codex_home = tmp_path / ".codex"
|
||||
codex_home.mkdir()
|
||||
config_path = codex_home / "config.toml"
|
||||
config_path.write_text(
|
||||
'[mcp_servers.dev-hub]\ncommand = "/tmp/dev-hub"\n'
|
||||
)
|
||||
|
||||
result = run_configure(codex_home, "--skip-verify")
|
||||
|
||||
assert "removed Codex MCP server dev-hub" in result.stdout
|
||||
config = tomllib.loads(config_path.read_text())
|
||||
assert "dev-hub" not in config.get("mcp_servers", {})
|
||||
|
||||
|
||||
def test_configure_codex_mcp_is_explicit_opt_in(tmp_path: Path) -> None:
|
||||
codex_home = tmp_path / ".codex"
|
||||
|
||||
run_configure(codex_home, "--skip-verify", "--with-mcp")
|
||||
|
||||
config = tomllib.loads((codex_home / "config.toml").read_text())
|
||||
command = config["mcp_servers"]["dev-hub"]["command"]
|
||||
assert command.endswith("/scripts/codex-state-hub-mcp.sh")
|
||||
|
|
@ -48,17 +48,30 @@ def _fake_get(path: str, params: dict | None = None):
|
|||
if path == "/repos":
|
||||
return [{"id": "repo-1", "slug": "demo-repo", "domain_slug": "infotech"}]
|
||||
if path == "/repo-goals":
|
||||
return [{"id": "goal-1", "title": "Ship it", "description": "d", "priority": "high"}]
|
||||
return [{
|
||||
"id": "goal-1",
|
||||
"repo_id": "repo-1",
|
||||
"repo_slug": "demo-repo",
|
||||
"title": "Ship it",
|
||||
"description": "d",
|
||||
"priority": "high",
|
||||
}]
|
||||
if path == "/capability-catalog/":
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def test_get_domain_summary_goal_guidance_is_workplan_first(monkeypatch) -> None:
|
||||
monkeypatch.setattr(server, "_get", lambda path, params=None: _fake_get(path, params))
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
server,
|
||||
"_get",
|
||||
lambda path, params=None: calls.append((path, params)) or _fake_get(path, params),
|
||||
)
|
||||
payload = json.loads(server.get_domain_summary("infotech"))
|
||||
assert "workplans" in payload
|
||||
assert payload["workstreams"] == payload["workplans"]
|
||||
action = payload["goal_guidance"]["needs_workplan"][0]["action"]
|
||||
assert "workplan" in action.lower()
|
||||
assert "workstream is linked" not in action.lower()
|
||||
assert "workstream is linked" not in action.lower()
|
||||
assert [path for path, _ in calls].count("/repo-goals") == 1
|
||||
|
|
|
|||
119
tests/test_ops_run_projection.py
Normal file
119
tests/test_ops_run_projection.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from api.config import settings
|
||||
from api.services import ops_run_projection as projection
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_projection_cache() -> None:
|
||||
projection.reset_ops_run_projection_cache()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ops_run_projection_counts_and_contract(monkeypatch) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"counts": {"open": 2, "claimed": 1, "succeeded": 9, "failed": 2},
|
||||
"items": [
|
||||
{
|
||||
"id": "run-open",
|
||||
"activity_definition_id": "definition-1",
|
||||
"target_repo": "state-hub",
|
||||
"state": "open",
|
||||
"created_at": (now - timedelta(hours=2)).isoformat(),
|
||||
"updated_at": (now - timedelta(hours=2)).isoformat(),
|
||||
"result": {},
|
||||
},
|
||||
{
|
||||
"id": "run-claimed",
|
||||
"activity_definition_id": "definition-2",
|
||||
"target_repo": "activity-core",
|
||||
"state": "claimed",
|
||||
"claim_owner": "rein-aharness@railiance01",
|
||||
"lease_until": (now + timedelta(minutes=10)).isoformat(),
|
||||
"attempt": 1,
|
||||
"created_at": (now - timedelta(minutes=10)).isoformat(),
|
||||
"updated_at": now.isoformat(),
|
||||
"result": {},
|
||||
},
|
||||
{
|
||||
"id": "run-failed",
|
||||
"activity_definition_id": "definition-3",
|
||||
"target_repo": "binky-control",
|
||||
"state": "failed",
|
||||
"created_at": (now - timedelta(hours=3)).isoformat(),
|
||||
"updated_at": (now - timedelta(hours=1)).isoformat(),
|
||||
"result": {"error": "executor timeout"},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
async def fake_fetch():
|
||||
return payload
|
||||
|
||||
monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
|
||||
result = await projection.get_ops_run_projection(refresh=True)
|
||||
|
||||
assert result.available is True
|
||||
assert result.open == 2
|
||||
assert result.claimed == 1
|
||||
assert result.failed_24h == 1
|
||||
assert result.stuck_open_or_claimed == 1
|
||||
assert {item.id for item in result.items} == {"run-open", "run-claimed", "run-failed"}
|
||||
failed = next(item for item in result.items if item.id == "run-failed")
|
||||
assert failed.last_error == "executor timeout"
|
||||
claimed = next(item for item in result.items if item.id == "run-claimed")
|
||||
assert claimed.lease["owner"] == "rein-aharness@railiance01"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ops_run_projection_serves_stale_cache_on_refresh_failure(monkeypatch) -> None:
|
||||
async def initial_fetch():
|
||||
return {"counts": {"open": 1}, "items": []}
|
||||
|
||||
monkeypatch.setattr(projection, "_fetch_ops_runs", initial_fetch)
|
||||
initial = await projection.get_ops_run_projection(refresh=True)
|
||||
assert initial.available is True
|
||||
|
||||
async def failed_fetch():
|
||||
raise RuntimeError("upstream down")
|
||||
|
||||
monkeypatch.setattr(projection, "_fetch_ops_runs", failed_fetch)
|
||||
stale = await projection.get_ops_run_projection(refresh=True)
|
||||
assert stale.available is True
|
||||
assert stale.stale is True
|
||||
assert "upstream down" in stale.error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ops_run_projection_reports_unconfigured(monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "activity_core_url", None)
|
||||
result = await projection.get_ops_run_projection(refresh=True)
|
||||
assert result.available is False
|
||||
assert "ACTIVITY_CORE_URL is not configured" in result.error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ops_runs_summary_route(client, monkeypatch) -> None:
|
||||
async def fake_fetch():
|
||||
return {"counts": {"open": 3, "claimed": 2}, "items": []}
|
||||
|
||||
monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
|
||||
response = await client.get("/ops-runs/summary", params={"refresh": "true"})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["open"] == 3
|
||||
assert response.json()["claimed"] == 2
|
||||
assert (await client.post("/ops-runs/claim", json={})).status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_summary_contains_ops_run_projection(client, monkeypatch) -> None:
|
||||
async def fake_fetch():
|
||||
return {"counts": {"open": 4}, "items": []}
|
||||
|
||||
monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
|
||||
response = await client.get("/state/summary", params={"refresh": "true"})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ops_runs"]["open"] == 4
|
||||
Loading…
Add table
Add a link
Reference in a new issue