183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
"""DGR-022 lifecycle and structured-status contract tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from meshnet_node.shard_lifecycle import (
|
|
CacheExpectation,
|
|
CacheResult,
|
|
CapabilityResponse,
|
|
CancellationToken,
|
|
FlowControl,
|
|
FlowControlLimits,
|
|
LifecycleContractError,
|
|
LifecycleState,
|
|
RpcName,
|
|
SessionFrame,
|
|
SessionLifecycle,
|
|
SessionPhase,
|
|
SessionRequest,
|
|
ShardRpcContract,
|
|
StatusCode,
|
|
StructuredStatus,
|
|
)
|
|
|
|
|
|
def _request(**overrides) -> SessionRequest:
|
|
values = dict(
|
|
schema_version=1,
|
|
request_id="request-1",
|
|
work_id="work-1",
|
|
route_session="route-1",
|
|
route_epoch=3,
|
|
artifact_fingerprint="artifact-sha",
|
|
runtime_fingerprint="runtime-sha",
|
|
shard_start=0,
|
|
shard_end=8,
|
|
effective_start=0,
|
|
cache_expectation=CacheExpectation.OPTIONAL,
|
|
)
|
|
values.update(overrides)
|
|
return SessionRequest(**values)
|
|
|
|
|
|
def _frame(phase: SessionPhase, step: int, position: int = 0) -> SessionFrame:
|
|
return SessionFrame(
|
|
phase=phase,
|
|
position=position,
|
|
idempotency_step=step,
|
|
payload=b"frame",
|
|
size_bytes=5,
|
|
)
|
|
|
|
|
|
def test_contract_roundtrip_exposes_all_lifecycle_rpcs_and_operational_hooks():
|
|
contract = ShardRpcContract()
|
|
restored = ShardRpcContract.from_bytes(contract.to_bytes())
|
|
|
|
assert restored == contract
|
|
assert restored.methods == tuple(RpcName)
|
|
assert restored.deadlines.session_idle_seconds > restored.deadlines.health_seconds
|
|
assert restored.flow_control.max_inflight_frames == 32
|
|
assert restored.tls_auth.tls_required is True
|
|
|
|
|
|
def test_supported_version_roundtrip_and_status_are_stable():
|
|
status = StructuredStatus(StatusCode.RESOURCE_EXHAUSTED, "window full", retryable=True, details={"limit": "32"})
|
|
restored = StructuredStatus.from_dict(status.to_dict())
|
|
|
|
assert restored == status
|
|
assert CapabilityResponse(
|
|
status=StructuredStatus(StatusCode.OK, "ready"),
|
|
schema_version=1,
|
|
supported_versions=(1,),
|
|
).validate() is None
|
|
|
|
|
|
@pytest.mark.parametrize("version", [0, 2, 99])
|
|
def test_unsupported_versions_fail_closed(version):
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
ShardRpcContract.from_bytes(json.dumps({
|
|
"schema": "meshnet.shard-runtime",
|
|
"version": version,
|
|
"methods": [method.value for method in RpcName],
|
|
}).encode())
|
|
assert exc.value.status.code is StatusCode.UNSUPPORTED_VERSION
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
_request(schema_version=version).validate()
|
|
assert exc.value.status.code is StatusCode.UNSUPPORTED_VERSION
|
|
|
|
|
|
def test_malformed_lifecycle_transitions_fail_closed():
|
|
session = SessionLifecycle(_request())
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
session.apply(_frame(SessionPhase.DECODE, 0))
|
|
assert exc.value.status.code is StatusCode.MALFORMED_LIFECYCLE
|
|
assert session.state is LifecycleState.OPEN
|
|
|
|
session.apply(_frame(SessionPhase.PREFILL, 0))
|
|
session.apply(_frame(SessionPhase.DECODE, 1, position=1))
|
|
assert session.state is LifecycleState.DECODING
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
session.apply(_frame(SessionPhase.PREFILL, 2))
|
|
assert exc.value.status.code is StatusCode.MALFORMED_LIFECYCLE
|
|
|
|
|
|
def test_duplicate_and_non_monotonic_idempotency_steps_are_rejected():
|
|
session = SessionLifecycle(_request())
|
|
session.apply(_frame(SessionPhase.PREFILL, 4))
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
session.apply(_frame(SessionPhase.PREFILL, 4))
|
|
assert exc.value.status.code is StatusCode.ALREADY_EXISTS
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
session.apply(_frame(SessionPhase.PREFILL, 3))
|
|
assert exc.value.status.code is StatusCode.MALFORMED_LIFECYCLE
|
|
|
|
|
|
def test_cancel_propagates_to_waiters_and_release_is_idempotent():
|
|
session = SessionLifecycle(_request())
|
|
assert session.cancel("client disconnected").code is StatusCode.CANCELLED
|
|
assert session.cancellation.cancelled is True
|
|
assert session.state is LifecycleState.CANCELLED
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
session.apply(_frame(SessionPhase.DECODE, 0))
|
|
assert exc.value.status.code is StatusCode.CANCELLED
|
|
|
|
assert session.release().code is StatusCode.OK
|
|
assert session.state is LifecycleState.RELEASED
|
|
assert session.release().code is StatusCode.OK
|
|
|
|
|
|
def test_bounded_flow_control_rejects_oversize_and_full_windows():
|
|
flow = FlowControl(FlowControlLimits(max_inflight_frames=1, max_inflight_bytes=10, max_frame_bytes=8))
|
|
flow.acquire(8)
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
flow.acquire(1)
|
|
assert exc.value.status.code is StatusCode.RESOURCE_EXHAUSTED
|
|
assert exc.value.status.retryable is True
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
flow.acquire(9)
|
|
assert exc.value.status.code is StatusCode.RESOURCE_EXHAUSTED
|
|
|
|
flow.release(8)
|
|
assert flow.outstanding == (0, 0)
|
|
|
|
|
|
def test_flow_control_wait_honours_cancellation():
|
|
token = CancellationToken()
|
|
flow = FlowControl(FlowControlLimits(max_inflight_frames=1, max_inflight_bytes=10, max_frame_bytes=8))
|
|
flow.acquire(4)
|
|
token.cancel()
|
|
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
flow.acquire(1, wait=True, cancelled=lambda: token.cancelled)
|
|
assert exc.value.status.code is StatusCode.CANCELLED
|
|
|
|
|
|
def test_cache_expectation_and_result_are_explicit():
|
|
request = _request(cache_expectation=CacheExpectation.REQUIRED)
|
|
assert request.cache_expectation is CacheExpectation.REQUIRED
|
|
assert CacheResult.MISS.value == "MISS"
|
|
|
|
|
|
def test_invalid_contract_methods_and_tls_hooks_fail_closed():
|
|
with pytest.raises(LifecycleContractError) as exc:
|
|
ShardRpcContract(methods=(RpcName.HEALTH,)).validate()
|
|
assert exc.value.status.code is StatusCode.FAILED_PRECONDITION
|
|
|
|
with pytest.raises(ValueError, match="server_name"):
|
|
ShardRpcContract(
|
|
tls_auth=type(ShardRpcContract().tls_auth)(tls_required=True, server_name="")
|
|
).validate()
|