agent: Implemented the explicit persisted webhook state machine.
This commit is contained in:
+36
-95
@@ -1,54 +1,29 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from agentci.api.webhook import _event_from_payload, _handle_command, valid_signature
|
||||
|
||||
|
||||
class FakeStorage:
|
||||
def __init__(self) -> None:
|
||||
self.jobs = []
|
||||
self.deliveries: set[str] = set()
|
||||
class FakeHost:
|
||||
def __init__(self, duplicate: bool = False) -> None:
|
||||
self.events = []
|
||||
self.duplicate = duplicate
|
||||
|
||||
async def enqueue(self, delivery_id, job):
|
||||
if delivery_id in self.deliveries:
|
||||
return False
|
||||
self.deliveries.add(delivery_id)
|
||||
self.jobs.append(job)
|
||||
return True
|
||||
|
||||
async def record_delivery(self, delivery_id, _comment_id):
|
||||
if delivery_id in self.deliveries:
|
||||
return False
|
||||
self.deliveries.add(delivery_id)
|
||||
return True
|
||||
|
||||
async def set_job_comment(self, *_args):
|
||||
return None
|
||||
|
||||
|
||||
class FakeGitea:
|
||||
def __init__(self, permitted: bool = True) -> None:
|
||||
self.permitted = permitted
|
||||
self.comments: list[str] = []
|
||||
|
||||
async def has_write_permission(self, *_args):
|
||||
return self.permitted
|
||||
|
||||
async def create_comment(self, _owner, _repo, _number, body):
|
||||
self.comments.append(body)
|
||||
return len(self.comments)
|
||||
async def receive(self, event):
|
||||
self.events.append(event)
|
||||
state = SimpleNamespace(id="job", receive_sequence=1)
|
||||
return SimpleNamespace(state=state, duplicate=self.duplicate)
|
||||
|
||||
|
||||
def payload(body: str, *, is_pull: bool = False) -> dict:
|
||||
value = {
|
||||
"action": "created",
|
||||
"comment": {"id": 8, "body": body, "user": {"login": "alice"}},
|
||||
"repository": {
|
||||
"name": "repo",
|
||||
"owner": {"login": "org"},
|
||||
},
|
||||
"repository": {"name": "repo", "owner": {"login": "org"}},
|
||||
"issue": {"number": 4},
|
||||
"is_pull": is_pull,
|
||||
}
|
||||
@@ -57,74 +32,40 @@ def payload(body: str, *, is_pull: bool = False) -> dict:
|
||||
return value
|
||||
|
||||
|
||||
def test_extracts_pull_request_event() -> None:
|
||||
event = _event_from_payload("delivery", payload("/agent fix now", is_pull=True))
|
||||
assert event is not None
|
||||
assert event.pr_number == 4
|
||||
assert event.target_key == "org/repo:pr:4"
|
||||
|
||||
|
||||
async def test_authorized_command_is_queued() -> None:
|
||||
storage = FakeStorage()
|
||||
gitea = FakeGitea()
|
||||
container = SimpleNamespace(storage=storage, gitea=gitea)
|
||||
event = _event_from_payload("delivery", payload("/agent plan consider migrations"))
|
||||
assert event is not None
|
||||
response = await _handle_command(container, event)
|
||||
assert response.status_code == 202
|
||||
assert len(storage.jobs) == 1
|
||||
assert "queued" in gitea.comments[0]
|
||||
|
||||
|
||||
async def test_unauthorized_command_is_rejected_and_deduplicated() -> None:
|
||||
storage = FakeStorage()
|
||||
gitea = FakeGitea(permitted=False)
|
||||
container = SimpleNamespace(storage=storage, gitea=gitea)
|
||||
event = _event_from_payload("delivery", payload("/agent implement"))
|
||||
assert event is not None
|
||||
|
||||
await _handle_command(container, event)
|
||||
await _handle_command(container, event)
|
||||
|
||||
assert storage.jobs == []
|
||||
assert len(gitea.comments) == 1
|
||||
assert "write permission" in gitea.comments[0]
|
||||
|
||||
|
||||
async def test_iterate_message_is_preserved_on_queued_job() -> None:
|
||||
storage = FakeStorage()
|
||||
container = SimpleNamespace(storage=storage, gitea=FakeGitea())
|
||||
async def test_command_is_forwarded_without_parsing() -> None:
|
||||
host = FakeHost()
|
||||
event = _event_from_payload(
|
||||
"delivery",
|
||||
payload(
|
||||
"/agent iterate\n\nkeep the API stable\nlimit changes to the parser",
|
||||
is_pull=True,
|
||||
),
|
||||
"delivery", payload("/agent iterate\n\nkeep raw body", is_pull=True)
|
||||
)
|
||||
assert event is not None
|
||||
|
||||
response = await _handle_command(container, event)
|
||||
|
||||
response = await _handle_command(SimpleNamespace(state_machine=host), event)
|
||||
assert response.status_code == 202
|
||||
assert len(storage.jobs) == 1
|
||||
assert storage.jobs[0].message == (
|
||||
"keep the API stable\nlimit changes to the parser"
|
||||
)
|
||||
assert host.events[0].body == "/agent iterate\n\nkeep raw body"
|
||||
|
||||
|
||||
async def test_duplicate_returns_200() -> None:
|
||||
event = _event_from_payload("delivery", payload("/agent plan"))
|
||||
assert event is not None
|
||||
response = await _handle_command(SimpleNamespace(state_machine=FakeHost(True)), event)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_missing_delivery_is_rejected() -> None:
|
||||
event = _event_from_payload("", payload("/agent plan"))
|
||||
assert event is not None
|
||||
with pytest.raises(HTTPException) as raised:
|
||||
await _handle_command(SimpleNamespace(state_machine=FakeHost()), event)
|
||||
assert raised.value.status_code == 400
|
||||
|
||||
|
||||
async def test_non_command_is_ignored() -> None:
|
||||
container = SimpleNamespace(storage=FakeStorage(), gitea=FakeGitea())
|
||||
event = _event_from_payload("delivery", payload("ordinary discussion"))
|
||||
assert event is not None
|
||||
response = await _handle_command(container, event)
|
||||
response = await _handle_command(SimpleNamespace(state_machine=FakeHost()), event)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_rejects_bad_signature() -> None:
|
||||
def test_signature_validation() -> None:
|
||||
signature = hmac.new(b"secret", b"{}", hashlib.sha256).hexdigest()
|
||||
assert valid_signature(b"secret", b"{}", signature)
|
||||
assert not valid_signature(b"secret", b"{}", "bad")
|
||||
|
||||
|
||||
def test_accepts_valid_signature() -> None:
|
||||
body = json.dumps(payload("ordinary comment")).encode()
|
||||
signature = hmac.new(b"secret", body, hashlib.sha256).hexdigest()
|
||||
assert valid_signature(b"secret", body, signature)
|
||||
|
||||
Reference in New Issue
Block a user