Files
2026-07-13 13:12:33 +08:00

223 lines
9.0 KiB
Python

"""Wire-contract freeze for the ``onboarding.catalog`` RPC payload.
The catalog is consumed by the Web UI onboarding wizard and by external
control clients, so its key names are a public protocol contract (see
CLAUDE.md: public RPC field names are stable). These tests pin today's
exact key sets:
- Renaming or removing any frozen key is a contract break and must fail here.
- Adding a key is allowed, but for the exact-key-set assertions below it
requires deliberately updating the frozen set in this file — that friction
is the point: additions to the wire surface should be a conscious decision.
Everything asserted here is built from pure in-process catalog builders
(``provider_catalog_payload`` / ``router_catalog_payload`` and the
``onboarding.catalog`` handler); no network, credentials, or gateway boot.
"""
from __future__ import annotations
from opensquilla.gateway import rpc_onboarding
from opensquilla.gateway.config import ROUTER_TIER_PROFILE_IDS
from opensquilla.gateway.rpc import RpcContext
from opensquilla.onboarding.provider_specs import provider_catalog_payload
from opensquilla.onboarding.router_specs import router_catalog_payload
# Top-level catalog sections the Web UI switches on. Exact set: a renamed or
# dropped section silently blanks a wizard page.
CATALOG_TOP_LEVEL_KEYS = frozenset(
{
"providers",
"channels",
"searchProviders",
"routerProfiles",
"memoryEmbeddingProviders",
"imageGenerationProviders",
"audioProviders",
}
)
# Per-provider entry shape rendered by the provider picker.
PROVIDER_ENTRY_KEYS = frozenset(
{
"providerId",
"label",
"backend",
"providerKind",
"runtimeSupported",
"verification",
"envKey",
"defaultBaseUrl",
"requiresApiKey",
"requiresBaseUrl",
"routerSupported",
"deployment",
"blocking",
"canProbe",
"readmeScenarios",
"whatYouNeed",
"defaultDirectModel",
# Deliberate preset-surface additions: each provider entry now carries
# its registry preset(s) and the preset-declared default model. Adding
# these to the frozen set is the conscious decision the friction forces.
"defaultModel",
"presets",
"capabilities",
"fields",
}
)
# Per-preset entry shape rendered by the preset picker (exactly one preset per
# provider today; the wire is a list to allow future multi-preset providers).
PROVIDER_PRESET_KEYS = frozenset(
{"presetId", "label", "description", "synthesized", "defaultModel", "tiers"}
)
# Per-field spec shape the wizard uses to render setup form inputs.
PROVIDER_FIELD_KEYS = frozenset(
{
"name",
"label",
"type",
"required",
"default",
"description",
"secret",
}
)
# Router catalog shapes: profile list + per-tier route cards.
ROUTER_CATALOG_TOP_LEVEL_KEYS = frozenset({"defaultTier", "textTiers", "modes", "profiles"})
ROUTER_MODE_KEYS = frozenset({"mode", "label", "description"})
ROUTER_PROFILE_KEYS = frozenset({"profileId", "providerId", "label", "tiers"})
ROUTER_TIER_PAYLOAD_KEYS = frozenset(
{"provider", "model", "description", "thinkingLevel", "supportsImage"}
)
# The canonical text tiers (c0-c3, per router_tiers.py) and the nine tier
# profiles shipped today. Hardcoded on purpose: dropping a profile or renaming
# a tier changes what onboarding clients can select, so it must fail here even
# if the source constant is edited in the same change.
FROZEN_TEXT_TIERS = ("c0", "c1", "c2", "c3")
FROZEN_ROUTER_PROFILE_IDS = frozenset(
{
"byteplus",
"dashscope",
"deepseek",
"gemini",
"moonshot",
"openai",
"openrouter",
"volcengine",
"zhipu",
}
)
# Curated presets that are NOT persistable tier_profile ids: packaged tier
# data applied as inline tiers (synthesized=False on the wire, but the id
# stays outside FROZEN_ROUTER_PROFILE_IDS).
FROZEN_CURATED_INLINE_PRESET_IDS = frozenset({"tokenrhythm"})
async def test_onboarding_catalog_top_level_sections_are_frozen() -> None:
# The handler ignores params and only reads pure catalog builders, so a
# bare RpcContext is enough — no gateway boot required.
payload = await rpc_onboarding._onboarding_catalog(None, RpcContext(conn_id="contract"))
assert set(payload) == CATALOG_TOP_LEVEL_KEYS
def test_provider_catalog_entry_keys_are_frozen() -> None:
entries = provider_catalog_payload()
assert entries, "provider catalog must not be empty"
for entry in entries:
assert set(entry) == PROVIDER_ENTRY_KEYS, entry["providerId"]
assert entry["fields"], entry["providerId"]
for field in entry["fields"]:
assert set(field) == PROVIDER_FIELD_KEYS, (entry["providerId"], field.get("name"))
# The default provider must stay addressable by its shipped id.
provider_ids = {entry["providerId"] for entry in entries}
assert "openrouter" in provider_ids
def test_provider_catalog_preset_keys_are_frozen() -> None:
entries = provider_catalog_payload()
for entry in entries:
presets = entry["presets"]
# Exactly one registry preset per runtime-supported provider today —
# widening to several curated presets is an allowed (deliberate)
# evolution, but silently losing the preset is a break.
assert len(presets) == 1, entry["providerId"]
for preset in presets:
assert set(preset) == PROVIDER_PRESET_KEYS, entry["providerId"]
# Preset ids are provider-bound (both packaged and synthesized
# preset ids equal their provider id); clients echo presetId back
# verbatim through onboarding.provider.configure.
assert preset["presetId"] == entry["providerId"]
assert isinstance(preset["synthesized"], bool), entry["providerId"]
# Preset tiers reuse the router-catalog tier payload shape so the
# same client component renders both.
tiers = preset["tiers"]
assert set(FROZEN_TEXT_TIERS) <= set(tiers), entry["providerId"]
for tier_name, tier in tiers.items():
assert set(tier) == ROUTER_TIER_PAYLOAD_KEYS, (entry["providerId"], tier_name)
# The entry-level defaultModel mirrors the (single) preset's.
assert entry["defaultModel"] == presets[0]["defaultModel"], entry["providerId"]
def test_provider_catalog_preset_synthesized_flag_tracks_packaged_presets() -> None:
# The legacy nine plus the curated-inline set ship packaged (curated)
# presets; everything else is synthesized. A packaged preset silently
# degrading to synthesized (or vice versa) changes what onboarding
# clients offer to apply.
packaged = FROZEN_ROUTER_PROFILE_IDS | FROZEN_CURATED_INLINE_PRESET_IDS
entries = {entry["providerId"]: entry for entry in provider_catalog_payload()}
for provider_id, entry in entries.items():
expected_synthesized = provider_id not in packaged
assert entry["presets"][0]["synthesized"] is expected_synthesized, provider_id
def test_router_catalog_top_level_shape_is_frozen() -> None:
payload = router_catalog_payload()
assert set(payload) == ROUTER_CATALOG_TOP_LEVEL_KEYS
# Tier names are canonical c0-c3 and c1 is the default route; clients
# (tier pills, onboarding tier tables) key directly off these strings.
assert payload["textTiers"] == list(FROZEN_TEXT_TIERS)
assert payload["defaultTier"] == "c1"
def test_router_catalog_modes_are_frozen() -> None:
payload = router_catalog_payload()
modes = payload["modes"]
for mode in modes:
assert set(mode) == ROUTER_MODE_KEYS
# Mode ids are sent back verbatim by onboarding.router.configure clients.
# "custom" is a deliberate addition: the provider-agnostic generalization
# of openrouter-mix accepted for any active provider.
assert {mode["mode"] for mode in modes} == {
"recommended",
"openrouter-mix",
"custom",
"disabled",
}
def test_router_catalog_profiles_and_tier_payloads_are_frozen() -> None:
payload = router_catalog_payload()
profiles = payload["profiles"]
profile_ids = {profile["profileId"] for profile in profiles}
assert profile_ids == FROZEN_ROUTER_PROFILE_IDS
# Keep the frozen wire set in lockstep with the source-of-truth constant;
# if this fails, either the wire or the constant changed without the other.
assert profile_ids == set(ROUTER_TIER_PROFILE_IDS)
for profile in profiles:
assert set(profile) == ROUTER_PROFILE_KEYS, profile.get("profileId")
tiers = profile["tiers"]
# Every profile must route all four text tiers; image_model is the
# only other tier slot exposed to onboarding.
assert set(FROZEN_TEXT_TIERS) <= set(tiers), profile["profileId"]
assert set(tiers) <= set(FROZEN_TEXT_TIERS) | {"image_model"}, profile["profileId"]
for tier_name, tier in tiers.items():
assert set(tier) == ROUTER_TIER_PAYLOAD_KEYS, (profile["profileId"], tier_name)