1a9374aa52
Adds an unauthenticated GET /api/health endpoint returning JSON
{version, status} for ops tooling. version is the running framework
version (read from VERSION at process start, cached as a module-level
constant in backend/app/health.py); status is "ok" with HTTP 200 in
v1, with the "degraded" / 503 path reserved in the response shape for
future degradation conditions.
The structural value is the version-match check: a deploy control
panel (flotilla, in particular) polls the endpoint after
`systemctl restart` reports active and verifies the returned version
equals the tag just deployed, catching the failure mode where a
restart did not pick up the new code.
Patch-shaped per SPEC.md §20.2 — no operator action required, no env
vars, no schema changes. The CHANGELOG carries one MAY-language step
naming the optional monitoring affordance.
SPEC.md §17 now lists the endpoint; the §19.2 candidate-topic entry
records the topic's settlement (version source = VERSION file at
import time; degraded = reserved v1 scaffold; CHANGELOG = patch shape
with MAY-language).
Tests: backend/tests/test_health.py (3 tests — 200/payload shape,
unauthenticated, version-matches-VERSION). Full suite 128/128 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Integration tests for the §17 `/api/health` endpoint.
|
|
|
|
The endpoint is the structural framework-side dependency for the
|
|
flotilla deploy control panel — see SPEC.md §17 and the §19.2
|
|
candidate-topic settlement. The tests cover the contract: HTTP 200,
|
|
JSON `{version, status}`, `version` matches `VERSION` at the repo
|
|
root, `status` is `"ok"` in v1, and no auth gate is in front of it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from test_propose_vertical import ( # noqa: F401
|
|
app_with_fake_gitea,
|
|
tmp_env,
|
|
)
|
|
|
|
|
|
_VERSION_PATH = Path(__file__).resolve().parents[2] / "VERSION"
|
|
|
|
|
|
def test_health_returns_version_and_ok_status(app_with_fake_gitea):
|
|
from fastapi.testclient import TestClient
|
|
|
|
app, _fake = app_with_fake_gitea
|
|
expected_version = _VERSION_PATH.read_text(encoding="utf-8").strip()
|
|
with TestClient(app) as client:
|
|
r = client.get("/api/health")
|
|
assert r.status_code == 200, r.text
|
|
payload = r.json()
|
|
assert payload == {"version": expected_version, "status": "ok"}
|
|
|
|
|
|
def test_health_is_unauthenticated(app_with_fake_gitea):
|
|
"""flotilla polls /api/health without credentials; the endpoint
|
|
must not require a session cookie. The §17 entry names it
|
|
unauthenticated by design — no PII in the payload, version-match
|
|
is the whole structural value, and gating it would force ops
|
|
tooling to hold a long-lived token for a check that needs none.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
app, _fake = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
# No sign-in step. A fresh client has no session cookie.
|
|
r = client.get("/api/health")
|
|
assert r.status_code == 200, r.text
|
|
assert "version" in r.json()
|
|
|
|
|
|
def test_health_version_matches_VERSION_file(app_with_fake_gitea):
|
|
"""The §20.1 invariant: VERSION at the repo root is the canonical
|
|
source. The endpoint reports the same string verbatim, so flotilla's
|
|
post-flight version-match check can compare against the tag
|
|
operators just deployed without a transform step.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
app, _fake = app_with_fake_gitea
|
|
raw = _VERSION_PATH.read_text(encoding="utf-8")
|
|
expected = raw.strip()
|
|
# The file itself carries no leading "v" per §20.1, and the endpoint
|
|
# mirrors that — a flotilla check comparing against `v0.2.3` would
|
|
# need to strip the prefix on its side, not ours.
|
|
assert not expected.startswith("v"), "VERSION must not carry a leading v per §20.1"
|
|
with TestClient(app) as client:
|
|
r = client.get("/api/health")
|
|
assert r.json()["version"] == expected
|