From 9f2b2acbf062838d295a0b5ec4299676b02d27cd Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 14:30:56 -0700 Subject: [PATCH] test(identity): IdentityCheck gate wiring, IdentityScore invariants, TurnEvent field coverage --- tests/test_identity_gate.py | 178 ++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/test_identity_gate.py diff --git a/tests/test_identity_gate.py b/tests/test_identity_gate.py new file mode 100644 index 00000000..fcc92ec7 --- /dev/null +++ b/tests/test_identity_gate.py @@ -0,0 +1,178 @@ +"""tests/test_identity_gate.py — Tests for IdentityCheck gate wiring. + +Covers: + - IdentityCheck.check() returns an IdentityScore for a valid trajectory + - IdentityScore.score is a float in [0, 1] + - IdentityScore.flagged is a bool + - IdentityScore.value and .alignment property aliases work + - IdentityScore.axes_evaluated returns a sorted list + - ChatRuntime.turn_log[-1].identity_score is populated after chat() + - ChatRuntime.turn_log[-1].flagged matches response.flagged + - TurnEvent.elaboration field exists and is Optional[str] + - A response that should not be flagged has flagged=False +""" +from __future__ import annotations + +import pytest + +from core.physics.identity import ( + IdentityCheck, + IdentityManifold, + IdentityScore, + TurnEvent, + ValueAxis, +) +from core.physics.reasoning import ReasoningTrajectory, TrajectoryOperator + +import numpy as np + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_manifold() -> IdentityManifold: + return IdentityManifold( + value_axes=( + ValueAxis( + name="truthfulness", + direction=np.array([1.0, 0.0, 0.0], dtype=np.float32), + weight=1.0, + ), + ValueAxis( + name="coherence", + direction=np.array([0.0, 1.0, 0.0], dtype=np.float32), + weight=1.0, + ), + ), + ) + + +def _make_trajectory(n_steps: int = 4) -> ReasoningTrajectory: + """Minimal trajectory with n_steps identity operators.""" + ops = [ + TrajectoryOperator( + versor=np.ones(32, dtype=np.float32) / np.sqrt(32), + step=i, + ) + for i in range(n_steps) + ] + return ReasoningTrajectory(operators=tuple(ops), turn=0) + + +# --------------------------------------------------------------------------- +# IdentityScore invariants +# --------------------------------------------------------------------------- + +class TestIdentityScore: + def test_score_is_float_in_unit_interval(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + trajectory = _make_trajectory() + score = check.check(trajectory) + assert isinstance(score, IdentityScore) + assert 0.0 <= score.score <= 1.0 + + def test_flagged_is_bool(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + score = check.check(_make_trajectory()) + assert isinstance(score.flagged, bool) + + def test_value_alias_matches_score(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + score = check.check(_make_trajectory()) + assert score.value == score.score + + def test_alignment_is_float_in_unit_interval(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + score = check.check(_make_trajectory()) + assert 0.0 <= score.alignment <= 1.0 + + def test_axes_evaluated_is_sorted_list(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + score = check.check(_make_trajectory()) + axes = score.axes_evaluated + assert isinstance(axes, list) + assert axes == sorted(axes) + + def test_deviation_axes_is_frozenset_of_str(self): + manifold = _make_manifold() + check = IdentityCheck(manifold=manifold) + score = check.check(_make_trajectory()) + assert isinstance(score.deviation_axes, frozenset) + for ax in score.deviation_axes: + assert isinstance(ax, str) + + +# --------------------------------------------------------------------------- +# TurnEvent field invariants +# --------------------------------------------------------------------------- + +class TestTurnEventFields: + def test_elaboration_field_exists_and_is_optional_str(self): + """TurnEvent.elaboration must be Optional[str] — verify field presence.""" + import dataclasses + fields = {f.name: f for f in dataclasses.fields(TurnEvent)} + assert "elaboration" in fields, "TurnEvent missing elaboration field" + # Default must be None. + # We can't instantiate without all required fields, so just check the + # default value via the field descriptor. + field_obj = fields["elaboration"] + assert field_obj.default is None + + def test_identity_score_field_is_optional(self): + import dataclasses + fields = {f.name: f for f in dataclasses.fields(TurnEvent)} + assert "identity_score" in fields + + def test_flagged_field_exists(self): + import dataclasses + fields = {f.name: f for f in dataclasses.fields(TurnEvent)} + assert "flagged" in fields + + +# --------------------------------------------------------------------------- +# ChatRuntime integration: identity_score wired into turn_log +# --------------------------------------------------------------------------- + +class TestChatRuntimeIdentityWiring: + """These tests instantiate a live ChatRuntime and check identity gate wiring. + + Skipped if ChatRuntime cannot be instantiated (missing language pack data). + """ + + @pytest.fixture(autouse=True) + def runtime(self): + try: + from chat.runtime import ChatRuntime + self._runtime = ChatRuntime() + except Exception as exc: + pytest.skip(f"ChatRuntime not available: {exc}") + + def test_turn_log_populated_after_chat(self): + self._runtime.chat("truth", max_tokens=4) + assert len(self._runtime.turn_log) >= 1 + + def test_identity_score_is_identityscore_or_none(self): + self._runtime.chat("light", max_tokens=4) + ev = self._runtime.turn_log[-1] + assert ev.identity_score is None or isinstance(ev.identity_score, IdentityScore) + + def test_flagged_matches_response(self): + response = self._runtime.chat("covenant", max_tokens=4) + ev = self._runtime.turn_log[-1] + assert ev.flagged == response.flagged + + def test_elaboration_is_none_or_str(self): + self._runtime.chat("word", max_tokens=8) + ev = self._runtime.turn_log[-1] + assert ev.elaboration is None or isinstance(ev.elaboration, str) + + def test_unflagged_response_has_non_empty_surface(self): + response = self._runtime.chat("beginning", max_tokens=8) + if not response.flagged: + assert response.surface, "Surface must not be empty for unflagged response"