718 lines
28 KiB
Python
718 lines
28 KiB
Python
"""Tests for the run runtime snapshot service (design doc 5.2, 8.1, 8.2).
|
||
|
||
Covers creation with inherit semantics, the selection-hash idempotency
|
||
rules, bind/abort state transitions, binding validation on reads, TTLs,
|
||
adapter-spec revalidation, and credential resolution against frozen
|
||
revisions — with no in-process secret caching.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from EvoScientist.model_registry.adapters import adapter_specs, find_adapter_spec
|
||
from EvoScientist.model_registry.errors import (
|
||
ADAPTER_NOT_SUPPORTED,
|
||
MODEL_REGISTRY_NOT_READY,
|
||
RUN_CREDENTIAL_REVISION_UNAVAILABLE,
|
||
RUN_REQUEST_CONFLICT,
|
||
SNAPSHOT_ALREADY_BOUND,
|
||
SNAPSHOT_EXPIRED,
|
||
SNAPSHOT_NOT_FOUND,
|
||
UNSUPPORTED_RUNTIME_PARAMETER,
|
||
ModelRegistryError,
|
||
)
|
||
from EvoScientist.model_registry.hashing import configuration_hash
|
||
from EvoScientist.model_registry.resolver import ModelRegistryResolver
|
||
from EvoScientist.model_registry.schemas import (
|
||
CredentialWrite,
|
||
ModelRef,
|
||
RegistryV4,
|
||
SamplingOverride,
|
||
)
|
||
from EvoScientist.model_registry.snapshots import (
|
||
BOUND_RETENTION_SECONDS,
|
||
PREPARED_TTL_SECONDS,
|
||
SnapshotCreateRequest,
|
||
SnapshotService,
|
||
compute_selection_hash,
|
||
config_for_role,
|
||
public_snapshot_view,
|
||
)
|
||
from EvoScientist.model_registry.store import ModelRuntimeStore
|
||
|
||
ZHIPU_REF = ModelRef(provider_id="zhipu-glm", model_key="glm-5.2")
|
||
OLLAMA_REF = ModelRef(provider_id="local-ollama", model_key="qwen3")
|
||
SECRET = "sk-live-9876abcd"
|
||
|
||
|
||
def _model_runtime(**overrides):
|
||
payload = {
|
||
"limit_mode": "combined",
|
||
"context_window_tokens": 1048576,
|
||
"max_input_tokens": None,
|
||
"max_output_tokens": 32768,
|
||
"min_effective_input_tokens": 8192,
|
||
"fixed_system_reserve_tokens": 4096,
|
||
"fixed_tools_reserve_tokens": 8192,
|
||
"fixed_attachments_reserve_tokens": 4096,
|
||
"limits_status": "confirmed",
|
||
"limits_source": "provider",
|
||
"temperature": None,
|
||
"top_p": None,
|
||
"reasoning_effort": "auto",
|
||
"declared_capabilities": {
|
||
"tools": True,
|
||
"vision": False,
|
||
"structured_output": True,
|
||
},
|
||
}
|
||
payload.update(overrides)
|
||
return payload
|
||
|
||
|
||
def _zhipu_provider():
|
||
return {
|
||
"id": "zhipu-glm",
|
||
"name": "Zhipu GLM",
|
||
"adapter": "openai-compatible",
|
||
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
||
"auth": {"mode": "api_key", "credential_id": "zhipu-primary"},
|
||
"enabled": True,
|
||
"runtime": {
|
||
"timeout_seconds": 120,
|
||
"max_retries": 2,
|
||
"default_temperature": 0.7,
|
||
"default_top_p": 0.95,
|
||
"default_reasoning_effort": "auto",
|
||
},
|
||
"models": [
|
||
{
|
||
"key": "glm-5.2",
|
||
"name": "GLM-5.2",
|
||
"upstream_model_id": "glm-5.2",
|
||
"enabled": True,
|
||
"runtime": _model_runtime(),
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
def _ollama_provider():
|
||
return {
|
||
"id": "local-ollama",
|
||
"name": "Local Ollama",
|
||
"adapter": "ollama",
|
||
"base_url": "http://localhost:11434",
|
||
"auth": {"mode": "none", "credential_id": None},
|
||
"enabled": True,
|
||
"runtime": {"timeout_seconds": 120, "max_retries": 2},
|
||
"models": [
|
||
{
|
||
"key": "qwen3",
|
||
"name": "Qwen3",
|
||
"upstream_model_id": "qwen3",
|
||
"enabled": True,
|
||
"runtime": _model_runtime(),
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
def _registry() -> RegistryV4:
|
||
return RegistryV4.model_validate(
|
||
{
|
||
"version": 4,
|
||
"revision": 1,
|
||
"state": "bootstrap",
|
||
"defaults": {
|
||
"primary": {"provider_id": "zhipu-glm", "model_key": "glm-5.2"},
|
||
},
|
||
"providers": [_zhipu_provider(), _ollama_provider()],
|
||
}
|
||
)
|
||
|
||
|
||
def _verify(store, registry, provider_id, model_key):
|
||
provider = registry.find_provider(provider_id)
|
||
model = provider.find_model(model_key)
|
||
spec = find_adapter_spec(provider.adapter, model.upstream_model_id)
|
||
store.record_model_verification(
|
||
provider_id=provider_id,
|
||
model_key=model_key,
|
||
configuration_hash=configuration_hash(provider, model),
|
||
credential_revision=1 if provider.auth.credential_id else 0,
|
||
adapter_spec_revision=spec.spec_revision,
|
||
result="passed",
|
||
verified_capabilities={
|
||
"tools": True,
|
||
"vision": False,
|
||
"structured_output": True,
|
||
},
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def store(tmp_path):
|
||
return ModelRuntimeStore(config_dir=tmp_path)
|
||
|
||
|
||
@pytest.fixture
|
||
def active_store(store):
|
||
registry = store.save_registry(
|
||
expected_revision=1,
|
||
registry=_registry(),
|
||
credential_writes=[
|
||
CredentialWrite(credential_id="zhipu-primary", secret_value=SECRET)
|
||
],
|
||
)
|
||
assert registry.state == "active"
|
||
_verify(store, registry, "zhipu-glm", "glm-5.2")
|
||
_verify(store, registry, "local-ollama", "qwen3")
|
||
return store
|
||
|
||
|
||
@pytest.fixture
|
||
def service(active_store):
|
||
return SnapshotService(active_store, ModelRegistryResolver(active_store))
|
||
|
||
|
||
def _request(**overrides):
|
||
payload = {
|
||
"run_request_id": "req-1",
|
||
"thread_id": "thread-1",
|
||
"deployment_id": "local",
|
||
"model_selection_revision": 0,
|
||
"primary": None,
|
||
}
|
||
payload.update(overrides)
|
||
return SnapshotCreateRequest.model_validate(payload)
|
||
|
||
|
||
# SHA-256 of the canonical JSON
|
||
# '{"primary":null,"reasoning_effort":null,"sampling_override":null}' —
|
||
# the inherit selection with no overrides. Pinned as a literal to lock the
|
||
# wire contract.
|
||
INHERIT_SELECTION_HASH = (
|
||
"417399175386cea86c726c1bd2b833bd951066eae2e8f419b99076e9c91724a6"
|
||
)
|
||
|
||
|
||
class TestCreate:
|
||
def test_inherit_resolves_registry_defaults(self, service, active_store):
|
||
creation = service.create(_request(model_selection_revision=4))
|
||
assert creation.created is True
|
||
snapshot = creation.snapshot
|
||
assert snapshot.status == "prepared"
|
||
assert snapshot.langgraph_run_id is None
|
||
payload = snapshot.payload
|
||
assert payload.registry_revision == 2
|
||
assert payload.model_selection_revision == 4
|
||
assert payload.primary.model_ref == ZHIPU_REF
|
||
assert payload.primary.role == "primary"
|
||
|
||
def test_explicit_selection_freezes_primary(self, service):
|
||
creation = service.create(_request(primary=OLLAMA_REF))
|
||
payload = creation.snapshot.payload
|
||
assert payload.primary.model_ref == OLLAMA_REF
|
||
|
||
def test_reasoning_effort_override_freezes_into_primary(self, active_store):
|
||
# glm-5.2's model-specific contract rejects reasoning overrides, so
|
||
# add a generic-contract model on the same provider to exercise the
|
||
# override path (disabled → verify → enable, the standard flow).
|
||
store = active_store
|
||
registry = store.load_registry()
|
||
payload = registry.model_dump(mode="json")
|
||
payload["providers"][0]["models"].append(
|
||
{
|
||
"key": "glm-air",
|
||
"name": "GLM Air",
|
||
"upstream_model_id": "glm-air",
|
||
"enabled": False,
|
||
"runtime": _model_runtime(),
|
||
}
|
||
)
|
||
registry = store.save_registry(
|
||
expected_revision=registry.revision,
|
||
registry=RegistryV4.model_validate(payload),
|
||
)
|
||
_verify(store, registry, "zhipu-glm", "glm-air")
|
||
payload = registry.model_dump(mode="json")
|
||
payload["providers"][0]["models"][1]["enabled"] = True
|
||
registry = store.save_registry(
|
||
expected_revision=registry.revision,
|
||
registry=RegistryV4.model_validate(payload),
|
||
)
|
||
service = SnapshotService(store, ModelRegistryResolver(store))
|
||
creation = service.create(
|
||
_request(
|
||
primary={"provider_id": "zhipu-glm", "model_key": "glm-air"},
|
||
reasoning_effort="high",
|
||
)
|
||
)
|
||
result = creation.snapshot.payload
|
||
assert result.primary.request_options.reasoning_effort == "high"
|
||
|
||
def test_reasoning_effort_override_rejected_by_model_contract(self, service):
|
||
# glm-5.2's model-specific contract does not accept reasoning_effort.
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.create(_request(reasoning_effort="high"))
|
||
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
||
|
||
def test_reasoning_effort_override_changes_selection_hash(self, service):
|
||
plain = service.create(_request())
|
||
assert compute_selection_hash(None, "low") != plain.snapshot.selection_hash
|
||
assert compute_selection_hash(None, "low") != INHERIT_SELECTION_HASH
|
||
# The hash covers the raw field, so an explicit ``auto`` differs from
|
||
# no override; both resolve to the same frozen configuration.
|
||
assert compute_selection_hash(None, "auto") != INHERIT_SELECTION_HASH
|
||
|
||
def test_reasoning_effort_override_rejected_for_unsupported_adapter(
|
||
self, service
|
||
):
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.create(_request(primary=OLLAMA_REF, reasoning_effort="high"))
|
||
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
||
|
||
def test_sampling_override_freezes_and_omits_the_other(self, active_store):
|
||
# glm-5.2's model-specific contract caps temperature at 1, so add a
|
||
# generic-contract model on the same provider to exercise the
|
||
# override path (disabled → verify → enable, the standard flow).
|
||
store = active_store
|
||
registry = store.load_registry()
|
||
payload = registry.model_dump(mode="json")
|
||
payload["providers"][0]["models"].append(
|
||
{
|
||
"key": "glm-air",
|
||
"name": "GLM Air",
|
||
"upstream_model_id": "glm-air",
|
||
"enabled": False,
|
||
"runtime": _model_runtime(),
|
||
}
|
||
)
|
||
registry = store.save_registry(
|
||
expected_revision=registry.revision,
|
||
registry=RegistryV4.model_validate(payload),
|
||
)
|
||
_verify(store, registry, "zhipu-glm", "glm-air")
|
||
payload = registry.model_dump(mode="json")
|
||
payload["providers"][0]["models"][1]["enabled"] = True
|
||
registry = store.save_registry(
|
||
expected_revision=registry.revision,
|
||
registry=RegistryV4.model_validate(payload),
|
||
)
|
||
service = SnapshotService(store, ModelRegistryResolver(store))
|
||
ref = {"provider_id": "zhipu-glm", "model_key": "glm-air"}
|
||
|
||
# temperature override: top_p (provider default 0.95) is omitted.
|
||
creation = service.create(
|
||
_request(
|
||
primary=ref,
|
||
sampling_override={"kind": "temperature", "value": 1.1},
|
||
)
|
||
)
|
||
options = creation.snapshot.payload.primary.request_options
|
||
assert options.temperature == 1.1
|
||
assert options.top_p is None
|
||
|
||
# top_p override: temperature (provider default 0.7) is omitted.
|
||
creation = service.create(
|
||
_request(
|
||
run_request_id="req-2",
|
||
primary=ref,
|
||
sampling_override={"kind": "top_p", "value": 0.5},
|
||
)
|
||
)
|
||
options = creation.snapshot.payload.primary.request_options
|
||
assert options.top_p == 0.5
|
||
assert options.temperature is None
|
||
|
||
def test_sampling_override_value_range_enforced(self):
|
||
for bad in (
|
||
{"kind": "temperature", "value": -0.1},
|
||
{"kind": "temperature", "value": 2.1},
|
||
{"kind": "temperature", "value": float("nan")},
|
||
{"kind": "top_p", "value": 0},
|
||
{"kind": "top_p", "value": 1.01},
|
||
):
|
||
with pytest.raises(ValidationError):
|
||
SamplingOverride.model_validate(bad)
|
||
|
||
def test_overrides_rejected_outside_model_contract(self, service):
|
||
# glm-5.2 走模型专属契约:temperature 上限 1;1.5 通过请求模型
|
||
# (0–2) 校验但被 adapter 契约拒绝。
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.create(
|
||
_request(sampling_override={"kind": "temperature", "value": 1.5})
|
||
)
|
||
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
||
|
||
def test_sampling_override_changes_selection_hash(self, service):
|
||
plain = service.create(_request())
|
||
assert compute_selection_hash(
|
||
None, None, SamplingOverride(kind="temperature", value=0.5)
|
||
) != plain.snapshot.selection_hash
|
||
assert compute_selection_hash(
|
||
None, None, SamplingOverride(kind="top_p", value=0.9)
|
||
) != plain.snapshot.selection_hash
|
||
# 未覆盖(None)与缺省一致:不传覆盖时 hash 与旧语义相同。
|
||
assert compute_selection_hash(None, None, None) == (
|
||
plain.snapshot.selection_hash
|
||
)
|
||
|
||
def test_bootstrap_registry_is_not_ready(self, store):
|
||
service = SnapshotService(store, ModelRegistryResolver(store))
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.create(_request())
|
||
assert excinfo.value.code == MODEL_REGISTRY_NOT_READY
|
||
assert excinfo.value.http_status == 422
|
||
|
||
def test_selection_hash_uses_pre_resolution_semantics(self, service):
|
||
creation = service.create(_request())
|
||
snapshot = creation.snapshot
|
||
# The inherit hash is pinned to a known literal (computed offline
|
||
# from the canonical JSON) so the contract can't drift together with
|
||
# a reimplemented expectation.
|
||
assert snapshot.selection_hash == INHERIT_SELECTION_HASH
|
||
assert compute_selection_hash(None) == INHERIT_SELECTION_HASH
|
||
# An explicit selection equal to the default still hashes differently.
|
||
assert compute_selection_hash(ZHIPU_REF) != INHERIT_SELECTION_HASH
|
||
|
||
def test_selection_hash_ignores_selection_revision(self, service):
|
||
first = service.create(_request(model_selection_revision=4))
|
||
second = service.create(_request(model_selection_revision=9))
|
||
assert second.created is False
|
||
assert second.snapshot.snapshot_id == first.snapshot.snapshot_id
|
||
|
||
def test_idempotent_same_selection_returns_original(self, service):
|
||
first = service.create(_request())
|
||
second = service.create(_request())
|
||
assert first.created is True
|
||
assert second.created is False
|
||
assert second.snapshot.snapshot_id == first.snapshot.snapshot_id
|
||
|
||
def test_conflicting_selection_same_triplet(self, service):
|
||
service.create(_request())
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.create(_request(primary=ZHIPU_REF))
|
||
assert excinfo.value.code == RUN_REQUEST_CONFLICT
|
||
assert excinfo.value.http_status == 409
|
||
|
||
def test_recreate_after_expired(self, service, active_store):
|
||
first = service.create(_request())
|
||
active_store.set_run_snapshot_status(first.snapshot.snapshot_id, "expired")
|
||
second = service.create(_request())
|
||
assert second.created is True
|
||
assert second.snapshot.snapshot_id != first.snapshot.snapshot_id
|
||
|
||
def test_recreate_after_aborted(self, service):
|
||
first = service.create(_request())
|
||
service.abort(first.snapshot.snapshot_id)
|
||
second = service.create(_request())
|
||
assert second.created is True
|
||
assert second.snapshot.snapshot_id != first.snapshot.snapshot_id
|
||
|
||
def test_payload_freezes_credential_revision_without_secret(self, service):
|
||
creation = service.create(_request())
|
||
payload = creation.snapshot.payload
|
||
assert payload.primary.auth_ref.credential_id == "zhipu-primary"
|
||
assert payload.primary.auth_ref.credential_revision == 1
|
||
encoded = json.dumps(payload.model_dump(mode="json"))
|
||
assert SECRET not in encoded
|
||
|
||
def test_prepared_ttl_is_fifteen_minutes(self, service):
|
||
creation = service.create(_request())
|
||
snapshot = creation.snapshot
|
||
assert PREPARED_TTL_SECONDS == 15 * 60
|
||
delta = snapshot.expires_at - snapshot.created_at
|
||
assert PREPARED_TTL_SECONDS - 1 <= delta <= PREPARED_TTL_SECONDS + 1
|
||
|
||
|
||
class TestBind:
|
||
def test_bind_once(self, service):
|
||
creation = service.create(_request())
|
||
before = int(time.time())
|
||
bound = service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
assert bound.status == "bound"
|
||
assert bound.langgraph_run_id == "lg-run-1"
|
||
assert BOUND_RETENTION_SECONDS == 24 * 60 * 60
|
||
delta = bound.expires_at - before
|
||
assert BOUND_RETENTION_SECONDS - 1 <= delta <= BOUND_RETENTION_SECONDS + 1
|
||
|
||
def test_bind_same_run_id_is_idempotent(self, service):
|
||
creation = service.create(_request())
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
again = service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
assert again.status == "bound"
|
||
assert again.langgraph_run_id == "lg-run-1"
|
||
|
||
def test_bind_different_run_id_conflicts(self, service):
|
||
creation = service.create(_request())
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-2")
|
||
assert excinfo.value.code == SNAPSHOT_ALREADY_BOUND
|
||
|
||
def test_bind_expired_snapshot(self, service, active_store):
|
||
creation = service.create(_request())
|
||
active_store.set_run_snapshot_status(creation.snapshot.snapshot_id, "expired")
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
assert excinfo.value.code == SNAPSHOT_EXPIRED
|
||
|
||
def test_bind_aborted_snapshot(self, service):
|
||
creation = service.create(_request())
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
assert excinfo.value.code == SNAPSHOT_EXPIRED
|
||
|
||
def test_bind_unknown_snapshot(self, service):
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.bind("snap-missing", "lg-run-1")
|
||
assert excinfo.value.code == SNAPSHOT_NOT_FOUND
|
||
assert excinfo.value.http_status == 404
|
||
|
||
|
||
class TestAbort:
|
||
def test_abort_prepared(self, service, active_store):
|
||
creation = service.create(_request())
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
row = active_store.get_run_snapshot(creation.snapshot.snapshot_id)
|
||
assert row["status"] == "aborted"
|
||
|
||
def test_abort_is_idempotent(self, service):
|
||
creation = service.create(_request())
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
|
||
def test_abort_bound_conflicts(self, service):
|
||
creation = service.create(_request())
|
||
service.bind(creation.snapshot.snapshot_id, "lg-run-1")
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
assert excinfo.value.code == SNAPSHOT_ALREADY_BOUND
|
||
|
||
def test_abort_expired(self, service, active_store):
|
||
creation = service.create(_request())
|
||
active_store.set_run_snapshot_status(creation.snapshot.snapshot_id, "expired")
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.abort(creation.snapshot.snapshot_id)
|
||
assert excinfo.value.code == SNAPSHOT_EXPIRED
|
||
|
||
def test_abort_unknown(self, service):
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.abort("snap-missing")
|
||
assert excinfo.value.code == SNAPSHOT_NOT_FOUND
|
||
|
||
def test_abort_run_snapshot_store_update_is_conditional(
|
||
self, service, active_store
|
||
):
|
||
creation = service.create(_request())
|
||
snapshot_id = creation.snapshot.snapshot_id
|
||
# The store-level abort only fires on prepared rows.
|
||
assert active_store.abort_run_snapshot(snapshot_id) is True
|
||
assert active_store.abort_run_snapshot(snapshot_id) is False
|
||
|
||
bound = service.create(_request(run_request_id="req-2"))
|
||
service.bind(bound.snapshot.snapshot_id, "lg-run-7")
|
||
assert active_store.abort_run_snapshot(bound.snapshot.snapshot_id) is False
|
||
row = active_store.get_run_snapshot(bound.snapshot.snapshot_id)
|
||
assert row["status"] == "bound"
|
||
assert row["langgraph_run_id"] == "lg-run-7"
|
||
|
||
def test_abort_losing_bind_race_keeps_bound_state(
|
||
self, service, active_store, monkeypatch
|
||
):
|
||
"""A bind committing between abort's read and write must win.
|
||
|
||
The abort read observes ``prepared``; a concurrent bind then commits;
|
||
the abort write must fail its conditional update, re-read, and raise
|
||
``SNAPSHOT_ALREADY_BOUND`` instead of clobbering the bound row (and
|
||
its ``langgraph_run_id``) back to ``aborted``.
|
||
"""
|
||
creation = service.create(_request())
|
||
snapshot_id = creation.snapshot.snapshot_id
|
||
original_get = active_store.get_run_snapshot
|
||
raced = False
|
||
|
||
def get_with_concurrent_bind(sid):
|
||
nonlocal raced
|
||
row = original_get(sid)
|
||
if not raced and row is not None and row["status"] == "prepared":
|
||
raced = True
|
||
assert active_store.bind_run_snapshot(
|
||
sid,
|
||
langgraph_run_id="lg-run-9",
|
||
expires_at=row["expires_at"] + 100,
|
||
)
|
||
return row
|
||
|
||
monkeypatch.setattr(active_store, "get_run_snapshot", get_with_concurrent_bind)
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.abort(snapshot_id)
|
||
assert raced is True
|
||
assert excinfo.value.code == SNAPSHOT_ALREADY_BOUND
|
||
row = active_store.get_run_snapshot(snapshot_id)
|
||
assert row["status"] == "bound"
|
||
assert row["langgraph_run_id"] == "lg-run-9"
|
||
|
||
|
||
class TestGet:
|
||
def test_get_returns_snapshot(self, service):
|
||
creation = service.create(_request())
|
||
snapshot = service.get(
|
||
creation.snapshot.snapshot_id,
|
||
deployment_id="local",
|
||
thread_id="thread-1",
|
||
)
|
||
assert snapshot.snapshot_id == creation.snapshot.snapshot_id
|
||
assert snapshot.payload.primary.model_ref == ZHIPU_REF
|
||
|
||
def test_get_rejects_foreign_thread(self, service):
|
||
creation = service.create(_request())
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.get(
|
||
creation.snapshot.snapshot_id,
|
||
deployment_id="local",
|
||
thread_id="thread-2",
|
||
)
|
||
assert excinfo.value.code == SNAPSHOT_NOT_FOUND
|
||
|
||
def test_get_rejects_foreign_deployment(self, service):
|
||
creation = service.create(_request())
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.get(
|
||
creation.snapshot.snapshot_id,
|
||
deployment_id="other",
|
||
thread_id="thread-1",
|
||
)
|
||
assert excinfo.value.code == SNAPSHOT_NOT_FOUND
|
||
|
||
def test_get_expired_fails(self, service, active_store):
|
||
creation = service.create(_request())
|
||
active_store.set_run_snapshot_status(creation.snapshot.snapshot_id, "expired")
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.get(
|
||
creation.snapshot.snapshot_id,
|
||
deployment_id="local",
|
||
thread_id="thread-1",
|
||
)
|
||
assert excinfo.value.code == SNAPSHOT_EXPIRED
|
||
|
||
def test_get_fails_when_frozen_spec_revision_removed(self, active_store):
|
||
service = SnapshotService(active_store, ModelRegistryResolver(active_store))
|
||
creation = service.create(_request())
|
||
# Simulate a contract upgrade that removed spec_revision 1 entirely.
|
||
bumped = tuple(
|
||
spec.model_copy(update={"spec_revision": 2}) for spec in adapter_specs()
|
||
)
|
||
stale_service = SnapshotService(
|
||
active_store, ModelRegistryResolver(active_store, specs=bumped)
|
||
)
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
stale_service.get(
|
||
creation.snapshot.snapshot_id,
|
||
deployment_id="local",
|
||
thread_id="thread-1",
|
||
)
|
||
assert excinfo.value.code == ADAPTER_NOT_SUPPORTED
|
||
|
||
|
||
class TestCleanupExpired:
|
||
def _insert(self, store, snapshot_id, *, status, expires_at):
|
||
store.insert_run_snapshot(
|
||
snapshot_id=snapshot_id,
|
||
deployment_id="local",
|
||
thread_id="thread-1",
|
||
run_request_id=f"req-{snapshot_id}",
|
||
selection_hash="b" * 64,
|
||
payload={},
|
||
expires_at=expires_at,
|
||
status=status,
|
||
langgraph_run_id="lg-run" if status == "bound" else None,
|
||
)
|
||
|
||
def test_cleanup_marks_due_snapshots_expired(self, store):
|
||
service = SnapshotService(store, ModelRegistryResolver(store))
|
||
now = 1_000_000
|
||
self._insert(store, "snap-p-old", status="prepared", expires_at=now - 1)
|
||
self._insert(store, "snap-b-old", status="bound", expires_at=now - 1)
|
||
self._insert(store, "snap-p-new", status="prepared", expires_at=now + 900)
|
||
expired = service.cleanup_expired(now)
|
||
assert sorted(expired) == ["snap-b-old", "snap-p-old"]
|
||
assert store.get_run_snapshot("snap-p-old")["status"] == "expired"
|
||
assert store.get_run_snapshot("snap-b-old")["status"] == "expired"
|
||
assert store.get_run_snapshot("snap-p-new")["status"] == "prepared"
|
||
|
||
|
||
class TestRoleMapping:
|
||
def test_primary_role_maps_to_frozen_primary(self, service):
|
||
snapshot = service.create(_request()).snapshot
|
||
assert config_for_role(snapshot, "primary").model_ref == ZHIPU_REF
|
||
|
||
def test_unknown_role_rejected(self, service):
|
||
snapshot = service.create(_request()).snapshot
|
||
with pytest.raises(ValueError, match="role"):
|
||
config_for_role(snapshot, "planner")
|
||
|
||
|
||
class TestSnapshotCredentials:
|
||
def test_resolves_frozen_revision(self, service):
|
||
snapshot = service.create(_request()).snapshot
|
||
assert service.resolve_snapshot_credential(snapshot, "primary") == SECRET
|
||
|
||
def test_rotation_keeps_frozen_revision(self, service, active_store):
|
||
snapshot = service.create(_request()).snapshot
|
||
active_store.write_credential_version("zhipu-primary", "sk-rotated-2222")
|
||
assert service.resolve_snapshot_credential(snapshot, "primary") == SECRET
|
||
|
||
def test_destroyed_revision_fails_loudly(self, service, active_store):
|
||
snapshot = service.create(_request()).snapshot
|
||
active_store.retire_credential_version("zhipu-primary", 1)
|
||
with pytest.raises(ModelRegistryError) as excinfo:
|
||
service.resolve_snapshot_credential(snapshot, "primary")
|
||
assert excinfo.value.code == RUN_CREDENTIAL_REVISION_UNAVAILABLE
|
||
|
||
def test_no_in_process_secret_cache(self, service, active_store):
|
||
snapshot = service.create(_request()).snapshot
|
||
# A brand-new store/service pair on the same database still resolves.
|
||
fresh_store = ModelRuntimeStore(config_dir=active_store.config_dir)
|
||
fresh_service = SnapshotService(fresh_store, ModelRegistryResolver(fresh_store))
|
||
assert fresh_service.resolve_snapshot_credential(snapshot, "primary") == SECRET
|
||
|
||
def test_mode_none_returns_empty_string(self, service):
|
||
snapshot = service.create(_request(primary=OLLAMA_REF)).snapshot
|
||
assert service.resolve_snapshot_credential(snapshot, "primary") == ""
|
||
|
||
|
||
class TestPublicView:
|
||
def test_matches_section_8_2_shape(self, service):
|
||
snapshot = service.create(_request()).snapshot
|
||
view = public_snapshot_view(snapshot)
|
||
assert view["snapshot_id"] == snapshot.snapshot_id
|
||
assert view["registry_revision"] == 2
|
||
primary = view["primary"]
|
||
assert primary["provider_id"] == "zhipu-glm"
|
||
assert primary["model_key"] == "glm-5.2"
|
||
assert primary["adapter_spec_revision"] == 1
|
||
assert primary["runtime"] == {
|
||
"max_output_tokens": 32768,
|
||
"temperature": 0.7,
|
||
"top_p": 0.95,
|
||
"reasoning_effort": "auto",
|
||
"timeout_seconds": 120,
|
||
"max_retries": 2,
|
||
}
|
||
|
||
def test_never_contains_secret_or_base_url(self, service):
|
||
snapshot = service.create(_request()).snapshot
|
||
encoded = json.dumps(public_snapshot_view(snapshot))
|
||
assert SECRET not in encoded
|
||
assert "open.bigmodel.cn" not in encoded
|
||
assert "localhost" not in encoded
|