diff --git a/ariadne/app_admin_routes.py b/ariadne/app_admin_routes.py index 175287a..ac33279 100644 --- a/ariadne/app_admin_routes.py +++ b/ariadne/app_admin_routes.py @@ -217,6 +217,27 @@ def _register_admin_routes(app: FastAPI, require_auth: Callable, deps: Callable[ module = deps() return JSONResponse(module.run_testing_triage_diagnosis(module.storage)) + # The scheduler runs this every minute, which is cron's finest granularity + # and still up to sixty seconds of silence after a build goes red. That is + # the single largest delay between a failure happening and the system + # visibly reacting to it. Running the same tick on demand collapses it; + # the tick is idempotent - incidents dedupe on job and build number - so + # an extra run can only ever be a no-op. + @app.post("/api/admin/hermes/autotriage/run") + def run_hermes_autotriage_now(ctx: AuthContext = Depends(require_auth)) -> JSONResponse: + """Run one Hermes auto-triage tick immediately.""" + + module = deps() + module._require_admin(ctx) + return JSONResponse(module.run_hermes_autotriage(module.storage)) + + @app.post("/api/internal/hermes/autotriage/run") + def run_hermes_autotriage_now_internal() -> JSONResponse: + """Run one Hermes auto-triage tick for trusted internal callers.""" + + module = deps() + return JSONResponse(module.run_hermes_autotriage(module.storage)) + @app.post("/api/admin/access/requests/{username}/approve") async def approve_access_request( username: str, diff --git a/tests/unit/app/test_app_admin_routes.py b/tests/unit/app/test_app_admin_routes.py index ecc79f2..c724b85 100644 --- a/tests/unit/app/test_app_admin_routes.py +++ b/tests/unit/app/test_app_admin_routes.py @@ -443,3 +443,39 @@ def test_retry_access_request_reports_update_failure(monkeypatch) -> None: resp = client.post("/api/access/requests/REQ1/retry") assert resp.status_code == 502 + +def test_hermes_autotriage_can_be_run_on_demand(monkeypatch) -> None: + """The cron tick is once a minute; a demo should not wait for it.""" + + ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) + client = _client(monkeypatch, ctx) + calls = [] + monkeypatch.setattr( + app_module, + "run_hermes_autotriage", + lambda storage: calls.append(storage) or {"status": "ok", "jobs": {}}, + ) + + admin_run = client.post( + "/api/admin/hermes/autotriage/run", + headers={"Authorization": "Bearer token"}, + ) + internal_run = client.post("/api/internal/hermes/autotriage/run") + + assert admin_run.status_code == 200 + assert internal_run.status_code == 200 + assert admin_run.json()["status"] == "ok" + assert len(calls) == 2 + + +def test_hermes_autotriage_on_demand_requires_admin(monkeypatch) -> None: + ctx = AuthContext(username="nobody", email="", groups=[], claims={}) + client = _client(monkeypatch, ctx) + monkeypatch.setattr(app_module, "run_hermes_autotriage", lambda storage: {"status": "ok"}) + + resp = client.post( + "/api/admin/hermes/autotriage/run", + headers={"Authorization": "Bearer token"}, + ) + + assert resp.status_code == 403