# SPDX-License-Identifier: GPL-3.0-or-later """Structural validation tests for the shared Observation envelope. These tests are STRUCTURAL ONLY — window ordering, confidence bounds, schema version, round-trip serialization, extra-field rejection. Registry-aware validation (unknown primitives, categorical-allowed-set, numeric-min-bound, etc.) lives in each sibling package's own test_envelope.py because the registry IS the sibling-specific concern. """ from __future__ import annotations import pytest from pydantic import ValidationError from decnet_behave_core.spec import OBSERVATION_SCHEMA_VERSION, Observation, Window def _make(primitive: str = "motor.example", value="x", **kwargs) -> Observation: base = dict( primitive=primitive, value=value, confidence=0.8, window=Window(start_ts=1.0, end_ts=2.0), source="test/sensor", ) base.update(kwargs) return Observation(**base) def test_minimal_observation_round_trips(): obs = _make() obs2 = Observation.model_validate_json(obs.model_dump_json()) assert obs == obs2 def test_schema_version_pinned_to_one(): assert OBSERVATION_SCHEMA_VERSION == 1 obs = _make() assert obs.v == 1 def test_window_end_must_be_after_start(): with pytest.raises(ValidationError): Window(start_ts=2.0, end_ts=1.0) def test_window_point_event_allowed(): w = Window(start_ts=5.0, end_ts=5.0) assert w.start_ts == w.end_ts def test_confidence_must_be_in_unit_interval(): with pytest.raises(ValidationError): _make(confidence=-0.01) with pytest.raises(ValidationError): _make(confidence=1.01) def test_extra_fields_forbidden(): with pytest.raises(ValidationError): Observation( primitive="motor.example", value="x", confidence=0.5, window=Window(start_ts=1.0, end_ts=2.0), source="test/sensor", unknown_field="oops", ) def test_id_and_ts_auto_default(): obs1 = _make() obs2 = _make() assert obs1.id != obs2.id assert obs1.ts > 0 def test_core_envelope_is_registry_agnostic(): """The base Observation accepts any primitive string; sibling subclasses validate.""" obs = _make(primitive="anything.goes.here", value="anything") assert obs.primitive == "anything.goes.here" assert obs.value == "anything" def test_source_must_be_nonempty(): with pytest.raises(ValidationError): _make(source="")