Files
aios/tests/test_repository_discovery_contract.py
T

224 lines
7.7 KiB
Python

from __future__ import annotations
import ast
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
BridgeV2Adapter,
BridgeV2Error,
OPERATION_OPTIONAL_FIELDS,
OPERATION_REQUIRED_FIELDS,
READ_ONLY_OPERATIONS,
operation_contracts,
)
DISCOVERY_CONTRACTS = {
"listRepositoryTree": {
"path": "/v2/repository/tree",
"legacy_path": "/api/repository/tree",
},
"searchRepositorySource": {
"path": "/v2/repository/search",
"legacy_path": "/api/repository/search",
},
"getDiffIndex": {
"path": "/v2/repository/diff-index",
"legacy_path": "/api/repository/diff-index",
},
"readRepositoryFile": {
"path": "/v2/repository/file",
"legacy_path": "/api/repository/file",
},
}
def _openapi_path_block(document: str, path: str) -> str:
marker = f" {path}:\n"
start = document.index(marker)
end = document.find("\n /", start + len(marker))
return document[start:] if end < 0 else document[start:end]
def _assignment(source: str, name: str) -> ast.AST:
module = ast.parse(source)
for node in module.body:
if isinstance(node, (ast.Assign, ast.AnnAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
if any(isinstance(target, ast.Name) and target.id == name for target in targets):
return node.value
raise AssertionError(f"assignment {name!r} not found")
def _literal_assignment(source: str, name: str):
value = _assignment(source, name)
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
assert value.func.id == "frozenset"
assert len(value.args) == 1
return set(ast.literal_eval(value.args[0]))
return ast.literal_eval(value)
def test_discovery_runtime_bridge_and_openapi_contracts_are_aligned():
document = (ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml").read_text(
encoding="utf-8"
)
bridge_source = (ROOT / "active-runtime" / "app" / "aios_chatgpt_bridge_v2_server.py").read_text(
encoding="utf-8"
)
actions_source = (ROOT / "active-runtime" / "app" / "aios_chatgpt_actions_api.py").read_text(
encoding="utf-8"
)
routes = _literal_assignment(bridge_source, "ROUTES")
discovery_actions = _literal_assignment(actions_source, "_DISCOVERY_ACTIONS")
read_only_paths = _literal_assignment(actions_source, "READ_ONLY_GET_PATHS")
runtime = operation_contracts()
for operation, expected in DISCOVERY_CONTRACTS.items():
assert runtime[operation] == {
"method": "GET",
"path": expected["path"],
"required_fields": list(OPERATION_REQUIRED_FIELDS[operation]),
"optional_fields": list(OPERATION_OPTIONAL_FIELDS[operation]),
"read_only": True,
}
assert operation in READ_ONLY_OPERATIONS
assert routes[expected["path"]] == operation
assert expected["legacy_path"] in read_only_paths
assert expected["legacy_path"] in discovery_actions
block = _openapi_path_block(document, expected["path"])
assert block.count(f"operationId: {operation}") == 1
assert " get:\n" in block
assert " security: []" in block
assert " x-openai-isConsequential: false" in block
assert " x-aios-read-only: true" in block
assert " x-aios-bounded: true" in block
assert " x-aios-repository-scoped: true" in block
assert " x-aios-no-shell: true" in block
assert f" x-aios-legacy-path: {expected['legacy_path']}" in block
parameter_names = {
line.split("name:", 1)[1].split(",", 1)[0].strip()
for line in block.splitlines()
if "name:" in line and "in: query" in line
}
assert parameter_names == set(OPERATION_REQUIRED_FIELDS[operation]) | set(
OPERATION_OPTIONAL_FIELDS[operation]
)
for operation in DISCOVERY_CONTRACTS:
assert document.count(f"operationId: {operation}") == 1
class_nodes = [
node
for node in ast.parse(actions_source).body
if isinstance(node, ast.ClassDef) and node.name == "H"
]
assert len(class_nodes) == 1
post_methods = [
node for node in class_nodes[0].body
if isinstance(node, ast.FunctionDef) and node.name == "do_POST"
]
assert post_methods
effective_post = ast.get_source_segment(actions_source, post_methods[-1]) or ""
assert "method_not_allowed" in effective_post
assert "max_num_fields=64" in actions_source
assert "query_too_large" in actions_source
def test_discovery_adapter_rejects_task_principal_and_write_identity_fields():
called = False
def handler(_request):
nonlocal called
called = True
return {}
adapter = BridgeV2Adapter(
runtime_status={}, handlers={"listRepositoryTree": handler}
)
request = {
"repository": "aios/aios",
"ref": "main",
"limit": 1,
"task_id": "must-not-enter-read-route",
"principal": "must-not-enter-read-route",
"writer_lease": "must-not-enter-read-route",
}
with pytest.raises(BridgeV2Error, match="BRIDGE_REQUEST_FIELDS_UNKNOWN"):
adapter.listRepositoryTree(request)
assert called is False
def test_discovery_source_is_read_only_and_staging_freeze_remains_intact():
source = (ROOT / "active-runtime" / "app" / "aios_repository_discovery.py").read_text(
encoding="utf-8"
)
assert '_READ_ONLY_COMMANDS = frozenset({"cat-file", "diff", "ls-tree", "rev-parse"})' in source
assert "shell=True" not in source
assert "os.system" not in source
assert "update-ref" not in source
assert "git push" not in source
unit = (
ROOT
/ "deploy"
/ "systemd"
/ "aios-gitops-model-api-v2r2c1-staging.service"
).read_text(encoding="utf-8")
assert "AIOS_GITOPS_WRITES_ENABLED=false" in unit
assert "AIOS_GITOPS_PORT=18893" in unit
assert "AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false" in unit
@pytest.mark.parametrize(
("path", "query"),
[
("/api/repository/tree", "repository=aios%2Faios&ref=main&limit=1"),
(
"/api/repository/search",
"repository=aios%2Faios&ref=main&query=needle&max_matches=1&max_bytes=16",
),
(
"/api/repository/diff-index",
"repository=aios%2Faios&from=main&to=main&limit=1",
),
(
"/api/repository/file",
"repository=aios%2Faios&ref=main&path=README.md",
),
],
)
def test_actions_discovery_get_route_dispatches_and_post_is_rejected(
monkeypatch: pytest.MonkeyPatch, path: str, query: str
):
if sys.platform == "win32":
pytest.skip("Actions adapter imports POSIX-only fcntl; Windows integration is unverified")
import aios_chatgpt_actions_api as actions_api
observed: list[tuple[str, object]] = []
def fake_discovery_request(route: str, values: dict[str, list[str]]):
observed.append((route, values))
return {"route": route}
monkeypatch.setattr(actions_api, "repository_discovery_request", fake_discovery_request)
handler = object.__new__(actions_api.H)
responses: list[tuple[object, int]] = []
handler.sendj = lambda value, code=200: responses.append((value, code))
handler.path = f"{path}?{query}"
actions_api.H.do_GET(handler)
assert observed[-1][0] == path
assert responses[-1] == ({"route": path}, 200)
handler.path = path
actions_api.H.do_POST(handler)
assert responses[-1] == ({"error": "method_not_allowed"}, 405)