Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libs/hackbot-client/hackbot_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from hackbot_client.client import HackbotClient
from hackbot_client.models import RunRef, RunStatus
from hackbot_client.models import RunRef, RunStatus, TriggeredRun

__all__ = [
"HackbotClient",
"RunRef",
"RunStatus",
"TriggeredRun",
]
12 changes: 8 additions & 4 deletions libs/hackbot-client/hackbot_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import httpx

from hackbot_client.models import RunRef
from hackbot_client.models import TriggeredRun


class HackbotClient:
Expand All @@ -27,12 +27,12 @@ async def trigger_run(
*,
on_behalf_of: str | None = None,
dedupe_key: str | None = None,
) -> RunRef:
) -> TriggeredRun:
"""Create an agent run and return the API's typed run reference.

`dedupe_key` keys the work the run does, and a key belongs to one run
for good: repeated triggers carrying it are no-ops, answered with the
same run reference.
same run reference and `is_new=False`.
"""
headers = {"X-API-Key": self._api_key}
if on_behalf_of is not None:
Expand All @@ -49,4 +49,8 @@ async def trigger_run(
)

response.raise_for_status()
return RunRef.model_validate(response.json())
# The API distinguishes the two outcomes only by status code: `201` for the
# run this request started, `200` for one a `dedupe_key` collapsed onto.
return TriggeredRun.model_validate(
{**response.json(), "is_new": response.status_code == 201}
)
13 changes: 13 additions & 0 deletions libs/hackbot-client/hackbot_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ class RunRef(BaseModel):
run_id: UUID
agent: str
status: RunStatus


class TriggeredRun(RunRef):
"""The run a trigger resolved to, and whether that request is what started it.

`is_new` is a property of the request rather than of the run, and says
nothing about the run's `status`: the same run is `is_new` to the request
that created it and not to every later request carrying its `dedupe_key`.
That is why it is not on the API's own `RunRef`. Subclasses `RunRef` so a
caller that only wants the run keeps reading `run_id` off it unchanged.
"""

is_new: bool
37 changes: 37 additions & 0 deletions libs/hackbot-client/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ async def test_trigger_run_posts_inputs_and_returns_typed_reference(monkeypatch)
assert run.run_id == UUID(RUN_ID)
assert run.agent == "bug-fix"
assert run.status is RunStatus.pending
# `201`: this request is what started it.
assert run.is_new is True
assert captured == {
"timeout": 30.0,
"url": "https://hackbot.example/agents/bug-fix/runs",
Expand Down Expand Up @@ -97,3 +99,38 @@ async def test_trigger_run_rejects_an_invalid_success_response(monkeypatch):

with pytest.raises(ValidationError):
await _client().trigger_run("bug-fix", {"bug_id": 1234})


async def test_trigger_run_reports_a_deduplicated_run_as_not_new(monkeypatch):
# `200` rather than `201`: the key already belonged to this run, so the
# request that got this answer started nothing.
_capture_post(
monkeypatch,
httpx.Response(
200,
json={"run_id": RUN_ID, "agent": "bug-fix", "status": "running"},
),
)

run = await _client().trigger_run(
"bug-fix", {"bug_id": 1234}, dedupe_key="push:autoland:abc123"
)

assert run.run_id == UUID(RUN_ID)
assert run.is_new is False


async def test_trigger_run_sends_the_dedupe_key_as_a_query_parameter(monkeypatch):
captured = _capture_post(
monkeypatch,
httpx.Response(
201,
json={"run_id": RUN_ID, "agent": "bug-fix", "status": "pending"},
),
)

await _client().trigger_run(
"bug-fix", {"bug_id": 1234}, dedupe_key="push:autoland:abc123"
)

assert captured["params"] == {"dedupe_key": "push:autoland:abc123"}