core/tests/test_runtime_config_passthrough.py
Shay c6b4f1d21e fix(runtime): config-replace + thin API wrappers + stale docstring
Three independent hygiene fixes named in the 2026-05-19 design review.
All small, all observable, none architectural.

1. ``RuntimeConfig`` flag drop on pack_id / frame_pack override
   chat/runtime.py:306-320 used to enumerate fields by hand when
   reconstructing RuntimeConfig under the pack_id / frame_pack
   override path.  The list stopped at ``admissibility_margin`` and
   silently dropped FIVE newer flags: identity_pack, ethics_pack,
   forward_graph_constraint, composed_surface, thread_anaphora.
   Caller side-effect:

     ChatRuntime(pack_id="x", config=RuntimeConfig(composed_surface=True))
       .config.composed_surface == False  # silently lost

   Fix: ``dataclasses.replace(config, input_packs=..., frame_pack=...)``.
   Every field on the dataclass survives by construction; future
   additions never need a synchronized edit on this path.

2. Stale CAUSE / VERIFICATION docstring
   tests/test_intent_classification_extensions.py described a sixth
   runtime-side fix (pack_grounded_surface fallback for
   CAUSE/VERIFICATION) that was considered, reverted, and the file's
   own test classes pin the opposite contract.  Docstring now states
   the doctrine correctly: no fallback, deliberately, so the discovery
   layer can log the teaching-gap signal.

3. Thin convenience wrappers: respond / achat / arespond
   tests/test_achat.py and tests/test_language_pack_runtime.py
   referenced these public methods since 2026-05-14, but they were
   never implemented on ChatRuntime — those 12 tests had been red on
   every full-lane run since the rebase.  Added as thin wrappers:

     respond(text) -> ChatResponse.surface
     achat(text)   -> async wrapper around chat()
     arespond(text)-> async wrapper around respond()

   The async wrappers are deliberately NOT genuinely non-blocking —
   the underlying CPU-bound walk/recall/composition remains sync.
   Docstrings say so explicitly.  Callers needing real concurrency
   should wrap in asyncio.to_thread at the call site; promoting the
   wrappers to true async event-loop integration is a future change
   gated by an actual concurrent caller.

Regression coverage:
  tests/test_runtime_config_passthrough.py — 4 tests
    - all 19 RuntimeConfig fields survive a pack_id override
    - all five newer flags survive a frame_pack override
    - no-override path preserves caller config by identity (no rebuild)
    - the four public methods exist and are callable

Verification:
  44/44 affected tests green (was 12 red pre-fix).
  Cognition eval byte-identical on both splits.
  No surface-format change; this commit is pure plumbing.
2026-05-19 07:04:10 -07:00

106 lines
4 KiB
Python

"""Regression test for ChatRuntime.__init__ config-flag passthrough.
Prior to fix d-2026-05-19, ``ChatRuntime.__init__`` manually reconstructed
``RuntimeConfig`` field-by-field whenever ``pack_id`` or ``frame_pack``
was supplied. The manual list was incomplete: newer flags
(``identity_pack``, ``ethics_pack``, ``forward_graph_constraint``,
``composed_surface``, ``thread_anaphora``) were silently dropped, so a
caller like
ChatRuntime(pack_id="x", config=RuntimeConfig(composed_surface=True))
would lose ``composed_surface=True`` without warning.
The fix uses ``dataclasses.replace`` so every field on the dataclass
survives by construction. This test pins that contract: pass a config
with all current flags set to *non-default* values, instantiate
``ChatRuntime`` with a ``pack_id`` override, and assert every flag
survived on ``runtime.config``.
If a new flag is added to ``RuntimeConfig``, this test should catch any
future drift on the override path provided the default differs from
the test value (the assertion compares post-init value against the
explicitly-set value, not against the default).
"""
from __future__ import annotations
from dataclasses import fields
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
def test_all_runtime_config_flags_survive_pack_id_override() -> None:
custom = RuntimeConfig(
input_packs=("en_minimal_v1",),
output_language="en",
frame_pack="en",
max_tokens=17, # non-default
allow_cross_language_recall=False,
allow_cross_language_generation=True,
vault_reproject_interval=99,
use_salience=False,
salience_top_k=8,
inhibition_threshold=0.42,
inner_loop_admissibility=True,
admissibility_threshold=0.13,
admissibility_mode="margin",
admissibility_margin=0.7,
identity_pack="precision_first_v1",
ethics_pack="default_general_ethics_v1",
forward_graph_constraint=True,
composed_surface=True,
thread_anaphora=True,
)
runtime = ChatRuntime(pack_id="en_minimal_v1", config=custom)
for field in fields(RuntimeConfig):
if field.name == "input_packs":
# The pack_id override deliberately rewrites input_packs;
# other fields must survive verbatim.
continue
expected = getattr(custom, field.name)
actual = getattr(runtime.config, field.name)
assert actual == expected, (
f"RuntimeConfig.{field.name} did not survive pack_id override: "
f"expected {expected!r}, got {actual!r}"
)
def test_all_runtime_config_flags_survive_frame_pack_override() -> None:
custom = RuntimeConfig(
composed_surface=True,
thread_anaphora=True,
forward_graph_constraint=True,
identity_pack="precision_first_v1",
ethics_pack="default_general_ethics_v1",
)
runtime = ChatRuntime(frame_pack="en", config=custom)
assert runtime.config.composed_surface is True
assert runtime.config.thread_anaphora is True
assert runtime.config.forward_graph_constraint is True
assert runtime.config.identity_pack == "precision_first_v1"
assert runtime.config.ethics_pack == "default_general_ethics_v1"
def test_no_override_path_passes_config_through_unchanged() -> None:
"""When neither pack_id nor frame_pack is supplied, the user's
config is preserved verbatim (it is the same object identity)."""
custom = RuntimeConfig(composed_surface=True)
runtime = ChatRuntime(config=custom)
# Same object: no reconstruction happened.
assert runtime.config is custom
assert runtime.config.composed_surface is True
def test_chat_method_exists() -> None:
"""Smoke: the user-facing public methods are present."""
rt = ChatRuntime()
assert callable(getattr(rt, "chat", None))
assert callable(getattr(rt, "respond", None))
assert callable(getattr(rt, "achat", None))
assert callable(getattr(rt, "arespond", None))