test: structured CannedQueue fixture builder for classifier mocks (T116)

Phase 4.5 carry-over from Phase 3. Tests across test_turn_flow.py,
test_meanwhile_turn_flow.py, and the phase3/4 integration suites built
positional canned-response arrays for MockLLMClient — adding a new
classifier call to a code path required updating the array index in
many places.

This change ships tests/fixtures.py with a fluent CannedQueue builder
that lets tests declare classifier expectations by name and call order
instead of by index. Each method appends one item to an internal queue
and returns self for chaining; build() emits the flat list[str] queue
that MockLLMClient(canned=...) already consumes. The mock's contract
is unchanged.

Builder methods cover: parse_turn, detect_addressee, state_update
(with zero_state alias), detect_interjection,
detect_interjection_targeted, detect_scene_close,
detect_event_transitions, summarize_scene_pov, detect_threads,
meanwhile_digest, score_significance, and a narrative() helper for
streaming bot beats. raw() is a passthrough escape hatch.

Migration scope: ship the builder + 2 sanity tests + migrate 3
representative tests in test_turn_flow.py as proof of concept
(test_single_bot_turn_no_guest_regression,
test_turn_with_event_transition_appends_started_event,
test_turn_with_no_active_events_skips_classifier). The remaining
positional-array tests stay as-is; the builder docstring documents
the migration template for Phase 5 work.
This commit is contained in:
Joseph Doherty
2026-04-27 07:03:20 -04:00
parent cfc05a140c
commit 4afaf01de7
3 changed files with 563 additions and 32 deletions
+40 -32
View File
@@ -22,6 +22,7 @@ from chat.db.connection import open_db
from chat.eventlog.log import append_and_apply, append_event
from chat.eventlog.projector import project
from chat.llm.mock import MockLLMClient
from tests.fixtures import CannedQueue
@pytest.fixture
@@ -362,14 +363,20 @@ def test_single_bot_turn_no_guest_regression(app_state_setup, tmp_path):
the chat has no guest, so ``detect_interjection`` is NOT invoked.
Ends with one user_turn, one assistant_turn, two edge_updates, and a
single ``memory_written``.
T116: migrated to :class:`tests.fixtures.CannedQueue` as a proof of
concept for the structured canned-queue builder.
"""
_seed(tmp_path / "test.db")
canned_parse = json.dumps(
{"segments": [{"kind": "dialogue", "text": "hello"}]}
)
mock = _override_llm(
[canned_parse, "Hi there.", _zero_state(), _zero_state()]
canned = (
CannedQueue()
.parse_turn(segments=[{"kind": "dialogue", "text": "hello"}])
.narrative("Hi there.")
.state_update()
.state_update()
.build()
)
mock = _override_llm(canned)
try:
response = app_state_setup.post(
"/chats/chat_bot_a/turns", data={"prose": "hello"}
@@ -979,29 +986,25 @@ def test_turn_with_event_transition_appends_started_event(
},
)
canned_parse = json.dumps(
{"segments": [{"kind": "dialogue", "text": "they arrived"}]}
)
canned_event_decision = json.dumps(
{
"transitions": [
{
"event_id": "evt_1",
"new_status": "active",
"reason": "they arrived",
}
]
}
)
mock = _override_llm(
[
canned_parse,
"They walk in.",
_zero_state(),
_zero_state(),
canned_event_decision,
]
# T116: migrated to :class:`tests.fixtures.CannedQueue`.
canned = (
CannedQueue()
.parse_turn(segments=[{"kind": "dialogue", "text": "they arrived"}])
.narrative("They walk in.")
.state_update()
.state_update()
.detect_event_transitions(
[
{
"event_id": "evt_1",
"new_status": "active",
"reason": "they arrived",
}
]
)
.build()
)
mock = _override_llm(canned)
try:
response = app_state_setup.post(
"/chats/chat_bot_a/turns", data={"prose": "they arrived"}
@@ -1155,18 +1158,23 @@ def test_turn_with_no_active_events_skips_classifier(app_state_setup, tmp_path):
short-circuits without an LLM call (per T52). The canned queue must
therefore have ZERO event-detection slots — same shape as the
Phase 2 no-guest baseline.
T116: migrated to :class:`tests.fixtures.CannedQueue`.
"""
_seed(tmp_path / "test.db")
canned_parse = json.dumps(
{"segments": [{"kind": "dialogue", "text": "hello"}]}
)
# Only 4 slots: parse + narrative + 2 state-updates. NO extra slot for
# event-detection — non-existent active_events causes the helper to
# short-circuit before pulling from the queue.
mock = _override_llm(
[canned_parse, "Hi there.", _zero_state(), _zero_state()]
canned = (
CannedQueue()
.parse_turn(segments=[{"kind": "dialogue", "text": "hello"}])
.narrative("Hi there.")
.state_update()
.state_update()
.build()
)
mock = _override_llm(canned)
try:
response = app_state_setup.post(
"/chats/chat_bot_a/turns", data={"prose": "hello"}