93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from backend.services.regulars_v2 import Regular, load_regular, REGULARS_DIR, SILAS_DIR
|
|
|
|
|
|
def test_load_regular_parses_frontmatter_and_body(tmp_path):
|
|
lore_file = tmp_path / "silas.md"
|
|
lore_file.write_text("""---
|
|
name: Silas
|
|
voice: Dennis
|
|
age: 54
|
|
arc_state: Cult is splintering after the eclipse failure
|
|
---
|
|
|
|
# Silas
|
|
|
|
Silas runs a small desert cult outside Truth or Consequences...
|
|
|
|
## Arc Log
|
|
|
|
- 2026-03-01: First call, introduced the cult
|
|
- 2026-03-20: Prophesied the eclipse
|
|
""")
|
|
reg = load_regular(lore_file)
|
|
assert reg.name == "Silas"
|
|
assert reg.voice == "Dennis"
|
|
assert reg.age == 54
|
|
assert "splintering" in reg.arc_state
|
|
assert "Silas runs a small desert cult" in reg.lore_body
|
|
|
|
|
|
def test_evaluate_promotion_returns_arc_plan_when_worthy():
|
|
from backend.services.regulars_v2 import evaluate_promotion
|
|
fake_response = {
|
|
"promote": True,
|
|
"arc_plan": "3 episodes. He'll start distant, then reveal he's actually the one who damaged the car, then resolve with an apology.",
|
|
"reason": "Has clear internal conflict with room to grow",
|
|
}
|
|
with patch("backend.services.regulars_v2._call_sonnet", new=AsyncMock(return_value=fake_response)):
|
|
result = asyncio.run(evaluate_promotion(caller_name="Bobby", call_transcript="..."))
|
|
assert result["promote"] is True
|
|
assert "3 episodes" in result["arc_plan"]
|
|
|
|
|
|
def test_evaluate_promotion_rejects_when_no_arc():
|
|
from backend.services.regulars_v2 import evaluate_promotion
|
|
fake_response = {"promote": False, "arc_plan": None, "reason": "One-note complaint, no growth"}
|
|
with patch("backend.services.regulars_v2._call_sonnet", new=AsyncMock(return_value=fake_response)):
|
|
result = asyncio.run(evaluate_promotion(caller_name="Carl", call_transcript="..."))
|
|
assert result["promote"] is False
|
|
|
|
|
|
def test_call_sonnet_strips_markdown_fences():
|
|
from backend.services.regulars_v2 import _call_sonnet
|
|
fake_resp = MagicMock()
|
|
fake_resp.raise_for_status = MagicMock()
|
|
fake_resp.json = MagicMock(return_value={
|
|
"choices": [{"message": {"content": "```json\n{\"promote\": true, \"arc_plan\": \"ok\"}\n```"}}],
|
|
"usage": {"prompt_tokens": 10, "completion_tokens": 5},
|
|
})
|
|
mock_client = MagicMock()
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client.post = AsyncMock(return_value=fake_resp)
|
|
|
|
with patch("backend.services.regulars_v2.httpx.AsyncClient", return_value=mock_client), \
|
|
patch("backend.services.regulars_v2.cost_tracker.record_llm_call"):
|
|
result = asyncio.run(_call_sonnet("prompt"))
|
|
assert result["promote"] is True
|
|
assert result["arc_plan"] == "ok"
|
|
|
|
|
|
def test_write_new_regular_creates_lore_file(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("backend.services.regulars_v2.REGULARS_DIR", tmp_path)
|
|
from backend.services.regulars_v2 import write_new_regular
|
|
|
|
write_new_regular(
|
|
name="Bobby",
|
|
voice="Marcus",
|
|
age=34,
|
|
identity_paragraph="A landscaper in Las Cruces who...",
|
|
arc_plan="3 episodes: distant → reveal → apology",
|
|
first_call_summary="Called about damaged car",
|
|
)
|
|
f = tmp_path / "bobby.md"
|
|
assert f.exists()
|
|
body = f.read_text()
|
|
assert "name: Bobby" in body
|
|
assert "voice: Marcus" in body
|
|
assert "A landscaper in Las Cruces" in body
|
|
assert "3 episodes: distant → reveal → apology" in body
|