refactor: implement trait-based DI for TdClient and fix stack overflow

Implement complete trait-based dependency injection pattern for TdClient
to enable testing with FakeTdClient mock. Fix critical stack overflow bugs
caused by infinite recursion in trait implementations.

Breaking Changes:
- App is now generic: App<T: TdClientTrait = TdClient>
- All UI and input handlers are generic over TdClientTrait
- TdClient methods now accessed through trait interface

New Files:
- src/tdlib/trait.rs: TdClientTrait definition with 40+ methods
- src/tdlib/client_impl.rs: TdClientTrait impl for TdClient
- tests/helpers/fake_tdclient_impl.rs: TdClientTrait impl for FakeTdClient

Critical Fixes:
- Fix stack overflow in send_message, edit_message, delete_messages
- Fix stack overflow in forward_messages, current_chat_messages
- Fix stack overflow in current_pinned_message
- All methods now call message_manager directly to avoid recursion

Testing:
- FakeTdClient supports configurable auth_state for auth screen tests
- Added pinned message support in FakeTdClient
- All 196+ tests passing (188 tests + 8 benchmarks)

Dependencies:
- Added async-trait = "0.1"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-02 05:42:19 +03:00
parent ed5a4f9c72
commit 8e48d076de
38 changed files with 1053 additions and 161 deletions

View File

@@ -1,10 +1,11 @@
//! Chat list navigation input handling
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crossterm::event::KeyEvent;
/// Обрабатывает ввод в списке чатов
pub async fn handle_chat_list_input(app: &mut App, key: KeyEvent) {
pub async fn handle_chat_list_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement chat list input handling
let _ = (app, key);
}

View File

@@ -7,6 +7,7 @@
//! - Ctrl+F: Search messages in chat
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::types::ChatId;
use crate::utils::{with_timeout, with_timeout_msg};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
@@ -17,7 +18,7 @@ use std::time::Duration;
/// # Returns
///
/// `true` если команда была обработана, `false` если нет
pub async fn handle_global_commands(app: &mut App, key: KeyEvent) -> bool {
pub async fn handle_global_commands<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) -> bool {
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
@@ -55,7 +56,7 @@ pub async fn handle_global_commands(app: &mut App, key: KeyEvent) -> bool {
}
/// Обрабатывает загрузку и отображение закреплённых сообщений
async fn handle_pinned_messages(app: &mut App) {
async fn handle_pinned_messages<T: TdClientTrait>(app: &mut App<T>) {
if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
if let Some(chat_id) = app.get_selected_chat_id() {
app.status_message = Some("Загрузка закреплённых...".to_string());

View File

@@ -1,10 +1,11 @@
//! Message input handling when chat is open
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crossterm::event::KeyEvent;
/// Обрабатывает ввод когда открыт чат
pub async fn handle_messages_input(app: &mut App, key: KeyEvent) {
pub async fn handle_messages_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement messages input handling
let _ = (app, key);
}

View File

@@ -7,28 +7,29 @@
//! - Forward mode
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crossterm::event::KeyEvent;
/// Обрабатывает ввод в режиме закреплённых сообщений
pub async fn handle_pinned_input(app: &mut App, key: KeyEvent) {
pub async fn handle_pinned_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement pinned messages input handling
let _ = (app, key);
}
/// Обрабатывает ввод в режиме выбора реакции
pub async fn handle_reaction_picker_input(app: &mut App, key: KeyEvent) {
pub async fn handle_reaction_picker_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement reaction picker input handling
let _ = (app, key);
}
/// Обрабатывает ввод в режиме подтверждения удаления
pub async fn handle_delete_confirmation_input(app: &mut App, key: KeyEvent) {
pub async fn handle_delete_confirmation_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement delete confirmation input handling
let _ = (app, key);
}
/// Обрабатывает ввод в режиме пересылки
pub async fn handle_forward_input(app: &mut App, key: KeyEvent) {
pub async fn handle_forward_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement forward mode input handling
let _ = (app, key);
}

View File

@@ -1,10 +1,11 @@
//! Profile mode input handling
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crossterm::event::KeyEvent;
/// Обрабатывает ввод в режиме профиля
pub async fn handle_profile_input(app: &mut App, key: KeyEvent) {
pub async fn handle_profile_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement profile input handling
// Временно делегируем обратно в main_input
let _ = (app, key);

View File

@@ -1,16 +1,17 @@
//! Search mode input handling (chat search and message search)
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crossterm::event::KeyEvent;
/// Обрабатывает ввод в режиме поиска чатов
pub async fn handle_chat_search_input(app: &mut App, key: KeyEvent) {
pub async fn handle_chat_search_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement chat search input handling
let _ = (app, key);
}
/// Обрабатывает ввод в режиме поиска сообщений
pub async fn handle_message_search_input(app: &mut App, key: KeyEvent) {
pub async fn handle_message_search_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
// TODO: Implement message search input handling
let _ = (app, key);
}