This commit is contained in:
lyuksovannyy
2026-09-10 12:48:56 +02:00
parent 90678506c6
commit bd34e7b8ba
35 changed files with 7059 additions and 4076 deletions
+694
View File
@@ -0,0 +1,694 @@
"""
Comprehensive simulation and unit test suite for AI Chat Summaries Plugin.
"""
import os
import sys
# Configure sys.path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
import json
import unittest
from unittest.mock import MagicMock, patch
from src.config import (
CONTEXT_WINDOW_BUDGETS,
DEFAULT_CONTEXT_WINDOW,
DEFAULT_MODEL_CUSTOM,
DEFAULT_MODEL_OAUTH_FREE,
DEFAULT_MODEL_OAUTH_PAID,
MAX_SINGLE_MESSAGE_CHARS,
OAUTH_FREE_MODELS,
OAUTH_PAID_MODELS,
OAUTH_TIER_FREE,
OAUTH_TIER_PLUS,
OAUTH_TIER_PRO,
PROVIDER_ANTHROPIC,
PROVIDER_CHATGPT_OAUTH,
PROVIDER_CUSTOM,
PROVIDER_GEMINI,
PROVIDER_OLLAMA,
PROVIDER_OPENAI,
SETTING_ENABLE_THINKING_STREAM,
SETTING_OAUTH_ACCESS_TOKEN,
SETTING_OAUTH_MODEL,
SETTING_OAUTH_USER_EMAIL,
SETTING_OAUTH_USER_TIER,
SETTING_PROVIDER,
STYLE_BRIEF,
STYLE_BULLETS,
STYLE_CUSTOM,
STYLE_DETAILED,
)
from src.localization import LocalizationManager, locali
from src.providers.base import (
build_debug_log,
build_system_prompt,
mask_sensitive_data,
)
from src.providers.builtin import (
AnthropicDirectHandler,
GeminiDirectHandler,
OllamaDirectHandler,
OpenAIDirectHandler,
)
from src.providers.custom import CustomAIHandler
from src.providers.dispatcher import UnifiedDispatcher
from src.providers.oauth import ChatGPTOAuthHandler
from src.services.message_fetcher import FormattedMessage, MessageFetcher
from src.ui.progress_widget import PinnedProgressManager
class MockPlugin:
"""Mock exteraGram plugin environment for testing."""
def __init__(self):
self._settings = {}
self.oauth_handler = ChatGPTOAuthHandler(self)
self.custom_handler = CustomAIHandler(self)
self.dispatcher = UnifiedDispatcher(self)
self.message_fetcher = MessageFetcher(self)
self.progress_manager = PinnedProgressManager(self)
def get_setting(self, key, default=None):
return self._settings.get(key, default)
def set_setting(self, key, value):
self._settings[key] = value
def hook_method(self, target, hook):
return "mock_hook_ref"
def unhook_method(self, ref):
pass
class TestPluginMetadata(unittest.TestCase):
"""Validates that plugin metadata contains non-empty id, name, and standard attributes."""
def test_config_metadata(self):
from src import config
self.assertTrue(bool(config.__id__))
self.assertTrue(bool(config.__name__))
self.assertTrue(bool(config.id))
self.assertTrue(bool(config.name))
self.assertEqual(config.__id__, "ai_chat_summaries")
self.assertEqual(config.__name__, "AI Chat Summaries")
def test_plugin_entrypoint_metadata(self):
from importlib.machinery import SourceFileLoader
plugin_path = os.path.join(BASE_DIR, "ai_chat_summary.plugin")
mod = SourceFileLoader("ai_chat_summaries_bundle", plugin_path).load_module()
self.assertTrue(bool(getattr(mod, "__id__", None)))
self.assertTrue(bool(getattr(mod, "__name__", None)))
self.assertTrue(bool(getattr(mod, "id", None)))
self.assertTrue(bool(getattr(mod, "name", None)))
self.assertEqual(mod.__id__, "ai_chat_summaries")
self.assertEqual(mod.__name__, "AI Chat Summaries")
def test_plugin_lifecycle_and_settings(self):
from importlib.machinery import SourceFileLoader
plugin_path = os.path.join(BASE_DIR, "ai_chat_summary.plugin")
mod = SourceFileLoader("ai_chat_summaries_bundle", plugin_path).load_module()
plugin = mod.AIChatSummariesPlugin()
# exteraGram invokes on_plugin_load / on_plugin_unload
self.assertTrue(hasattr(plugin, "on_plugin_load"))
self.assertTrue(hasattr(plugin, "on_plugin_unload"))
self.assertTrue(hasattr(plugin, "on_menu_click"))
self.assertTrue(plugin.has_settings())
plugin.on_plugin_load()
self.assertTrue(len(plugin.create_settings()) > 0)
plugin.on_plugin_unload()
class TestDiagnostics(unittest.TestCase):
"""Tests structured error retention and copyable reports."""
def setUp(self):
from src import diagnostics
self.diagnostics = diagnostics
diagnostics.clear()
def tearDown(self):
self.diagnostics.uninstall_uncaught_exception_hooks()
self.diagnostics.clear()
def test_structured_error_report_contains_full_context(self):
try:
raise ValueError("broken provider")
except ValueError as exc:
self.diagnostics.record_error("provider.test", exc)
errors = self.diagnostics.get_errors()
self.assertEqual(len(errors), 1)
self.assertEqual(errors[0]["where"], "provider.test")
self.assertEqual(errors[0]["type"], "ValueError")
self.assertEqual(errors[0]["message"], "broken provider")
self.assertIn("Traceback", errors[0]["traceback"])
self.assertTrue(errors[0]["thread"])
report = self.diagnostics.build_errors_report()
self.assertIn("provider.test", report)
self.assertIn("ValueError", report)
self.assertIn("broken provider", report)
self.assertIn("Traceback", report)
def test_logs_and_full_report_include_errors(self):
self.diagnostics.record_error_message(
"pipeline.fetch",
"history failed",
"TLRPC: HISTORY_FAILED",
)
logs = self.diagnostics.build_logs_report()
full = self.diagnostics.build_report()
self.assertIn("pipeline.fetch", logs)
self.assertIn("history failed", logs)
self.assertIn("pipeline.fetch", full)
self.assertIn("TLRPC: HISTORY_FAILED", full)
def test_uncaught_hooks_install_and_restore(self):
old_sys = sys.excepthook
old_thread = getattr(__import__("threading"), "excepthook", None)
self.diagnostics.install_uncaught_exception_hooks()
self.assertIsNot(sys.excepthook, old_sys)
self.diagnostics.uninstall_uncaught_exception_hooks()
self.assertIs(sys.excepthook, old_sys)
if old_thread is not None:
import threading
self.assertIs(threading.excepthook, old_thread)
class TestLocalization(unittest.TestCase):
"""Tests for internationalization dictionaries and fallback behavior."""
def test_english_strings(self):
loc = LocalizationManager("en")
title = loc.get("PLUGIN_NAME")
self.assertEqual(title, "AI Chat Summaries")
self.assertIn("ChatGPT", loc.get("OAUTH_HEADER"))
def test_russian_strings(self):
loc = LocalizationManager("ru")
title = loc.get("PLUGIN_NAME")
self.assertEqual(title, "AI Саммари Чатов")
self.assertIn("ChatGPT", loc.get("OAUTH_HEADER"))
def test_interpolation_and_fallback(self):
loc = LocalizationManager("en")
msg = loc.get("OAUTH_CONNECTED_STATUS", email="user@test.com", tier="PRO")
self.assertIn("user@test.com", msg)
self.assertIn("PRO", msg)
# Fallback to key itself if not found
non_existent = loc.get("NON_EXISTENT_KEY_12345")
self.assertEqual(non_existent, "NON_EXISTENT_KEY_12345")
class TestBaseProviderUtilities(unittest.TestCase):
"""Tests for prompt building, sensitive data masking, and debug log construction."""
def test_sensitive_masking(self):
raw_header = {"Authorization": "Bearer sk-1234567890abcdef12345678", "User-Agent": "test"}
masked = mask_sensitive_data(raw_header)
self.assertTrue("sk-" in str(masked["Authorization"]))
self.assertTrue("..." in str(masked["Authorization"]))
self.assertNotIn("abcdef12345678", str(masked["Authorization"]))
raw_str = "Key: AIzaSyD9876543210abcdef and sk-proj-1122334455667788"
masked_str = mask_sensitive_data(raw_str)
self.assertNotIn("9876543210abcdef", masked_str)
self.assertNotIn("1122334455667788", masked_str)
def test_debug_log_building(self):
log = build_debug_log(
url="https://api.openai.com/v1/chat/completions",
method="POST",
headers={"Authorization": "Bearer sk-testsecret12345"},
payload={"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]},
status_code=401,
response_body={"error": {"message": "Invalid API key"}},
)
self.assertIn("=== AI Chat Summaries Debug Log ===", log)
self.assertIn("Status Code: 401", log)
self.assertIn("Invalid API key", log)
self.assertNotIn("sk-testsecret12345", log)
def test_system_prompt_builder(self):
prompt_brief = build_system_prompt(
style=STYLE_BRIEF,
lang=1, # Russian
chat_title="Team Alpha",
custom_prompt="Focus on deadlines",
)
self.assertIn("Team Alpha", prompt_brief)
self.assertIn("Russian", prompt_brief)
self.assertIn("Focus on deadlines", prompt_brief)
self.assertIn("Brief overview", prompt_brief)
prompt_custom = build_system_prompt(
style=STYLE_CUSTOM,
lang=2, # English
custom_prompt="Provide a 3-bullet list of blockers",
)
self.assertIn("Provide a 3-bullet list of blockers", prompt_custom)
self.assertIn("English", prompt_custom)
class TestChatGPTOAuth(unittest.TestCase):
"""Tests for OAuth PKCE generation, code parsing, and tier-based model selection."""
def setUp(self):
self.plugin = MockPlugin()
self.oauth = self.plugin.oauth_handler
def test_auth_url_generation(self):
url, verifier, state = self.oauth.generate_auth_url()
self.assertTrue(url.startswith("https://auth0.openai.com/authorize?"))
self.assertIn("code_challenge=", url)
self.assertIn("code_challenge_method=S256", url)
self.assertIn("client_id=exteragram_ai_summaries", url)
self.assertTrue(len(verifier) > 40)
self.assertTrue(len(state) > 10)
def test_code_extraction(self):
raw_code = "4/0AeanS0abcdef12345"
self.assertEqual(self.oauth.extract_code_from_input(raw_code), raw_code)
redirect_url = "exteragram://oauth/chatgpt?code=test_code_xyz123&state=state_abc"
self.assertEqual(self.oauth.extract_code_from_input(redirect_url), "test_code_xyz123")
frag_url = "https://example.com/oauth/callback#code=frag_code_999"
self.assertEqual(self.oauth.extract_code_from_input(frag_url), "frag_code_999")
def test_tier_model_mapping(self):
# Free tier
self.plugin.set_setting(SETTING_OAUTH_USER_TIER, OAUTH_TIER_FREE)
free_models = self.oauth.get_tier_models()
self.assertEqual(free_models, OAUTH_FREE_MODELS)
self.assertIn("luna", free_models)
self.assertEqual(self.oauth.get_active_model(), DEFAULT_MODEL_OAUTH_FREE)
# Plus / Pro tier
self.plugin.set_setting(SETTING_OAUTH_USER_TIER, OAUTH_TIER_PLUS)
paid_models = self.oauth.get_tier_models()
self.assertEqual(paid_models, OAUTH_PAID_MODELS)
self.assertIn("terra", paid_models)
self.assertIn("sol", paid_models)
self.assertEqual(self.oauth.get_active_model(), DEFAULT_MODEL_OAUTH_PAID)
@patch("requests.post")
@patch("requests.get")
def test_token_exchange_and_profile_inspection(self, mock_get, mock_post):
# Mock token exchange response
mock_post_resp = MagicMock()
mock_post_resp.status_code = 200
mock_post_resp.json.return_value = {
"access_token": "mock_access_token_123",
"refresh_token": "mock_refresh_token_456",
"expires_in": 3600,
}
mock_post.return_value = mock_post_resp
# Mock user info profile response (Pro tier)
mock_get_resp = MagicMock()
mock_get_resp.status_code = 200
mock_get_resp.json.return_value = {
"email": "dev@company.com",
"subscription": "pro_tier",
}
mock_get.return_value = mock_get_resp
success, data, err = self.oauth.exchange_code_for_tokens("code_test_123")
self.assertTrue(success)
self.assertIsNotNone(data)
self.assertEqual(data["email"], "dev@company.com")
self.assertEqual(data["tier"], OAUTH_TIER_PLUS)
# Verify saved in plugin settings
self.assertEqual(self.plugin.get_setting(SETTING_OAUTH_ACCESS_TOKEN), "mock_access_token_123")
self.assertEqual(self.plugin.get_setting(SETTING_OAUTH_USER_EMAIL), "dev@company.com")
self.assertEqual(self.plugin.get_setting(SETTING_OAUTH_MODEL), DEFAULT_MODEL_OAUTH_PAID)
class TestCustomAIProvider(unittest.TestCase):
"""Tests for Custom AI Provider multi-stage verification and context budgeting."""
def setUp(self):
self.plugin = MockPlugin()
self.custom = self.plugin.custom_handler
@patch("requests.head")
def test_stage1_reachability(self, mock_head):
# Invalid scheme
ok, msg = self.custom.verify_stage1_reachability("ftp://example.com")
self.assertFalse(ok)
# Reachable HTTP 200
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_head.return_value = mock_resp
ok, msg = self.custom.verify_stage1_reachability("https://api.deepseek.com/v1")
self.assertTrue(ok)
self.assertIn("HTTP 200", msg)
@patch("requests.post")
def test_stage2_probe_test(self, mock_post):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"choices": [{"message": {"role": "assistant", "content": "Hello!"}}]
}
mock_post.return_value = mock_resp
ok, msg, debug_log, elapsed = self.custom.verify_stage2_probe_test(
base_url="https://api.deepseek.com/v1",
api_key="sk-test-12345",
model_name="deepseek-chat",
)
self.assertTrue(ok)
self.assertIsNone(debug_log)
# Verify request payload has thinking disabled & 'Hey there'
call_kwargs = mock_post.call_args[1]
json_payload = call_kwargs["json"]
self.assertEqual(json_payload["model"], "deepseek-chat")
self.assertEqual(json_payload["messages"][0]["content"], "Hey there")
self.assertEqual(json_payload["max_tokens"], 10)
self.assertEqual(json_payload["thinking"], {"type": "disabled"})
def test_context_window_budgeting(self):
self.plugin.set_setting("custom_context_window", "8k")
budget_8k = self.custom.get_context_character_budget()
self.assertEqual(budget_8k, CONTEXT_WINDOW_BUDGETS["8k"])
# Generate long message (e.g. 10,000 chars) -> must clamp to 8,000 chars per message
long_msg = "X" * 10_000
short_msg = "Normal message 123"
messages = [long_msg, short_msg]
transcript = self.custom.budget_and_chunk_transcript(messages)
self.assertIn("Normal message 123", transcript)
self.assertIn("[... truncated long message content ...]", transcript)
self.assertTrue(len(transcript) <= budget_8k)
class TestMessageFetcher(unittest.TestCase):
"""Tests for message conversion, sender formatting, and chronological sorting."""
def test_formatted_message_transcript_line(self):
msg = FormattedMessage(
id=101,
date=1700000000,
time_str="2023-11-14 22:13",
sender_name="Alice Wonderland",
sender_username="@alice",
text="Let's schedule our release for Monday 10am UTC.",
reply_to_id=98,
forward_from="Bob",
media_info="[Photo: System architecture]",
)
line = msg.to_transcript_line()
self.assertIn("[2023-11-14 22:13] #101 Alice Wonderland (fwd: Bob, reply-to #98):", line)
self.assertIn("[Photo: System architecture]", line)
self.assertIn("Let's schedule our release for Monday 10am UTC.", line)
def test_chronological_ordering(self):
fetcher = MessageFetcher()
raw_msgs = [
MagicMock(id=3, date=1700000300, message="Third message", from_id=None, peer_id=None, reply_to=None, fwd_from=None, media=None, action=None),
MagicMock(id=1, date=1700000100, message="First message", from_id=None, peer_id=None, reply_to=None, fwd_from=None, media=None, action=None),
MagicMock(id=2, date=1700000200, message="Second message", from_id=None, peer_id=None, reply_to=None, fwd_from=None, media=None, action=None),
]
processed = fetcher._process_messages(raw_msgs, {}, {})
self.assertEqual(len(processed), 3)
self.assertEqual(processed[0].id, 1)
self.assertEqual(processed[1].id, 2)
self.assertEqual(processed[2].id, 3)
class TestThinkingStreamDispatcher(unittest.TestCase):
"""Tests for UnifiedDispatcher streaming token accumulation and thinking parsing."""
def test_streaming_token_accumulator(self):
plugin = MockPlugin()
dispatcher = plugin.dispatcher
dispatcher.reset_stream_state("Custom endpoint", "deepseek-r1", "General Chat")
accumulated_thoughts = []
accumulated_content = []
def listener(t_delta, c_delta):
if t_delta:
accumulated_thoughts.append(t_delta)
if c_delta:
accumulated_content.append(c_delta)
dispatcher.add_stream_listener(listener)
# Simulate streaming chunk delivery
dispatcher._on_stream_chunk("Thinking step 1... ", "")
dispatcher._on_stream_chunk("Thinking step 2.", "")
dispatcher._on_stream_chunk("", "Summary result: ")
dispatcher._on_stream_chunk("", "Everything is completed successfully.")
self.assertEqual(dispatcher.active_stream_state["thinking"], "Thinking step 1... Thinking step 2.")
self.assertEqual(dispatcher.active_stream_state["content"], "Summary result: Everything is completed successfully.")
self.assertEqual(len(accumulated_thoughts), 2)
class TestBuiltinDirectProviders(unittest.TestCase):
"""Tests for OpenAI, Anthropic, Gemini, and Ollama direct API handlers and retry backoff."""
def setUp(self):
self.plugin = MockPlugin()
@patch("requests.post")
def test_openai_direct_summarize(self, mock_post):
self.plugin.set_setting("openai_api_key", "sk-proj-test12345")
self.plugin.set_setting("openai_model", "gpt-4o")
handler = OpenAIDirectHandler(self.plugin)
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"choices": [{"message": {"role": "assistant", "content": "Summary of chat"}}]
}
mock_post.return_value = mock_resp
resp = handler.summarize(
messages_transcript="Alice: Hi\nBob: Hello",
style=STYLE_BRIEF,
lang=0,
enable_stream=False,
)
self.assertTrue(resp.success)
self.assertEqual(resp.text, "Summary of chat")
self.assertEqual(resp.model, "gpt-4o")
@patch("requests.post")
def test_anthropic_direct_with_thinking(self, mock_post):
self.plugin.set_setting("anthropic_api_key", "sk-ant-test12345")
self.plugin.set_setting("anthropic_model", "claude-3-7-sonnet-latest")
handler = AnthropicDirectHandler(self.plugin)
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"content": [
{"type": "thinking", "thinking": "Analyzing topics..."},
{"type": "text", "text": "Claude Summary of chat"}
]
}
mock_post.return_value = mock_resp
resp = handler.summarize(
messages_transcript="Alice: Project roadmap",
style=STYLE_DETAILED,
lang=2,
enable_stream=False,
)
self.assertTrue(resp.success)
self.assertEqual(resp.text, "Claude Summary of chat")
self.assertEqual(resp.reasoning, "Analyzing topics...")
@patch("requests.post")
def test_gemini_direct_summarize(self, mock_post):
self.plugin.set_setting("gemini_api_key", "AIzaSyTest12345")
self.plugin.set_setting("gemini_model", "gemini-2.0-flash")
handler = GeminiDirectHandler(self.plugin)
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"candidates": [
{
"content": {
"parts": [
{"thought": True, "text": "Gemini thought"},
{"text": "Gemini summary text"}
]
}
}
]
}
mock_post.return_value = mock_resp
resp = handler.summarize(
messages_transcript="User: Update?",
style=STYLE_BULLETS,
lang=1,
enable_stream=False,
)
self.assertTrue(resp.success)
self.assertEqual(resp.text, "Gemini summary text")
self.assertEqual(resp.reasoning, "Gemini thought")
@patch("time.sleep")
def test_exponential_backoff_retry(self, mock_sleep):
handler = OpenAIDirectHandler(self.plugin)
attempts = [0]
def flaky_call():
attempts[0] += 1
if attempts[0] < 3:
# Simulate 429 Rate Limit
resp = MagicMock()
resp.status_code = 429
import requests
err = requests.exceptions.HTTPError("429 Rate Limit")
err.response = resp
raise err
return "success_after_retry"
result = handler.execute_with_retry(flaky_call, max_retries=3, base_delay=0.1)
self.assertEqual(result, "success_after_retry")
self.assertEqual(attempts[0], 3)
self.assertEqual(mock_sleep.call_count, 2)
class TestUIWidgetsAndLifecycle(unittest.TestCase):
"""Tests for ProgressManager, PreRequest configuration, and unread count calculations."""
def test_pinned_view_padding_shrink_and_restore(self):
manager = PinnedProgressManager()
mock_pinned_view = MagicMock()
mock_pinned_view.getPaddingRight.return_value = 16
mock_pinned_view.getPaddingLeft.return_value = 16
mock_pinned_view.getPaddingTop.return_value = 8
mock_pinned_view.getPaddingBottom.return_value = 8
manager.attach_pinned_view(mock_pinned_view)
self.assertEqual(manager.original_right_padding, 16)
# Simulate show_progress
mock_activity = MagicMock()
manager.show_progress(mock_activity, 12345, "Tech Chat")
self.assertTrue(manager.is_showing)
# Hide progress -> must restore padding
manager.hide_progress()
self.assertFalse(manager.is_showing)
mock_pinned_view.setPadding.assert_called_with(16, 8, 16, 8)
def test_unread_count_and_offset_calculation(self):
from src.hooks.unread_hook import UnreadBadgeHook
hook = UnreadBadgeHook()
# Case 1: 0 or empty unread -> count 100, offset 0
raw_unread = 0
target_count = min(1000, max(1, raw_unread)) if raw_unread > 0 else 100
target_offset = max(0, raw_unread - target_count) if raw_unread > 0 else 0
self.assertEqual(target_count, 100)
self.assertEqual(target_offset, 0)
# Case 2: 250 unread messages -> count 250, offset 0 (all unread)
raw_unread = 250
target_count = min(1000, max(1, raw_unread)) if raw_unread > 0 else 100
target_offset = max(0, raw_unread - target_count) if raw_unread > 0 else 0
self.assertEqual(target_count, 250)
self.assertEqual(target_offset, 0)
# Case 3: 1500 unread messages -> count 1000, offset 500 (oldest 1000 unread batch)
raw_unread = 1500
target_count = min(1000, max(1, raw_unread)) if raw_unread > 0 else 100
target_offset = max(0, raw_unread - target_count) if raw_unread > 0 else 0
self.assertEqual(target_count, 1000)
self.assertEqual(target_offset, 500)
def test_scroll_to_latest_message_navigation(self):
from src.ui.summary_dialog import scroll_to_message_in_chat
mock_activity = MagicMock()
mock_activity.scrollToMessageId = MagicMock()
# Navigate to message #54321
success = scroll_to_message_in_chat(mock_activity, message_id=54321, topic_id=0)
self.assertTrue(success)
mock_activity.scrollToMessageId.assert_called_with(54321, 0, True, 0, False, 0)
def test_page_down_long_press_timing_contract(self):
from src.hooks import unread_hook
class Event:
def __init__(self, action):
self._action = action
def getAction(self):
return self._action
# Short press: below threshold, should remain native.
with patch.object(unread_hook.time, "monotonic", side_effect=[10.0, 10.2, 10.2]):
unread_hook._gesture_before_dispatch(Event(0))
unread_hook._gesture_before_dispatch(Event(1))
self.assertLess(unread_hook._held_duration(), unread_hook.LONG_PRESS_SECONDS)
# Long press: above threshold, should enter our interception path.
with patch.object(unread_hook.time, "monotonic", side_effect=[20.0, 20.6, 20.6]):
unread_hook._gesture_before_dispatch(Event(0))
unread_hook._gesture_before_dispatch(Event(1))
self.assertGreaterEqual(unread_hook._held_duration(), unread_hook.LONG_PRESS_SECONDS)
class TestPluginEndToEndSimulation(unittest.TestCase):
"""End-to-end simulation of summarization pipeline from trigger to result."""
@patch("requests.post")
def test_full_pipeline_run(self, mock_post):
plugin = MockPlugin()
plugin.set_setting(SETTING_PROVIDER, PROVIDER_CUSTOM)
plugin.set_setting("custom_base_url", "https://api.deepseek.com/v1")
plugin.set_setting("custom_api_key", "sk-deepseek-test")
plugin.set_setting("custom_model", "deepseek-chat")
plugin.set_setting(SETTING_ENABLE_THINKING_STREAM, False)
# Mock AI completion response
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"choices": [{"message": {"role": "assistant", "content": "Summary: Key topics discussed."}}]
}
mock_post.return_value = mock_resp
# Verify unified dispatcher routes properly
provider = plugin.dispatcher.get_active_provider()
self.assertEqual(provider.name, "Custom endpoint")
# Dispatch summarization
response = plugin.dispatcher.run_summary(
messages_transcript="Alice: Deployment ready.\nBob: Approved.",
chat_title="Release Channel",
style=STYLE_BRIEF,
lang=0,
)
self.assertTrue(response.success)
self.assertEqual(response.text, "Summary: Key topics discussed.")
self.assertEqual(response.model, "deepseek-chat")
self.assertEqual(response.provider_name, "Custom endpoint")
if __name__ == "__main__":
unittest.main()