# SPDX-License-Identifier: GPL-3.0-or-later """Registry-aware envelope tests for BEHAVE-SHELL. Structural envelope tests (window, confidence bounds, schema version, etc.) live in `behave-core`'s test suite. This file exercises the SHELL- SPECIFIC validation: that BEHAVE-SHELL's Observation subclass rejects primitives not in the shell registry and rejects values that violate the per-primitive ValueTypeSpec. """ from __future__ import annotations import pytest from pydantic import ValidationError from behave_shell.spec import Observation, Window def _make(primitive: str = "motor.keystroke_cadence", value="steady", **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_unknown_primitive_rejected(): with pytest.raises(ValidationError) as exc_info: _make(primitive="motor.nonexistent", value="whatever") assert "unknown primitive" in str(exc_info.value) def test_categorical_value_outside_allowed_rejected(): with pytest.raises(ValidationError) as exc_info: _make(primitive="motor.keystroke_cadence", value="not_a_real_value") assert "not in allowed set" in str(exc_info.value) def test_categorical_wrong_type_rejected(): with pytest.raises(ValidationError): _make(primitive="motor.keystroke_cadence", value=42) def test_numeric_min_bound_enforced(): with pytest.raises(ValidationError): _make(primitive="toolchain.c2.beacon_interval_ms", value=-1) def test_numeric_accepts_valid(): obs = _make(primitive="toolchain.c2.beacon_interval_ms", value=60_000) assert obs.value == 60_000 def test_numeric_rejects_bool(): # bool is a subclass of int — must be rejected explicitly. with pytest.raises(ValidationError): _make(primitive="toolchain.c2.beacon_interval_ms", value=True) def test_hash_requires_nonempty_string(): with pytest.raises(ValidationError): _make(primitive="toolchain.tls.ja3_client", value="") def test_array_validates_elements(): obs = _make( primitive="toolchain.ssh.kex_algorithm_order", value=["curve25519-sha256", "ecdh-sha2-nistp256"], ) assert isinstance(obs.value, list) def test_array_rejects_non_list(): with pytest.raises(ValidationError): _make(primitive="toolchain.ssh.kex_algorithm_order", value="not a list") def test_bool_primitive_accepts_bool(): obs = _make(primitive="toolchain.protocol_abuse.mitm6_signature", value=True) assert obs.value is True def test_bool_primitive_rejects_int(): with pytest.raises(ValidationError): _make(primitive="toolchain.protocol_abuse.mitm6_signature", value=1) def test_free_string_primitive_accepts_arbitrary_string(): obs = _make(primitive="environmental.locale", value="pt-BR") assert obs.value == "pt-BR" def test_extra_fields_still_forbidden_via_subclass(): # Inherited from base — the subclass shouldn't relax this. with pytest.raises(ValidationError): Observation( primitive="motor.keystroke_cadence", value="steady", confidence=0.5, window=Window(start_ts=1.0, end_ts=2.0), source="test/sensor", unknown_field="oops", )