"""Unit coverage for SPEC-001 test-class requirements (ENV-10..12).""" import json import time import unittest from fjerkroa_bot.ai_responder import AIMessage, AIResponder from .test_bdd_envelope import FakeModelResponder def entry(channel: str, text: str = "x"): return {"role": "user", "content": json.dumps({"message": text, "channel": channel})} class TestSystemPromptTemplate(unittest.TestCase): def test_dynamic_context_in_suffix(self): """ENV-10: date/memory reach the system message via the context suffix (ENV-20).""" config = {"system": "Date {date} memory {memory} news {news}", "history-limit": 5, "news": "/nonexistent/news.txt"} responder = AIResponder(config, "chat") responder.memory = "MEMSTR" messages = responder.message(AIMessage("alice", "hei")) system = messages[0]["content"] self.assertIn(time.strftime("%Y-%m-%d"), system) self.assertIn("MEMSTR", system) self.assertNotIn("{news}", system) # placeholders stripped since ENV-20 class TestHistoryShrink(unittest.TestCase): def test_shrink_prefers_busy_channel(self): """ENV-11: entry from the channel exceeding history-per-channel is removed first.""" config = {"system": "s", "history-limit": 4} responder = AIResponder(config, "chat") responder.history = [entry("a", "a0"), entry("a", "a1"), entry("a", "a2"), entry("a", "a3"), entry("b", "b0")] responder.shrink_history_by_one() self.assertEqual(len(responder.history), 4) self.assertNotIn("a0", responder.history[0]["content"]) # oldest busy-channel entry gone def test_shrink_falls_back_to_oldest(self): """ENV-11: when no channel exceeds the cap, the oldest entry overall is removed.""" config = {"system": "s", "history-limit": 4} responder = AIResponder(config, "chat") responder.history = [entry("a", "a0"), entry("b", "b0"), entry("c", "c0")] responder.shrink_history_by_one() self.assertEqual(len(responder.history), 2) self.assertNotIn("a0", responder.history[0]["content"]) class TestRetriesExhausted(unittest.IsolatedAsyncioTestCase): async def test_send_raises_after_three_failures(self): """ENV-12: three model failures -> RuntimeError (revision pending in FDB-004/D1).""" responder = FakeModelResponder({"system": "s", "history-limit": 5}, "chat") # scripted list empty -> chat() returns None every attempt with self.assertRaises(RuntimeError): await responder.send(AIMessage("alice", "hei", "chat")) self.assertEqual(responder.chat_calls, 3)