core/tests/test_identity_packs.py
Shay 7c839d2e12 feat(cli): core chat --list-identity-packs + companion-file filter
Adds the discovery flag callers have been asking for since ADR-0027.
Short-circuits before the REPL launches; supports both a human-readable
table and `--json` machine output.  Drives the loader's existing
`available_packs()` helper.

Bug fix on the way: `available_packs()` was globbing every `*.json`
in the search path, so the Phase-5 companion `<pack_id>.mastery_report.json`
files were leaking into the list as fake packs with empty fields.  The
helper now skips any file ending in `.mastery_report.json` and rejects
JSON that lacks the required `schema_version` / `value_axes` fields.

CLI output:

  pack_id              version  ratified  description
  -------------------  -------  --------  -----------
  default_general_v1   1.0.0    yes       Balanced general identity...
  generosity_first_v1  1.0.0    yes       Generosity-first specialization...
  precision_first_v1   1.0.0    yes       Precision-first specialization...

Tests: +3 (CLI table, CLI JSON, companion-file filter regression).
test_identity_packs.py: 23 -> 26.  cognition / smoke green.

Docs: docs/identity_packs.md CLI usage block updated; memory
'identity-packs.md' closes that follow-up.
2026-05-17 19:47:13 -07:00

407 lines
15 KiB
Python
Raw Permalink 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
# Phase 5 complete — v1 packs are ratified.
ratified_ids = {p["pack_id"] for p in packs if p["ratified"]}
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= ratified_ids
def test_available_packs_excludes_mastery_reports(self) -> None:
"""Companion ``<pack_id>.mastery_report.json`` files must not surface as packs."""
packs = available_packs()
ids = {p["pack_id"] for p in packs}
for pid in ids:
assert not pid.endswith(".mastery_report"), (
f"mastery report companion leaked into available_packs(): {pid!r}"
)
def test_v1_packs_load_in_production_mode(self) -> None:
# require_ratified default (None) -> production unless env override.
for pid in (
"default_general_v1", "precision_first_v1", "generosity_first_v1",
):
m = load_identity_manifold(pid)
assert len(m.value_axes) == 3
# ---------- 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_unratified_pack_refused_in_production(self, tmp_path: Path) -> None:
# An unratified test pack (empty mastery_report_sha256) must be
# refused in production mode.
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [1.0, 0.0, 0.0], "weight": 1.0,
}],
)
with pytest.raises(IdentityPackError, match="not ratified"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=True,
)
def test_missing_companion_report_refused(
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,
}],
mastery_report_sha256="0" * 64,
)
with pytest.raises(IdentityPackError, match="companion report file"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=True,
)
def test_companion_report_sha_mismatch_refused(
self, tmp_path: Path,
) -> None:
# Pack claims one SHA; companion report carries a different one.
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [1.0, 0.0, 0.0], "weight": 1.0,
}],
mastery_report_sha256="a" * 64,
)
report_path = tmp_path / f"{bad['pack_id']}.mastery_report.json"
report_path.write_text(
json.dumps({"report_sha256": "b" * 64, "ratified": True}),
encoding="utf-8",
)
with pytest.raises(IdentityPackError, match="does not match"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], require_ratified=True,
)
def test_companion_report_seal_failure_refused(
self, tmp_path: Path,
) -> None:
# Companion report's claimed SHA does not actually self-seal.
bogus_sha = "c" * 64
bad = _write_pack(
tmp_path,
value_axes=[{
"axis_id": "x", "name": "x",
"direction": [1.0, 0.0, 0.0], "weight": 1.0,
}],
mastery_report_sha256=bogus_sha,
)
report_path = tmp_path / f"{bad['pack_id']}.mastery_report.json"
report_path.write_text(
json.dumps(
{
"report_sha256": bogus_sha,
"ratified": True,
"other_field": "anything",
},
),
encoding="utf-8",
)
with pytest.raises(IdentityPackError, match="self-seal"):
load_identity_manifold(
bad["pack_id"], search_paths=[tmp_path], 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
# ---------- CLI: --list-identity-packs ----------
class TestListIdentityPacksCLI:
def test_table_output_lists_all_three_packs(self) -> None:
import subprocess
import sys
repo_root = Path(__file__).resolve().parents[1]
result = subprocess.run(
[sys.executable, "-m", "core.cli", "chat", "--list-identity-packs"],
cwd=str(repo_root),
env={"PYTHONPATH": str(repo_root), "PATH": "/usr/bin:/bin"},
capture_output=True, text=True, check=True,
)
# Header + three pack rows; no mastery_report companion rows.
assert "default_general_v1" in result.stdout
assert "precision_first_v1" in result.stdout
assert "generosity_first_v1" in result.stdout
assert "mastery_report" not in result.stdout
# All three ratified -> three "yes" flags.
assert result.stdout.count(" yes ") >= 3
def test_json_output_is_valid_json(self) -> None:
import subprocess
import sys
repo_root = Path(__file__).resolve().parents[1]
result = subprocess.run(
[
sys.executable, "-m", "core.cli", "chat",
"--list-identity-packs", "--json",
],
cwd=str(repo_root),
env={"PYTHONPATH": str(repo_root), "PATH": "/usr/bin:/bin"},
capture_output=True, text=True, check=True,
)
payload = json.loads(result.stdout)
assert isinstance(payload, list)
pack_ids = {p["pack_id"] for p in payload}
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= pack_ids
for p in payload:
assert p["ratified"] is True
# ---------- ratification script idempotency ----------
class TestRatificationScript:
def test_script_idempotent(self) -> None:
"""Re-running scripts/ratify_identity_packs.py must not change anything."""
import subprocess
import sys
repo_root = Path(__file__).resolve().parents[1]
before = {
pid: (repo_root / "packs" / "identity" / f"{pid}.json").read_text(
encoding="utf-8"
)
for pid in (
"default_general_v1", "precision_first_v1", "generosity_first_v1",
)
}
result = subprocess.run(
[sys.executable, "scripts/ratify_identity_packs.py"],
cwd=str(repo_root),
env={"PYTHONPATH": str(repo_root), "PATH": "/usr/bin:/bin"},
check=True,
capture_output=True,
text=True,
)
assert "ratified 0 pack(s); 3 already current" in result.stdout
after = {
pid: (repo_root / "packs" / "identity" / f"{pid}.json").read_text(
encoding="utf-8"
)
for pid in before
}
for pid, txt in before.items():
assert after[pid] == txt, f"pack {pid} changed during idempotent re-run"
# ---------- helpers ----------
def _write_pack(
tmp_path: Path,
*,
value_axes: list,
pack_id: str = "test_pack",
mastery_report_sha256: str = "",
) -> dict:
body = {
"pack_id": pack_id,
"version": "1.0.0",
"description": "test",
"schema_version": "1.0.0",
"mastery_report_sha256": 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