feat: thread-detection service (T55)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Tests for the thread-detection service (T55).
|
||||
|
||||
On scene close, the transcript is classified to detect open threads
|
||||
(unresolved arcs, dangling questions, promises made). The service can
|
||||
also signal **update** to an existing thread when the scene developed
|
||||
it, or **close** when the scene resolved it.
|
||||
|
||||
These tests cover:
|
||||
|
||||
* A new thread the scene introduced — action="open" with a fresh title.
|
||||
* An update to an existing thread — action="update" with
|
||||
``existing_thread_id`` referencing the prior thread.
|
||||
* Classifier failure — three bad responses degrade to an empty
|
||||
candidates list (graceful degradation, §3.3).
|
||||
* Empty transcript short-circuits before any classifier call.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from chat.llm.mock import MockLLMClient
|
||||
from chat.services.thread_detection import (
|
||||
ThreadCandidate,
|
||||
ThreadDetectionResult,
|
||||
detect_threads,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detects_new_thread_open():
|
||||
canned = json.dumps(
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"action": "open",
|
||||
"title": "Maya's job hunt",
|
||||
"summary": "Maya is looking for a new job",
|
||||
"existing_thread_id": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
mock = MockLLMClient(canned=[canned])
|
||||
result = await detect_threads(
|
||||
mock,
|
||||
classifier_model="x",
|
||||
scene_transcript=[
|
||||
{"speaker": "Maya", "text": "I need to find a new job soon."},
|
||||
{"speaker": "Sam", "text": "What kind of role are you looking for?"},
|
||||
],
|
||||
open_threads=[],
|
||||
)
|
||||
assert isinstance(result, ThreadDetectionResult)
|
||||
assert len(result.candidates) == 1
|
||||
cand = result.candidates[0]
|
||||
assert isinstance(cand, ThreadCandidate)
|
||||
assert cand.action == "open"
|
||||
assert cand.title == "Maya's job hunt"
|
||||
assert cand.summary == "Maya is looking for a new job"
|
||||
assert cand.existing_thread_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detects_update_to_existing_thread():
|
||||
canned = json.dumps(
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"action": "update",
|
||||
"title": "",
|
||||
"summary": "Maya interviewed at Acme today",
|
||||
"existing_thread_id": "thr_jobhunt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
mock = MockLLMClient(canned=[canned])
|
||||
result = await detect_threads(
|
||||
mock,
|
||||
classifier_model="x",
|
||||
scene_transcript=[
|
||||
{"speaker": "Maya", "text": "I had the Acme interview today."},
|
||||
{"speaker": "Sam", "text": "How did it go?"},
|
||||
],
|
||||
open_threads=[
|
||||
{
|
||||
"thread_id": "thr_jobhunt",
|
||||
"title": "Maya's job hunt",
|
||||
"summary": "Maya is looking for a new job",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert len(result.candidates) == 1
|
||||
cand = result.candidates[0]
|
||||
assert cand.action == "update"
|
||||
assert cand.existing_thread_id == "thr_jobhunt"
|
||||
assert cand.summary == "Maya interviewed at Acme today"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_classifier_failure_returns_empty():
|
||||
"""Three malformed classifier responses → empty candidates list."""
|
||||
mock = MockLLMClient(canned=["not json", "still not json", "{bad"])
|
||||
result = await detect_threads(
|
||||
mock,
|
||||
classifier_model="x",
|
||||
scene_transcript=[
|
||||
{"speaker": "Maya", "text": "Anything could happen here."},
|
||||
],
|
||||
open_threads=[],
|
||||
)
|
||||
assert result.candidates == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_transcript_short_circuits():
|
||||
"""Empty transcript short-circuits — classifier must not be called."""
|
||||
mock = MockLLMClient(canned=[])
|
||||
result = await detect_threads(
|
||||
mock,
|
||||
classifier_model="x",
|
||||
scene_transcript=[],
|
||||
open_threads=[],
|
||||
)
|
||||
assert result.candidates == []
|
||||
Reference in New Issue
Block a user