test(identity): IdentityCheck gate wiring, IdentityScore invariants, TurnEvent field coverage
This commit is contained in:
parent
1343171fab
commit
9f2b2acbf0
1 changed files with 178 additions and 0 deletions
178
tests/test_identity_gate.py
Normal file
178
tests/test_identity_gate.py
Normal file
|
|
@ -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"
|
||||
Loading…
Reference in a new issue