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
12 changes: 12 additions & 0 deletions hookdeck/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,18 @@ async def queue_depth(
async def list_issues(self, **params: Any) -> Any:
return await self.request("GET", "/issues", params=params)

async def count_issues(self, **params: Any) -> int:
"""How many issues match, in total rather than on a page.

The list endpoints paginate and their ``count`` is the page's own size,
so counting from a list means counting to whatever limit was asked for.
Issues have a dedicated count endpoint; events do not.
"""
result = await self.request("GET", "/issues/count", params=params)
if isinstance(result, dict):
return int(result.get("count") or 0)
return 0


def run_sync(coro: Any) -> Any:
"""Run *coro* from synchronous CLI code."""
Expand Down
18 changes: 14 additions & 4 deletions hookdeck/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@

TOOLSET = "hookdeck"

#: How many failed events `hookdeck_queue_status` counts before giving up and
#: reporting a floor. High enough that a real inbox is counted exactly, low
#: enough that a badly broken one does not stall the tool.
FAILED_SCAN_LIMIT = 100


def _run(coro: Any) -> Any:
"""Run *coro* from a synchronous tool handler.
Expand Down Expand Up @@ -140,13 +145,18 @@ def hookdeck_queue_status(_args: dict) -> str:
async def _go() -> str:
async with HookdeckAPI() as api:
depth = await api.queue_depth()
failed = await api.list_events(status="FAILED", limit=1)
issues = await api.list_issues(status="OPENED", limit=1)
failed = await api.list_events(status="FAILED", limit=FAILED_SCAN_LIMIT)
open_issues = await api.count_issues(status="OPENED")
seen = len(_models(failed))
return json.dumps(
{
"queue_depth": depth,
"failed_events_page_count": len(_models(failed)),
"open_issues_page_count": len(_models(issues)),
"failed_events": seen,
# Events have no count endpoint, so this is what one page
# holds. Saying when it is capped stops the model reporting a
# ceiling as though it were a total.
"failed_events_is_at_least": seen >= FAILED_SCAN_LIMIT,
"open_issues": open_issues,
}
)

Expand Down
49 changes: 49 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ async def list_events(self, **kw):
async def list_issues(self, **kw):
return await self._record("list_issues", **kw)

async def count_issues(self, **kw):
return await self._record("count_issues", **kw) or 0

async def get_event_raw_body(self, event_id):
return await self._record("get_event_raw_body", event_id)

Expand Down Expand Up @@ -466,3 +469,49 @@ def test_every_required_parameter_is_a_declared_parameter():
params = schema["function"]["parameters"]
missing = set(params["required"]) - set(params["properties"])
assert not missing, f"{name} requires undeclared {missing}"


# ----------------------------------------------------------------------
# Queue status reports numbers, not page sizes
# ----------------------------------------------------------------------


def test_queue_status_reports_the_real_number_of_open_issues(api):
# Counted from the dedicated endpoint. Counting from a listing would count
# to whatever limit was asked for — a project with four open issues used
# to be reported as one, and the model then guessed at what "1" meant.
api.responses["count_issues"] = 4
api.responses["list_events"] = {"models": []}
status = json.loads(call("hookdeck_queue_status"))

assert status["open_issues"] == 4
assert calls_named(api, "count_issues") == [
("count_issues", (), {"status": "OPENED"})
]
# The old page-size fields are gone, not merely renamed alongside.
assert "open_issues_page_count" not in status
assert "failed_events_page_count" not in status


def test_queue_status_counts_failed_events_exactly_when_it_can(api):
api.responses["list_events"] = {"models": [{"id": f"evt_{i}"} for i in range(7)]}
status = json.loads(call("hookdeck_queue_status"))
assert status["failed_events"] == 7
assert status["failed_events_is_at_least"] is False


def test_a_full_page_of_failures_is_flagged_as_a_floor(api):
# Events have no count endpoint, so a full page means "at least this
# many". Reporting it bare would let the model state a ceiling as a total.
api.responses["list_events"] = {
"models": [{"id": f"evt_{i}"} for i in range(tools.FAILED_SCAN_LIMIT)]
}
status = json.loads(call("hookdeck_queue_status"))
assert status["failed_events"] == tools.FAILED_SCAN_LIMIT
assert status["failed_events_is_at_least"] is True


def test_queue_status_asks_for_more_than_one_failure(api):
api.responses["list_events"] = {"models": []}
call("hookdeck_queue_status")
assert calls_named(api, "list_events")[0][2]["limit"] == tools.FAILED_SCAN_LIMIT
Loading