# -*- 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)
@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('