mirror of
https://github.com/Colton-z/AstraBox.git
synced 2026-09-28 06:02:56 +08:00
The open-source, self-hosted alternative to Claude Managed Agents: run Claude Code, Codex, Hermes, DeepSeek Harness and Pi as managed Agents on your own infrastructure, with conversations that start and resume in seconds.
381 lines
14 KiB
Python
381 lines
14 KiB
Python
"""Apply, diff and destroy semantics against a recording deployment.
|
|
|
|
The fake deployment records every request, so each test can assert not only the
|
|
reported verdict but that the right number of writes — often zero — reached the
|
|
API. A verdict that is right while the writes are wrong is the failure these
|
|
tests exist to catch.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from astrabox.cli.client import ApiClient, Endpoint
|
|
from astrabox.cli.document import DOCUMENT_VERSION, parse_document
|
|
from astrabox.cli.output import EXIT_CONFLICT, CliError
|
|
from astrabox.cli.resources import (
|
|
ACTION_ABSENT,
|
|
ACTION_CREATE,
|
|
ACTION_DELETE,
|
|
ACTION_RETAINED,
|
|
ACTION_UNCHANGED,
|
|
ACTION_UPDATE,
|
|
_cmd_destroy,
|
|
plan_and_run,
|
|
)
|
|
|
|
_WRITE_METHODS = ("POST", "PUT", "PATCH", "DELETE")
|
|
|
|
#: The authoring contract a deployment serves. `display_name` is nested in the
|
|
#: stored document exactly as the real Agent schema nests it.
|
|
_AGENT_FIELDS = [
|
|
{"key": "name", "type": "string", "required": True},
|
|
{"key": "model", "type": "string", "required": True},
|
|
{"key": "environment_name", "type": "env_ref", "required": True},
|
|
{"key": "display_name", "type": "string", "path": "display_meta.display_name"},
|
|
{"key": "enabled", "type": "boolean"},
|
|
]
|
|
_ENVIRONMENT_FIELDS = [
|
|
{"key": "name", "type": "string", "required": True},
|
|
{"key": "engine_kind", "type": "enum", "required": True},
|
|
{"key": "endpoint_provider", "type": "enum"},
|
|
{"key": "enabled", "type": "boolean"},
|
|
]
|
|
|
|
|
|
class _Deployment:
|
|
"""A deployment that answers the routes apply reads and writes."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
agents: list[dict[str, Any]] | None = None,
|
|
environments: list[dict[str, Any]] | None = None,
|
|
) -> None:
|
|
self.agents = agents or []
|
|
self.environments = environments or []
|
|
self.requests: list[tuple[str, str, Any]] = []
|
|
|
|
def client(self) -> ApiClient:
|
|
return ApiClient(
|
|
Endpoint(base_url="http://deployment.test", token=None),
|
|
transport=httpx.MockTransport(self._handle),
|
|
)
|
|
|
|
@property
|
|
def writes(self) -> list[tuple[str, str, Any]]:
|
|
return [entry for entry in self.requests if entry[0] in _WRITE_METHODS]
|
|
|
|
def _handle(self, request: httpx.Request) -> httpx.Response:
|
|
body = json.loads(request.content) if request.content else None
|
|
self.requests.append((request.method, request.url.path, body))
|
|
path = request.url.path
|
|
|
|
if request.method == "GET" and path == "/api/v1/admin/agent-schema":
|
|
return _envelope({"version": 1, "groups": [], "fields": _AGENT_FIELDS})
|
|
if request.method == "GET" and path == "/api/v1/admin/environment-schema":
|
|
return _envelope({"version": 1, "groups": [], "fields": _ENVIRONMENT_FIELDS})
|
|
if request.method == "GET" and path == "/api/v1/agents":
|
|
return _envelope(self.agents)
|
|
if request.method == "GET" and path == "/api/v1/admin/environments":
|
|
return _envelope(self.environments)
|
|
if request.method == "POST" and path == "/api/v1/agents":
|
|
created = {"agent_id": f"id-{len(self.agents)}", "version": 1, **(body or {})}
|
|
self.agents.append(created)
|
|
return _envelope(created)
|
|
if request.method == "PUT" and path.startswith("/api/v1/agents/"):
|
|
return _envelope({"agent_id": path.rsplit("/", 1)[-1], **(body or {})})
|
|
if request.method == "PUT" and path.startswith("/api/v1/admin/environments/"):
|
|
return _envelope({"name": path.rsplit("/", 1)[-1], **(body or {})})
|
|
if request.method == "DELETE" and path.startswith("/api/v1/agents/"):
|
|
return _envelope({"deleted": True})
|
|
return httpx.Response(404, json={"code": "NOT_FOUND", "message": path, "data": None})
|
|
|
|
|
|
def _envelope(data: Any) -> httpx.Response:
|
|
return httpx.Response(200, json={"code": "OK", "message": "success", "data": data})
|
|
|
|
|
|
def _document(**overrides: Any):
|
|
raw: dict[str, Any] = {
|
|
"version": DOCUMENT_VERSION,
|
|
"environments": [{"name": "default", "endpoint_provider": "litellm"}],
|
|
"agents": [
|
|
{"name": "researcher", "model": "claude-opus-5", "environment_name": "default"}
|
|
],
|
|
}
|
|
raw.update(overrides)
|
|
return parse_document(raw)
|
|
|
|
|
|
def test_apply_creates_a_missing_agent_and_environment() -> None:
|
|
deployment = _Deployment()
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, _document(), write=True)
|
|
|
|
assert [row["action"] for row in results] == [ACTION_CREATE, ACTION_CREATE]
|
|
assert [(method, path) for method, path, _ in deployment.writes] == [
|
|
("PUT", "/api/v1/admin/environments/default"),
|
|
("POST", "/api/v1/agents"),
|
|
]
|
|
|
|
|
|
def test_apply_writes_the_environment_before_the_agent() -> None:
|
|
"""The Agent's environment_name is validated against an existing
|
|
environment, so a first-run document that applied in declaration order
|
|
would be rejected by the deployment."""
|
|
deployment = _Deployment()
|
|
|
|
with deployment.client() as client:
|
|
plan_and_run(client, _document(), write=True)
|
|
|
|
paths = [path for method, path, _ in deployment.writes]
|
|
assert paths.index("/api/v1/admin/environments/default") < paths.index("/api/v1/agents")
|
|
|
|
|
|
def test_apply_updates_an_existing_agent_and_fences_on_the_stored_version() -> None:
|
|
"""Sending the stored version turns a document written against a stale read
|
|
into a 409 instead of an overwrite of somebody else's edit."""
|
|
deployment = _Deployment(
|
|
agents=[
|
|
{
|
|
"agent_id": "agent-1",
|
|
"version": 7,
|
|
"name": "researcher",
|
|
"model": "claude-sonnet-5",
|
|
"environment_name": "default",
|
|
}
|
|
],
|
|
environments=[{"name": "default", "endpoint_provider": "litellm"}],
|
|
)
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, _document(), write=True)
|
|
|
|
agent_result = next(row for row in results if row["kind"] == "agent")
|
|
assert agent_result["action"] == ACTION_UPDATE
|
|
assert agent_result["fields"] == ["model"]
|
|
method, path, body = deployment.writes[0]
|
|
assert (method, path) == ("PUT", "/api/v1/agents/agent-1")
|
|
assert body["version"] == 7
|
|
assert body["model"] == "claude-opus-5"
|
|
|
|
|
|
def test_apply_reports_unchanged_and_sends_no_write() -> None:
|
|
"""Re-applying an unchanged document must converge without touching the
|
|
deployment; a write here would bump versions on every run."""
|
|
deployment = _Deployment(
|
|
agents=[
|
|
{
|
|
"agent_id": "agent-1",
|
|
"version": 3,
|
|
"name": "researcher",
|
|
"model": "claude-opus-5",
|
|
"environment_name": "default",
|
|
}
|
|
],
|
|
environments=[{"name": "default", "endpoint_provider": "litellm"}],
|
|
)
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, _document(), write=True)
|
|
|
|
assert [row["action"] for row in results] == [ACTION_UNCHANGED, ACTION_UNCHANGED]
|
|
assert deployment.writes == []
|
|
|
|
|
|
def test_diff_sends_no_writes_while_reporting_the_same_verdicts() -> None:
|
|
deployment = _Deployment()
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, _document(), write=False)
|
|
|
|
assert [row["action"] for row in results] == [ACTION_CREATE, ACTION_CREATE]
|
|
assert deployment.writes == []
|
|
assert deployment.requests, "diff must still read the deployment"
|
|
|
|
|
|
def test_apply_refuses_an_ambiguous_agent_name_without_writing() -> None:
|
|
"""The deployment does not constrain Agent names to be unique. Picking one
|
|
of two matches would make the same document mean different things on two
|
|
deployments, so apply stops — and stops before writing anything."""
|
|
deployment = _Deployment(
|
|
agents=[
|
|
{"agent_id": "agent-1", "version": 1, "name": "researcher"},
|
|
{"agent_id": "agent-2", "version": 1, "name": "researcher"},
|
|
],
|
|
environments=[{"name": "default", "endpoint_provider": "litellm"}],
|
|
)
|
|
|
|
with deployment.client() as client:
|
|
with pytest.raises(CliError) as caught:
|
|
plan_and_run(client, _document(), write=True)
|
|
|
|
assert caught.value.exit_code == EXIT_CONFLICT
|
|
assert caught.value.details["agent_ids"] == ["agent-1", "agent-2"]
|
|
assert deployment.writes == []
|
|
|
|
|
|
def test_the_environment_body_omits_the_name_carried_in_the_path() -> None:
|
|
"""`PUT /admin/environments/{name}` takes the name from the path and drops
|
|
it from the payload, so sending it back would be a field the deployment
|
|
discards."""
|
|
deployment = _Deployment()
|
|
|
|
with deployment.client() as client:
|
|
plan_and_run(client, _document(), write=True)
|
|
|
|
_, _, body = deployment.writes[0]
|
|
assert "name" not in body
|
|
assert body["endpoint_provider"] == "litellm"
|
|
|
|
|
|
def test_destroy_deletes_agents_and_reports_environments_as_retained(
|
|
monkeypatch: pytest.MonkeyPatch, capsys
|
|
) -> None:
|
|
"""The deployment serves no delete route for an environment preset.
|
|
Reporting the removal anyway would tell a caller its deployment is clean
|
|
when the environment is still there."""
|
|
deployment = _Deployment(
|
|
agents=[{"agent_id": "agent-1", "version": 1, "name": "researcher"}],
|
|
environments=[{"name": "default"}],
|
|
)
|
|
args = _destroy_args()
|
|
_wire(monkeypatch, deployment)
|
|
|
|
assert _cmd_destroy(args) == 0
|
|
|
|
rows = json.loads(capsys.readouterr().out)["resources"]
|
|
assert {row["name"]: row["action"] for row in rows} == {
|
|
"researcher": ACTION_DELETE,
|
|
"default": ACTION_RETAINED,
|
|
}
|
|
assert [(method, path) for method, path, _ in deployment.writes] == [
|
|
("DELETE", "/api/v1/agents/agent-1")
|
|
]
|
|
|
|
|
|
def test_destroy_reports_an_absent_agent_rather_than_failing(
|
|
monkeypatch: pytest.MonkeyPatch, capsys
|
|
) -> None:
|
|
deployment = _Deployment(environments=[{"name": "default"}])
|
|
args = _destroy_args()
|
|
_wire(monkeypatch, deployment)
|
|
|
|
assert _cmd_destroy(args) == 0
|
|
|
|
rows = json.loads(capsys.readouterr().out)["resources"]
|
|
assert next(row for row in rows if row["name"] == "researcher")["action"] == ACTION_ABSENT
|
|
assert deployment.writes == []
|
|
|
|
|
|
def test_a_nested_field_is_compared_where_the_deployment_stores_it() -> None:
|
|
"""The schema says display_name lives at display_meta.display_name. Looking
|
|
for it at the top level finds nothing, so an unchanged document would
|
|
report that field as changed on every single run — and an export of a
|
|
deployment would never converge against the deployment it came from."""
|
|
deployment = _Deployment(
|
|
agents=[
|
|
{
|
|
"agent_id": "agent-1",
|
|
"version": 2,
|
|
"name": "researcher",
|
|
"model": "claude-opus-5",
|
|
"environment_name": "default",
|
|
"display_meta": {"display_name": "Researcher"},
|
|
}
|
|
],
|
|
environments=[{"name": "default", "endpoint_provider": "litellm"}],
|
|
)
|
|
document = _document(
|
|
agents=[
|
|
{
|
|
"name": "researcher",
|
|
"model": "claude-opus-5",
|
|
"environment_name": "default",
|
|
"display_name": "Researcher",
|
|
}
|
|
]
|
|
)
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, document, write=True)
|
|
|
|
agent_result = next(row for row in results if row["kind"] == "agent")
|
|
assert agent_result["action"] == ACTION_UNCHANGED, (
|
|
f"nested field reported as changed: {agent_result['fields']}"
|
|
)
|
|
assert deployment.writes == []
|
|
|
|
|
|
def test_a_nested_field_that_really_differs_is_still_reported() -> None:
|
|
"""The path lookup must not turn into "never changed"."""
|
|
deployment = _Deployment(
|
|
agents=[
|
|
{
|
|
"agent_id": "agent-1",
|
|
"version": 2,
|
|
"name": "researcher",
|
|
"model": "claude-opus-5",
|
|
"environment_name": "default",
|
|
"display_meta": {"display_name": "Old name"},
|
|
}
|
|
],
|
|
environments=[{"name": "default", "endpoint_provider": "litellm"}],
|
|
)
|
|
document = _document(
|
|
agents=[
|
|
{
|
|
"name": "researcher",
|
|
"model": "claude-opus-5",
|
|
"environment_name": "default",
|
|
"display_name": "New name",
|
|
}
|
|
]
|
|
)
|
|
|
|
with deployment.client() as client:
|
|
results = plan_and_run(client, document, write=True)
|
|
|
|
agent_result = next(row for row in results if row["kind"] == "agent")
|
|
assert agent_result["action"] == ACTION_UPDATE
|
|
assert agent_result["fields"] == ["display_name"]
|
|
|
|
|
|
def _wire(monkeypatch: pytest.MonkeyPatch, deployment: _Deployment) -> None:
|
|
"""Point the subcommand's client factory at the recording deployment."""
|
|
monkeypatch.setattr(
|
|
"astrabox.cli.resources._client", lambda _args: deployment.client()
|
|
)
|
|
|
|
|
|
def _destroy_args() -> Any:
|
|
"""The parsed-args object `_cmd_destroy` reads, over a real document file."""
|
|
import tempfile
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
document_path = Path(tempfile.mkdtemp()) / "astrabox.yaml"
|
|
document_path.write_text(
|
|
"version: 1\n"
|
|
"environments:\n"
|
|
" - name: default\n"
|
|
"agents:\n"
|
|
" - name: researcher\n"
|
|
" model: claude-opus-5\n"
|
|
" environment_name: default\n",
|
|
encoding="utf-8",
|
|
)
|
|
return SimpleNamespace(
|
|
file=str(document_path),
|
|
yes=True,
|
|
output="json",
|
|
endpoint="http://deployment.test",
|
|
token=None,
|
|
)
|