# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 """Pure-helper tests for fusion_helpdesk_central.utils. No Odoo env, no network — these run in any environment with `markupsafe` installed. Mirrors the pattern in fusion_helpdesk/tests/test_utils.py. """ from unittest.mock import patch from odoo.tests import TransactionCase, tagged from odoo.addons.fusion_helpdesk_central.utils import ( SUMMARY_PROMPT, OPENAI_PROMPT_MAX_CHARS, build_summary_prompt, call_openai_chat, format_engagement_chatter, truncate_for_openai, ) @tagged('post_install', '-at_install', 'fusion_helpdesk_central') class TestBuildSummaryPrompt(TransactionCase): def test_includes_title_description_and_messages(self): prompt = build_summary_prompt( 'My ticket', 'Detailed description here', [{'author': 'Alice', 'date': '2026-05-27 10:00:00', 'body_plain': 'first reply'}], ) self.assertIn('My ticket', prompt) self.assertIn('Detailed description here', prompt) self.assertIn('Alice', prompt) self.assertIn('first reply', prompt) def test_empty_messages_becomes_explicit_marker(self): # The model needs to know "no replies yet" — a blank section would # invite hallucination. prompt = build_summary_prompt('t', 'd', []) self.assertIn('(no replies yet)', prompt) def test_empty_title_and_description_use_fallbacks(self): prompt = build_summary_prompt('', '', []) self.assertIn('(untitled)', prompt) self.assertIn('(no description)', prompt) def test_empty_message_body_is_marked_not_dropped(self): prompt = build_summary_prompt('t', 'd', [ {'author': 'A', 'date': 'X', 'body_plain': ''}, ]) # An empty body must still produce a line so author + date context # survives; '(empty)' marker keeps the model honest. self.assertIn('(empty)', prompt) def test_findings_included_in_prompt(self): # Findings is the support engineer's analysis — must reach OpenAI. prompt = build_summary_prompt( 't', 'd', [], findings='Scope is small. ~2h of work.', ) self.assertIn('Scope is small. ~2h of work.', prompt) def test_findings_absent_uses_explicit_marker(self): # Empty findings collapses to an explicit marker so the model # doesn't read a blank section as "missing". prompt = build_summary_prompt('t', 'd', [], findings='') self.assertIn('none provided', prompt) @tagged('post_install', '-at_install', 'fusion_helpdesk_central') class TestTruncateForOpenAI(TransactionCase): def test_no_truncation_when_under_limit(self): prompt = 'x' * 100 self.assertEqual(truncate_for_openai(prompt, max_chars=200), prompt) def test_truncation_appends_marker_and_respects_cap(self): prompt = 'x' * 500 out = truncate_for_openai(prompt, max_chars=200) self.assertLessEqual(len(out), 200) self.assertIn('truncated', out.lower()) def test_default_cap_is_8000(self): # Regression guard — flipping this default has real $$ implications. self.assertEqual(OPENAI_PROMPT_MAX_CHARS, 8000) @tagged('post_install', '-at_install', 'fusion_helpdesk_central') class TestFormatEngagementChatter(TransactionCase): def test_approve_without_comment(self): out = str(format_engagement_chatter('approved', 'Kris')) self.assertIn('Approved', out) self.assertIn('Kris', out) self.assertIn('✓', out) # No blockquote when no comment. self.assertNotIn('blockquote', out) def test_reject_with_comment(self): out = str(format_engagement_chatter( 'rejected', 'Kris', comment='not in scope this quarter')) self.assertIn('Rejected', out) self.assertIn('✗', out) self.assertIn('blockquote', out) self.assertIn('not in scope this quarter', out) def test_comment_html_escaped(self): # XSS guard: a malicious comment must not inject script tags. out = str(format_engagement_chatter( 'approved', 'Kris', comment='')) self.assertNotIn('