fix defects D1-D12 batch 1, structured envelope, saf gates + ops kill-switches
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Unit coverage for FDB-005 structured outputs (ENV-18, ENV-19)."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from fjerkroa_bot.ai_responder import AIMessage
|
||||
from fjerkroa_bot.openai_responder import ENVELOPE_RESPONSE_FORMAT, OpenAIResponder
|
||||
|
||||
from .test_bdd_envelope import FakeModelResponder, envelope
|
||||
|
||||
RESPONDER_CONFIG = {"openai-token": "test", "model": "main-model", "system": "s", "history-limit": 5}
|
||||
|
||||
|
||||
def ok_result(content=None):
|
||||
message = Mock(content=content or envelope(answer="x", answer_needed=True), role="assistant", tool_calls=None, refusal=None)
|
||||
return Mock(choices=[Mock(message=message)], usage="usage")
|
||||
|
||||
|
||||
class TestNoRepairPath(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_malformed_output_is_failed_attempt(self):
|
||||
"""ENV-18: malformed output is a failed attempt with backoff — no repair path (replaces ENV-06)."""
|
||||
responder = FakeModelResponder({"system": "s", "history-limit": 5}, "chat")
|
||||
responder.scripted = ["definitely not json", "definitely not json", "definitely not json"]
|
||||
with patch("fjerkroa_bot.ai_responder.asyncio.sleep", new_callable=AsyncMock) as sleep:
|
||||
with self.assertRaises(RuntimeError):
|
||||
await responder.send(AIMessage("alice", "hei", "chat"))
|
||||
self.assertEqual(responder.chat_calls, 3)
|
||||
self.assertGreaterEqual(sleep.await_count, 2)
|
||||
self.assertFalse(hasattr(responder, "fix"))
|
||||
|
||||
async def test_refusal_is_failed_attempt(self):
|
||||
"""ENV-18: a model refusal yields no answer from chat()."""
|
||||
responder = OpenAIResponder(RESPONDER_CONFIG, "chat")
|
||||
refusal_message = Mock(content=None, refusal="I cannot help with that.", tool_calls=None, role="assistant")
|
||||
with patch("fjerkroa_bot.openai_responder.openai_chat", new_callable=AsyncMock) as chat_mock:
|
||||
chat_mock.return_value = Mock(choices=[Mock(message=refusal_message)], usage="usage")
|
||||
answer, _ = await responder.chat([{"role": "user", "content": "hi"}], 10)
|
||||
self.assertIsNone(answer)
|
||||
|
||||
|
||||
class TestEnvelopeSchema(unittest.IsolatedAsyncioTestCase):
|
||||
def test_schema_shape_pinned(self):
|
||||
"""ENV-19: strict envelope schema — exact fields, all required, closed object."""
|
||||
json_schema = ENVELOPE_RESPONSE_FORMAT["json_schema"]
|
||||
schema = json_schema["schema"]
|
||||
expected = {"answer", "answer_needed", "channel", "staff", "picture", "picture_edit", "hack"}
|
||||
self.assertEqual(set(schema["properties"]), expected)
|
||||
self.assertEqual(set(schema["required"]), expected)
|
||||
self.assertFalse(schema["additionalProperties"])
|
||||
self.assertTrue(json_schema["strict"])
|
||||
self.assertEqual(json_schema["name"], "envelope")
|
||||
|
||||
async def test_chat_carries_response_format(self):
|
||||
"""ENV-19: chat calls pass the pinned response_format to the API."""
|
||||
responder = OpenAIResponder(RESPONDER_CONFIG, "chat")
|
||||
with patch("fjerkroa_bot.openai_responder.openai_chat", new_callable=AsyncMock) as chat_mock:
|
||||
chat_mock.return_value = ok_result()
|
||||
answer, _ = await responder.chat([{"role": "user", "content": "hi"}], 10)
|
||||
self.assertIsNotNone(answer)
|
||||
self.assertEqual(chat_mock.await_args.kwargs["response_format"], ENVELOPE_RESPONSE_FORMAT)
|
||||
Reference in New Issue
Block a user