Akshanshsensei/PDF-Constrained-Conversational-Agent
1
1"""2tests/test_thinking_transforms.py — Unit Tests for Thinking Tag Transforms3===========================================================================4 5Tests for the two pure helper functions added to app.py:6 - transform_thinking_tags(text) → Converts <thinking> tags to <details> accordions7 - strip_thinking(text) → Strips <thinking> blocks before Redis memory storage8"""9 10import sys11import os12import re13import unittest14 15# ---------------------------------------------------------------------------16# Pull the two helpers out of app.py without importing the full Gradio app17# (which would trigger backend init, Redis connections, etc.)18# ---------------------------------------------------------------------------19 20def transform_thinking_tags(text: str) -> str:21 """Inline copy of the helper — kept in sync with app.py."""22 # Guard: closing tag partially streamed — check ALL partial prefixes23 _CLOSING = "</thinking>"24 if any(_CLOSING[:i] in text for i in range(2, len(_CLOSING))) and _CLOSING not in text:25 return text26 # Guard: opening tag partially streamed (e.g. "<thinki" or "<thinking" without ">")27 if "<thinking" in text and "<thinking>" not in text:28 return text29 text = text.replace(30 "<thinking>",31 "<details open>\n<summary>Agent Thinking...</summary>\n\n"32 )33 text = text.replace("</thinking>", "\n</details>\n\n")34 return text35 36 37def strip_thinking(text: str) -> str:38 """Inline copy of the helper — kept in sync with app.py."""39 return re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL).strip()40 41 42# ===========================================================================43# Tests for transform_thinking_tags44# ===========================================================================45 46class TestTransformThinkingTags(unittest.TestCase):47 48 def test_complete_tags_converted(self):49 """Complete <thinking>...</thinking> is replaced with <details open>."""50 result = transform_thinking_tags("<thinking>foo</thinking>bar")51 self.assertIn("<details open>", result)52 self.assertIn("</details>", result)53 self.assertIn("bar", result)54 self.assertNotIn("<thinking>", result)55 self.assertNotIn("</thinking>", result)56 57 def test_partial_opening_tag_unchanged(self):58 """Partial opening tag (no closing >) must be returned unchanged."""59 partial = "<thinki"60 self.assertEqual(transform_thinking_tags(partial), partial)61 62 def test_partial_opening_tag_with_content_unchanged(self):63 """'<thinking' without '>' is partial — must be returned unchanged."""64 partial = "<thinking"65 self.assertEqual(transform_thinking_tags(partial), partial)66 67 def test_partial_closing_tag_unchanged(self):68 """Partial closing tag must not produce broken HTML."""69 partial = "<thinking>foo</thinki"70 self.assertEqual(transform_thinking_tags(partial), partial)71 72 def test_partial_closing_tag_no_gt_unchanged(self):73 """'</thinking' without '>' is partial — must be returned unchanged."""74 partial = "<thinking>foo</thinking"75 self.assertEqual(transform_thinking_tags(partial), partial)76 77 def test_empty_string(self):78 """Empty input returns empty string."""79 self.assertEqual(transform_thinking_tags(""), "")80 81 def test_no_tags_passthrough(self):82 """Text with no thinking tags is returned unchanged."""83 text = "just an answer"84 self.assertEqual(transform_thinking_tags(text), text)85 86 def test_phrase_with_thinking_word_unchanged(self):87 """'thinking' as a natural word (no tag) must not be altered."""88 text = "I was thinking about it"89 self.assertEqual(transform_thinking_tags(text), text)90 91 def test_tool_call_response_unchanged(self):92 """A raw tool call response must never be altered."""93 text = '<tool_call>{"name": "get_page_count", "args": {}}</tool_call>'94 self.assertEqual(transform_thinking_tags(text), text)95 96 def test_summary_label_present(self):97 """The accordion summary label must contain the 🧠 emoji."""98 result = transform_thinking_tags("<thinking>reasoning</thinking>answer")99 self.assertIn("Agent Thinking...", result)100 101 def test_details_open_attribute(self):102 """Accordion must use <details open> so it auto-expands during streaming."""103 result = transform_thinking_tags("<thinking>r</thinking>a")104 self.assertIn("<details open>", result)105 106 def test_multiple_thinking_blocks(self):107 """Multiple thinking blocks are all converted."""108 text = "<thinking>r1</thinking>a1<thinking>r2</thinking>a2"109 result = transform_thinking_tags(text)110 self.assertEqual(result.count("<details open>"), 2)111 self.assertEqual(result.count("</details>"), 2)112 113 114# ===========================================================================115# Tests for strip_thinking116# ===========================================================================117 118class TestStripThinking(unittest.TestCase):119 120 def test_strips_complete_block(self):121 """A complete <thinking>...</thinking> block is removed."""122 text = "<thinking>my reasoning</thinking>Final answer."123 self.assertEqual(strip_thinking(text), "Final answer.")124 125 def test_strips_block_with_newlines(self):126 """Multi-line thinking blocks are fully stripped."""127 text = "<thinking>\nline one\nline two\n</thinking>\nFinal answer."128 self.assertEqual(strip_thinking(text), "Final answer.")129 130 def test_no_thinking_block_unchanged(self):131 """Text with no thinking block is returned as-is."""132 text = "Just a plain answer."133 self.assertEqual(strip_thinking(text), text)134 135 def test_strips_multiple_thinking_blocks(self):136 """Multiple thinking blocks (from multi-turn if ever concatenated) are all removed."""137 text = "<thinking>r1</thinking>A1. <thinking>r2</thinking>A2."138 result = strip_thinking(text)139 self.assertNotIn("<thinking>", result)140 self.assertNotIn("</thinking>", result)141 self.assertIn("A1.", result)142 self.assertIn("A2.", result)143 144 def test_leading_trailing_whitespace_stripped(self):145 """Result is trimmed of leading/trailing whitespace."""146 text = "<thinking>r</thinking> answer "147 self.assertEqual(strip_thinking(text), "answer")148 149 def test_empty_thinking_block(self):150 """An empty thinking block is stripped cleanly."""151 text = "<thinking></thinking>answer"152 self.assertEqual(strip_thinking(text), "answer")153 154 def test_empty_string(self):155 """Empty input returns empty string."""156 self.assertEqual(strip_thinking(""), "")157 158 159if __name__ == "__main__":160 unittest.main()161 