feat: add gitea agentic runtime control plane
This commit is contained in:
BIN
tests/unit/__pycache__/test_cli.cpython-314-pytest-8.4.2.pyc
Normal file
BIN
tests/unit/__pycache__/test_cli.cpython-314-pytest-8.4.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
84
tests/unit/test_cli.py
Normal file
84
tests/unit/test_cli.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from engine.devops_agent import cli
|
||||
|
||||
|
||||
class FakeProvider:
|
||||
def __init__(self, *, base_url: str, token: str) -> None:
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
|
||||
def parse_issue_comment_event(self, payload: dict[str, object]) -> dict[str, object]:
|
||||
repository = payload["repository"]
|
||||
issue = payload["issue"]
|
||||
comment = payload["comment"]
|
||||
return {
|
||||
"repo": repository["full_name"],
|
||||
"issue_number": issue["number"],
|
||||
"comment_body": comment["body"],
|
||||
}
|
||||
|
||||
def get_issue(self, repo: str, issue_number: int) -> dict[str, object]:
|
||||
return {
|
||||
"number": issue_number,
|
||||
"title": "Fix issue delivery flow",
|
||||
"body": "The agent should post evidence back to the issue.",
|
||||
"state": "open",
|
||||
"repo": repo,
|
||||
}
|
||||
|
||||
def post_issue_comment(self, repo: str, issue_number: int, body: str) -> dict[str, object]:
|
||||
return {"id": 1, "repo": repo, "issue_number": issue_number, "body": body}
|
||||
|
||||
|
||||
def test_compile_command_writes_lock_file() -> None:
|
||||
output_path = Path(".tmp/cli-tests/gitea-issue-delivery.lock.json")
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
exit_code = cli.main(
|
||||
[
|
||||
"compile",
|
||||
"workflows/gitea-issue-delivery.md",
|
||||
"--output",
|
||||
str(output_path),
|
||||
]
|
||||
)
|
||||
|
||||
assert exit_code == 0
|
||||
assert output_path.exists()
|
||||
|
||||
|
||||
def test_validate_command_returns_success() -> None:
|
||||
exit_code = cli.main(["validate", "workflows/gitea-issue-delivery.md"])
|
||||
|
||||
assert exit_code == 0
|
||||
|
||||
|
||||
def test_run_command_writes_runtime_artifact(monkeypatch) -> None:
|
||||
output_dir = Path(".tmp/cli-tests/runtime-run")
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setattr(cli, "GiteaProvider", FakeProvider)
|
||||
|
||||
exit_code = cli.main(
|
||||
[
|
||||
"run",
|
||||
"workflows/gitea-issue-delivery.md",
|
||||
"--event-payload",
|
||||
"tests/fixtures/gitea/comment_event.json",
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
"--base-url",
|
||||
"https://fun-md.com",
|
||||
"--token",
|
||||
"fake-token",
|
||||
]
|
||||
)
|
||||
|
||||
artifact_path = output_dir / "run-artifact.json"
|
||||
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert exit_code == 0
|
||||
assert artifact["result"] == "success"
|
||||
26
tests/unit/test_compiler.py
Normal file
26
tests/unit/test_compiler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from engine.devops_agent.compiler import compile_workflow
|
||||
from engine.devops_agent.spec import load_workflow_spec
|
||||
|
||||
|
||||
def test_compile_emits_normalized_lock_payload() -> None:
|
||||
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||
|
||||
lock = compile_workflow(spec)
|
||||
|
||||
assert lock["version"] == 1
|
||||
assert lock["workflow_name"] == "gitea-issue-delivery"
|
||||
assert lock["provider"] == "gitea"
|
||||
assert lock["triggers"] == [
|
||||
{
|
||||
"event": "issue_comment",
|
||||
"commands": ["@devops-agent"],
|
||||
}
|
||||
]
|
||||
assert lock["policy"]["path_scope"] == []
|
||||
assert lock["policy"]["require_human_merge"] is True
|
||||
assert lock["safe_outputs"]["add_comment"]["max"] == 3
|
||||
assert "issue_comment" in lock["required_evidence"]
|
||||
72
tests/unit/test_gitea_provider.py
Normal file
72
tests/unit/test_gitea_provider.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from engine.devops_agent.providers.gitea import GiteaProvider
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
body: dict[str, object] | None,
|
||||
) -> dict[str, object]:
|
||||
self.calls.append(
|
||||
{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
}
|
||||
)
|
||||
if url.endswith("/comments"):
|
||||
return {"id": 999, "body": body["body"] if body else ""}
|
||||
return json.loads(Path("tests/fixtures/gitea/issue.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_gitea_provider_fetches_issue() -> None:
|
||||
transport = FakeTransport()
|
||||
provider = GiteaProvider(
|
||||
base_url="https://fun-md.com",
|
||||
token="test-token",
|
||||
transport=transport,
|
||||
)
|
||||
|
||||
issue = provider.get_issue("Fun_MD/devops-skills", 48)
|
||||
|
||||
assert issue["number"] == 48
|
||||
assert transport.calls[0]["method"] == "GET"
|
||||
assert str(transport.calls[0]["url"]).endswith("/api/v1/repos/Fun_MD/devops-skills/issues/48")
|
||||
|
||||
|
||||
def test_gitea_provider_posts_issue_comment() -> None:
|
||||
transport = FakeTransport()
|
||||
provider = GiteaProvider(
|
||||
base_url="https://fun-md.com",
|
||||
token="test-token",
|
||||
transport=transport,
|
||||
)
|
||||
|
||||
response = provider.post_issue_comment("Fun_MD/devops-skills", 48, "Evidence posted")
|
||||
|
||||
assert response["id"] == 999
|
||||
assert transport.calls[0]["method"] == "POST"
|
||||
assert transport.calls[0]["body"] == {"body": "Evidence posted"}
|
||||
|
||||
|
||||
def test_gitea_provider_parses_comment_event() -> None:
|
||||
provider = GiteaProvider(base_url="https://fun-md.com", token="test-token")
|
||||
payload = json.loads(Path("tests/fixtures/gitea/comment_event.json").read_text(encoding="utf-8"))
|
||||
|
||||
event = provider.parse_issue_comment_event(payload)
|
||||
|
||||
assert event["repo"] == "Fun_MD/devops-skills"
|
||||
assert event["issue_number"] == 48
|
||||
assert "@devops-agent" in event["comment_body"]
|
||||
34
tests/unit/test_policies.py
Normal file
34
tests/unit/test_policies.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from engine.devops_agent.policies import PolicyViolation, RuntimePolicy
|
||||
|
||||
|
||||
def test_policy_allows_declared_safe_output() -> None:
|
||||
policy = RuntimePolicy(
|
||||
safe_outputs={"add_comment": {"max": 2}},
|
||||
path_scope=["engine/devops_agent/", "README.md"],
|
||||
)
|
||||
|
||||
policy.assert_operation_allowed("add_comment")
|
||||
|
||||
|
||||
def test_policy_rejects_undeclared_write_action() -> None:
|
||||
policy = RuntimePolicy(
|
||||
safe_outputs={"add_comment": {"max": 2}},
|
||||
path_scope=[],
|
||||
)
|
||||
|
||||
with pytest.raises(PolicyViolation, match="close_issue"):
|
||||
policy.assert_operation_allowed("close_issue")
|
||||
|
||||
|
||||
def test_policy_rejects_paths_outside_scope() -> None:
|
||||
policy = RuntimePolicy(
|
||||
safe_outputs={"write_file": {"max": 5}},
|
||||
path_scope=["engine/devops_agent/"],
|
||||
)
|
||||
|
||||
with pytest.raises(PolicyViolation, match="outside allowed path scope"):
|
||||
policy.assert_path_allowed("skills/gitea-issue-devops-agent/SKILL.md")
|
||||
11
tests/unit/test_smoke_imports.py
Normal file
11
tests/unit/test_smoke_imports.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import importlib
|
||||
|
||||
|
||||
def test_core_modules_are_importable() -> None:
|
||||
module_names = [
|
||||
"engine.devops_agent",
|
||||
"engine.devops_agent.cli",
|
||||
]
|
||||
|
||||
for module_name in module_names:
|
||||
importlib.import_module(module_name)
|
||||
29
tests/unit/test_spec_loader.py
Normal file
29
tests/unit/test_spec_loader.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from engine.devops_agent.spec import WorkflowSpecError, load_workflow_spec
|
||||
|
||||
|
||||
def test_load_workflow_spec_splits_frontmatter_and_body() -> None:
|
||||
spec = load_workflow_spec(Path("tests/fixtures/specs/valid_workflow.md"))
|
||||
|
||||
assert spec.name == "issue-delivery"
|
||||
assert spec.provider == "gitea"
|
||||
assert spec.frontmatter["safe_outputs"]["add_comment"]["max"] == 2
|
||||
assert "Read the selected issue" in spec.body
|
||||
|
||||
|
||||
def test_load_workflow_spec_rejects_missing_provider() -> None:
|
||||
with pytest.raises(WorkflowSpecError, match="provider"):
|
||||
load_workflow_spec(Path("tests/fixtures/specs/invalid_missing_provider.md"))
|
||||
|
||||
|
||||
def test_sample_workflow_spec_exists_and_loads() -> None:
|
||||
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||
|
||||
assert spec.name == "gitea-issue-delivery"
|
||||
assert spec.provider == "gitea"
|
||||
assert "add_comment" in spec.frontmatter["safe_outputs"]
|
||||
30
tests/unit/test_validator.py
Normal file
30
tests/unit/test_validator.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from engine.devops_agent.spec import load_workflow_spec
|
||||
from engine.devops_agent.validator import validate_workflow_spec
|
||||
|
||||
|
||||
def test_validate_accepts_the_sample_workflow() -> None:
|
||||
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||
|
||||
errors = validate_workflow_spec(spec)
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_validate_rejects_write_permissions_without_safe_outputs() -> None:
|
||||
spec = load_workflow_spec(Path("tests/fixtures/specs/no_safe_outputs_for_write.md"))
|
||||
|
||||
errors = validate_workflow_spec(spec)
|
||||
|
||||
assert any("safe_outputs" in error for error in errors)
|
||||
|
||||
|
||||
def test_validate_rejects_invalid_path_scope() -> None:
|
||||
spec = load_workflow_spec(Path("tests/fixtures/specs/invalid_path_scope.md"))
|
||||
|
||||
errors = validate_workflow_spec(spec)
|
||||
|
||||
assert any("path_scope" in error for error in errors)
|
||||
Reference in New Issue
Block a user