feat(shell): initial decnet_behave_shell spec + tests

Shell-session behavioral observation registry layered on core.
SPDX: GPL-3.0-or-later (code) / CC-BY-SA-4.0 (attribution-recipes.md).
This commit is contained in:
2026-05-10 06:17:28 -04:00
parent abe7dde778
commit bf654b9aed
12 changed files with 1337 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# 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 `decnet-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 decnet_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",
)