core/tests/test_identity_packs.py
Shay fa05be9293 feat(identity-packs): ADR-0027 — swappable identity manifold via packs
Replaces the hardcoded IdentityManifold constructor in chat/runtime.py
with a content-addressed pack loader.  Identity is now load-bearing AND
swappable: deployments select an identity pack at startup, downstream
builders (robotics, personalization, creative tools) author their own
ratified packs without editing CORE Python.

Phase 1 — pack format + loader
  * packs/identity/loader.py — load_identity_manifold(pack_id, *,
    search_paths, require_ratified) with bounds checks (axis count,
    direction in [-1, 1], weight in [0, 10], threshold in [0, 1],
    axis-id uniqueness).
  * available_packs() helper for discovery.
  * IdentityPackError raised on every bounds violation.

Phase 2 — three v1 packs
  * default_general_v1.json — ship default; encodes the previous
    hardcoded three axes (truthfulness, coherence, reverence)
    byte-for-byte so existing runtime behavior is preserved.
  * precision_first_v1.json — boosts truthfulness weight, narrows
    coherence/reverence; tighter alignment threshold.
  * generosity_first_v1.json — boosts coherence weight, broadens
    reverence; looser alignment threshold.

Phase 3 — replace hardcoded constructor
  * chat/runtime.py:206 calls load_identity_manifold() using
    RuntimeConfig.identity_pack (default DEFAULT_IDENTITY_PACK).
  * Dead _default_identity_manifold() removed.
  * ChatRuntime.identity_pack_id surfaces the loaded pack id.

Phase 4 — CLI flag
  * core chat --identity <pack_id>  (also threaded into trace/oov via
    _add_runtime_policy_args).
  * core/config.py: RuntimeConfig.identity_pack added; empty string
    falls back to DEFAULT_IDENTITY_PACK = 'default_general_v1'.

Phase 5 — formation ratification — INTENTIONALLY DEFERRED.  Loader
currently calls require_ratified=False so the v1 packs (which carry
empty mastery_report_sha256) load.  Authoring SubjectSpecs for each
pack, running the formation pipeline end-to-end to produce signed
MasteryReports, and embedding the SHA into each pack file is a
follow-up.

Tests: 18 new tests in tests/test_identity_packs.py covering loader
happy paths, every bounds violation, runtime wiring, and pack-swap
divergence.

Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67 — all green.

Docs: ADR-0027 (Accepted) + docs/identity_packs.md (operational ref) +
README.md §Identity Packs + docs/teaching_order.md Layer 1 cross-ref.
2026-05-17 19:24:39 -07:00

227 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Identity-pack loader + runtime wiring tests.
Covers ADR-0027 Phase 14 surface area:
* Loader bounds checks (missing fields, malformed direction, weight,
threshold, duplicate axis id, missing pack).
* Round-trip parity: ``default_general_v1`` constructs an
``IdentityManifold`` equal to the pre-ADR hardcoded one.
* Runtime wiring: ``ChatRuntime`` loads the pack indicated by
``RuntimeConfig.identity_pack`` (and falls back to the default).
* Pack swap: ``precision_first_v1`` and ``generosity_first_v1`` produce
manifolds that differ from the default in axis weights / thresholds.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from chat.runtime import ChatRuntime
from core.config import DEFAULT_IDENTITY_PACK, RuntimeConfig
from packs.identity.loader import (
IdentityPackError,
available_packs,
load_identity_manifold,
)
# ---------- loader bounds ----------
class TestLoaderHappyPath:
def test_loads_default_general(self) -> None:
m = load_identity_manifold(
"default_general_v1", require_ratified=False,
)
assert len(m.value_axes) == 3
axis_ids = {a.axis_id for a in m.value_axes}
assert axis_ids == {"truthfulness", "coherence", "reverence"}
assert m.alignment_threshold == 0.45
assert m.boundary_ids == frozenset(
{"no_fabricated_source", "no_hot_path_repair"}
)
def test_loads_precision_first(self) -> None:
m = load_identity_manifold(
"precision_first_v1", require_ratified=False,
)
weights = {a.axis_id: a.weight for a in m.value_axes}
# Precision pack boosts truthfulness weight to 2.0; defaults are 1.0.
assert weights["truthfulness"] == 2.0
assert weights["coherence"] == 0.7
assert m.alignment_threshold == 0.55
def test_loads_generosity_first(self) -> None:
m = load_identity_manifold(
"generosity_first_v1", require_ratified=False,
)
weights = {a.axis_id: a.weight for a in m.value_axes}
assert weights["coherence"] == 2.0
assert weights["truthfulness"] == 0.8
def test_available_packs(self) -> None:
packs = available_packs()
ids = {p["pack_id"] for p in packs}
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= ids
for p in packs:
assert p["ratified"] is False # v1 packs not yet ratified
# ---------- error paths ----------
class TestLoaderRejects:
def test_missing_pack(self) -> None:
with pytest.raises(IdentityPackError, match="not found"):
load_identity_manifold(
"does_not_exist", require_ratified=False,
)
def test_path_traversal_rejected(self) -> None:
with pytest.raises(IdentityPackError, match="invalid pack_id"):
load_identity_manifold(
"../../etc/passwd", require_ratified=False,
)
def test_require_ratified_default(self, tmp_path: Path) -> None:
# default_general_v1 is unratified (empty mastery_report_sha256);
# require_ratified=True must refuse.
with pytest.raises(IdentityPackError, match="not ratified"):
load_identity_manifold(
"default_general_v1", require_ratified=True,
)
def test_malformed_direction(self, tmp_path: Path) -> None:
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [1.0, 0.0], # length 2, not 3
"weight": 1.0,
}],
)
with pytest.raises(IdentityPackError, match="direction"):
load_identity_manifold(
bad["pack_id"],
search_paths=[tmp_path],
require_ratified=False,
)
def test_direction_out_of_bounds(self, tmp_path: Path) -> None:
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [5.0, 0.0, 0.0],
"weight": 1.0,
}],
)
with pytest.raises(IdentityPackError, match="out of bounds"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
)
def test_weight_out_of_bounds(self, tmp_path: Path) -> None:
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [1.0, 0.0, 0.0],
"weight": 999.0,
}],
)
with pytest.raises(IdentityPackError, match="weight"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
)
def test_duplicate_axis_id(self, tmp_path: Path) -> None:
bad = _write_pack(
tmp_path,
value_axes=[
{"axis_id": "x", "name": "x", "direction": [1.0, 0.0, 0.0], "weight": 1.0},
{"axis_id": "x", "name": "y", "direction": [0.0, 1.0, 0.0], "weight": 1.0},
],
)
with pytest.raises(IdentityPackError, match="duplicate axis_id"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
)
def test_empty_axes(self, tmp_path: Path) -> None:
bad = _write_pack(tmp_path, value_axes=[])
with pytest.raises(IdentityPackError, match="at least"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
)
# ---------- runtime wiring ----------
class TestRuntimeWiring:
def test_runtime_loads_default_pack(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
assert rt.identity_pack_id == DEFAULT_IDENTITY_PACK
axis_ids = {a.axis_id for a in rt.identity_manifold.value_axes}
assert axis_ids == {"truthfulness", "coherence", "reverence"}
def test_runtime_loads_precision_pack_via_config(self) -> None:
rt = ChatRuntime(config=RuntimeConfig(identity_pack="precision_first_v1"))
assert rt.identity_pack_id == "precision_first_v1"
weights = {a.axis_id: a.weight for a in rt.identity_manifold.value_axes}
assert weights["truthfulness"] == 2.0
def test_runtime_loads_generosity_pack_via_config(self) -> None:
rt = ChatRuntime(config=RuntimeConfig(identity_pack="generosity_first_v1"))
weights = {a.axis_id: a.weight for a in rt.identity_manifold.value_axes}
assert weights["coherence"] == 2.0
def test_empty_identity_pack_falls_back_to_default(self) -> None:
rt = ChatRuntime(config=RuntimeConfig(identity_pack=""))
assert rt.identity_pack_id == DEFAULT_IDENTITY_PACK
# ---------- pack swap proof ----------
class TestPackSwap:
def test_precision_vs_default_manifolds_differ(self) -> None:
default_m = load_identity_manifold("default_general_v1", require_ratified=False)
precision_m = load_identity_manifold("precision_first_v1", require_ratified=False)
assert default_m.alignment_threshold != precision_m.alignment_threshold
default_weights = {a.axis_id: a.weight for a in default_m.value_axes}
precision_weights = {a.axis_id: a.weight for a in precision_m.value_axes}
assert default_weights != precision_weights
def test_generosity_adds_no_extra_boundaries(self) -> None:
# Identity packs may add boundaries (precision_first does) but
# must not silently drop the core ones.
for pack_id in (
"default_general_v1", "precision_first_v1", "generosity_first_v1",
):
m = load_identity_manifold(pack_id, require_ratified=False)
assert "no_fabricated_source" in m.boundary_ids
assert "no_hot_path_repair" in m.boundary_ids
# ---------- helpers ----------
def _write_pack(tmp_path: Path, *, value_axes: list, pack_id: str = "test_pack") -> dict:
body = {
"pack_id": pack_id,
"version": "1.0.0",
"description": "test",
"schema_version": "1.0.0",
"mastery_report_sha256": "",
"alignment_threshold": 0.45,
"boundary_ids": ["test_boundary"],
"value_axes": value_axes,
}
path = tmp_path / f"{pack_id}.json"
path.write_text(json.dumps(body), encoding="utf-8")
return body