mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 00:27:11 +09:00
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>
117 lines
4.4 KiB
Python
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'))
|