Files
ServerKit/backend/tests/test_sdk_contract.py
Juan DenisandClaude Opus 5 9c0ae9e957 feat(extensions): enforce the SDK gate at install; localize the GUI builtin
Follow-up audit of the extension surface, across all of them rather than one.

THE COMPATIBILITY HOLE. Two places documented an install-time `sdk_version`
check as existing -- app/utils/sdk.py's docstring ("checked at install (manifest
lint) and at load") and the runtime loader's comment, which named
`_assert_manifest_sdk_compatible` -- but `sdk_version_satisfies` had ZERO
production callers; only its own unit test. Consequences split by extension
kind: a runtime ESM extension installed cleanly and failed later at the load
gate, while a builtin/in-repo extension never passes that gate at all, so its
range went unchecked everywhere. The function now exists and runs at install,
beside the min/max_panel_version check, and refuses with what the operator
needs to act: what the extension wants, what this panel offers. Fail-open on a
missing range, matching the load gate's 'grace'. Proved both directions.

This matters now because SDK 1.3.0 added the localization surface and made
i18next/react-i18next shared singletons: an extension that externalizes them
needs a panel at 1.3.0+. Pinning `^1.3.0` is what turns a white screen on an
older panel into a readable message. The bump is additive, so anything pinned
to ^1.2.0 keeps installing and loading. Documented as a version table.

THE AUDIT. Measured, not assumed -- both censuses now take `--root` so they can
run against any extension repo (and refuse `--update` there, since the ceilings
belong to the panel):

  2,105 untranslated literals across 13 extension repos (WordPress alone 1,099
  in 44 files), plus 78 direct toLocale*/Intl calls that follow the BROWSER's
  locale (WordPress 56). None of it was visible to any check before, because
  every census stopped at frontend/src.

serverkit-gui -- the one builtin with real UI -- is converted as the worked
example, importing useTranslation/useFormat from 'serverkit-sdk' exactly as a
third-party extension would. That also closes its own toLocaleTimeString.
Ceiling 8055 -> 8034, intl door 124 -> 123.

The probe now asserts the claim the SDK surface rests on: an extension
resolving i18next through the shared vendor gets the HOST's INITIALISED
instance, and an extension-style t() returns Spanish. A second instance would
be uninitialised and every t() in that extension would render English forever
with nothing reporting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:31:42 -04:00

117 lines
4.4 KiB
Python

"""SDK compatibility contract (plan 25 Phase 1 #1/#2).
Locks three things:
- the backend SDK_VERSION mirror matches the frontend `SDK_VERSION` export,
- GET /api/v1/plugins/contributions reports it,
- the semver-range gate (`sdk_version_satisfies`) behaves.
"""
import os
import re
import pytest
from app.utils.sdk import SDK_VERSION, sdk_version_satisfies
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_SDK_JS = os.path.join(_REPO_ROOT, 'frontend', 'src', 'plugins', 'sdk', 'index.js')
def test_backend_mirror_matches_frontend_sdk_version():
"""backend/app/utils/sdk.py SDK_VERSION must equal the JS export, or the
runtime `sdk_version` report lies to extensions."""
with open(_SDK_JS, 'r', encoding='utf-8') as f:
src = f.read()
m = re.search(r'export\s+const\s+SDK_VERSION\s*=\s*[\'"]([^\'"]+)[\'"]', src)
assert m, 'SDK_VERSION export not found in frontend/src/plugins/sdk/index.js'
assert m.group(1) == SDK_VERSION, (
f'SDK version drift: JS={m.group(1)!r} backend={SDK_VERSION!r}. Update both in lock-step.'
)
def test_contributions_reports_sdk_version(client, auth_headers):
resp = client.get('/api/v1/plugins/contributions', headers=auth_headers)
assert resp.status_code == 200
body = resp.get_json()
assert body.get('sdk_version') == SDK_VERSION
assert isinstance(body.get('frontends'), dict)
@pytest.mark.parametrize('range_str,current,expected', [
('', '1.0.0', True),
(None, '1.0.0', True),
('*', '9.9.9', True),
('1.0.0', '1.0.0', True),
('1.0.0', '1.0.1', False),
('^1.0.0', '1.4.2', True),
('^1.0.0', '2.0.0', False),
('^1.2.0', '1.1.0', False),
('~1.2.0', '1.2.9', True),
('~1.2.0', '1.3.0', False),
('>=1.0.0', '1.0.0', True),
('>=1.0.0', '0.9.0', False),
('>=1.0.0,<2.0.0', '1.5.0', True),
('>=1.0.0 <2.0.0', '2.0.0', False),
('^0.1.0', '0.1.5', True),
('^0.1.0', '0.2.0', False),
])
def test_sdk_version_satisfies(range_str, current, expected):
assert sdk_version_satisfies(range_str, current) is expected
def test_sdk_version_satisfies_defaults_to_panel():
assert sdk_version_satisfies(f'^{SDK_VERSION.split(".")[0]}.0.0') is True
# --- install-time SDK gate -------------------------------------------------
#
# Both this module's docstring and the frontend loader claimed the range was
# "checked at install" long before anything called it. The consequences split
# by extension kind: a runtime ESM extension installed fine and failed later at
# the load gate, while a builtin/in-repo extension never passes that gate at
# all, so its range went unchecked everywhere.
def _manifest(**overrides):
base = {'name': 'demo-ext', 'display_name': 'Demo Extension', 'version': '1.0.0'}
base.update(overrides)
return base
def test_install_refuses_an_extension_built_for_a_newer_sdk():
from app.services.plugin_service import _assert_manifest_sdk_compatible
major = int(SDK_VERSION.split('.')[0])
with pytest.raises(ValueError) as excinfo:
_assert_manifest_sdk_compatible(_manifest(sdk_version=f'^{major + 1}.0.0'))
message = str(excinfo.value)
# The operator has to be able to act on it: what it needs, what we have.
assert 'Demo Extension' in message
assert SDK_VERSION in message
assert f'^{major + 1}.0.0' in message
def test_install_allows_a_satisfied_range():
from app.services.plugin_service import _assert_manifest_sdk_compatible
major = int(SDK_VERSION.split('.')[0])
_assert_manifest_sdk_compatible(_manifest(sdk_version=f'^{major}.0.0'))
@pytest.mark.parametrize('value', [None, '', ' ', '*'])
def test_install_fails_open_without_a_pinned_range(value):
"""Deliberate: matches the load gate's 'grace'. An extension that pins
nothing installs anywhere rather than being blocked by a field its author
never filled in."""
from app.services.plugin_service import _assert_manifest_sdk_compatible
manifest = _manifest() if value is None else _manifest(sdk_version=value)
_assert_manifest_sdk_compatible(manifest)
def test_current_sdk_surface_is_reachable_by_a_caret_pin():
"""An extension pinning ^1.3.0 (the localization surface) must install on
this panel — the pin authors are told to use in docs/EXTENSIONS.md."""
from app.services.plugin_service import _assert_manifest_sdk_compatible
_assert_manifest_sdk_compatible(_manifest(sdk_version='^1.3.0'))