Files
ServerKit/backend/tests/test_api_surface_inventory.py
Juan DenisandClaude Fable 5 0fe6d090fd test(api): pin the served API surface to a committed inventory
OpenAPIService.generate_spec() existed but nothing snapshotted it, so a
route rename/removal/method change shipped invisibly to API-key
consumers, the CLI, and agents. docs/API_SURFACE.md now pins the sorted
METHOD /path inventory (1418 operations) with the same
regenerate-and-review contract as docs/MIGRATION_INVENTORY.md: an
intentional surface change regenerates the doc in the same commit
(SERVERKIT_UPDATE_API_SURFACE=1 pytest
tests/test_api_surface_inventory.py) so the diff is the review.
Method+path only on purpose — summaries and schemas churn without
breaking anyone. Named *_inventory so preflight's globs run it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 05:03:40 -04:00

76 lines
3.1 KiB
Python

"""The committed API surface must match the served one (plan 82 §G.2).
OpenAPIService.generate_spec() existed but nothing snapshotted it, so a
breaking route change (rename, removal, method change) shipped invisibly to
API-key consumers, the CLI, and agents. This pins the sorted
``METHOD /path`` inventory of the spec against ``docs/API_SURFACE.md`` —
the same regenerate-and-review contract as docs/MIGRATION_INVENTORY.md:
an intentional surface change regenerates the doc in the same commit, so
every add/remove/method change is a visible reviewed diff::
SERVERKIT_UPDATE_API_SURFACE=1 pytest tests/test_api_surface_inventory.py
Deliberately method+path only: summaries and schemas churn without breaking
anyone; the surface is what consumers dial.
"""
import os
HERE = os.path.dirname(os.path.abspath(__file__))
DOC = os.path.join(os.path.dirname(os.path.dirname(HERE)),
'docs', 'API_SURFACE.md')
HEADER = (
'# ServerKit API surface\n'
'\n'
'Generated from OpenAPIService.generate_spec() — do not edit by hand.\n'
'Regenerate (backend/):\n'
'`SERVERKIT_UPDATE_API_SURFACE=1 pytest tests/test_api_surface_inventory.py`\n'
'\n'
)
def _measured_surface(app):
from app.services.openapi_service import OpenAPIService
with app.app_context(), app.test_request_context():
spec = OpenAPIService.generate_spec()
lines = []
for path, ops in spec.get('paths', {}).items():
for method in ops:
if method.upper() in ('GET', 'POST', 'PUT', 'PATCH', 'DELETE',
'HEAD', 'OPTIONS'):
lines.append(f'{method.upper()} {path}')
return sorted(set(lines))
def test_committed_surface_matches_served_surface(app):
measured = _measured_surface(app)
assert len(measured) > 300, (
f'only {len(measured)} operations measured — generate_spec() or the '
f'extraction broke; fix that before trusting this gate')
rendered = HEADER + '\n'.join(f'- `{line}`' for line in measured) + '\n'
if os.environ.get('SERVERKIT_UPDATE_API_SURFACE') == '1':
with open(DOC, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(rendered)
assert os.path.exists(DOC), (
'docs/API_SURFACE.md missing — generate it with '
'SERVERKIT_UPDATE_API_SURFACE=1 pytest '
'tests/test_api_surface_inventory.py')
with open(DOC, encoding='utf-8') as fh:
committed = fh.read()
committed_lines = {line[3:-1] for line in committed.splitlines()
if line.startswith('- `') and line.endswith('`')}
measured_lines = set(measured)
added = sorted(measured_lines - committed_lines)
removed = sorted(committed_lines - measured_lines)
assert not added and not removed, (
'API surface changed but docs/API_SURFACE.md was not regenerated.\n'
+ (f' new operations: {added[:20]}\n' if added else '')
+ (f' gone operations: {removed[:20]}\n' if removed else '')
+ 'If intentional, regenerate in this commit: '
'SERVERKIT_UPDATE_API_SURFACE=1 pytest '
'tests/test_api_surface_inventory.py — the diff is the review.')