codetether-agent 4.5.7

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
//! State module tests covering cursor movement, scrolling, model picker, and timing.

use super::AppState;
use std::time::{Duration, Instant};

#[test]
fn utf8_cursor_moves_by_character_not_byte() {
    let mut state = AppState {
        input: "aé🙂z".to_string(),
        ..Default::default()
    };
    state.move_cursor_end();
    assert_eq!(state.input_cursor, 4);
    state.move_cursor_left();
    assert_eq!(state.input_cursor, 3);
    state.move_cursor_left();
    assert_eq!(state.input_cursor, 2);
    state.move_cursor_right();
    assert_eq!(state.input_cursor, 3);
}

#[test]
fn utf8_backspace_removes_full_character() {
    let mut state = AppState {
        input: "aé🙂z".to_string(),
        ..Default::default()
    };
    state.move_cursor_end();
    state.move_cursor_left();
    state.delete_backspace();
    assert_eq!(state.input, "aéz");
    assert_eq!(state.input_cursor, 2);
}

#[test]
fn utf8_delete_forward_removes_full_character() {
    let mut state = AppState {
        input: "aé🙂z".to_string(),
        input_cursor: 1,
        ..Default::default()
    };
    state.delete_forward();
    assert_eq!(state.input, "a🙂z");
    assert_eq!(state.input_cursor, 1);
}

#[test]
fn utf8_insert_respects_character_cursor() {
    let mut state = AppState {
        input: "a🙂z".to_string(),
        input_cursor: 2,
        ..Default::default()
    };
    state.insert_char('é');
    assert_eq!(state.input, "a🙂éz");
    assert_eq!(state.input_cursor, 3);
}

#[test]
fn model_filter_limits_visible_models() {
    let mut state = AppState {
        available_models: vec![
            "zai/glm-5".to_string(),
            "openai/gpt-4o".to_string(),
            "openrouter/qwen/qwen3-coder".to_string(),
        ],
        ..Default::default()
    };
    state.model_filter_push('g');
    state.model_filter_push('p');
    state.model_filter_push('t');
    assert_eq!(state.filtered_models(), vec!["openai/gpt-4o"]);
    assert_eq!(state.selected_model(), Some("openai/gpt-4o"));
}

#[test]
fn scroll_up_from_follow_latest_enters_manual_scroll_mode() {
    let mut state = AppState::default();
    state.set_chat_max_scroll(25);
    state.scroll_to_bottom();
    state.scroll_up(1);
    assert_eq!(state.chat_scroll, 24);
}

#[test]
fn scroll_down_returns_to_follow_latest_at_bottom() {
    let mut state = AppState::default();
    state.set_chat_max_scroll(25);
    state.chat_scroll = 24;
    state.scroll_down(1);
    assert_eq!(state.chat_scroll, 1_000_000);
}

#[test]
fn tool_preview_scroll_clamps_to_rendered_max() {
    let mut state = AppState::default();
    state.set_tool_preview_max_scroll(4);
    state.scroll_tool_preview_down(10);
    assert_eq!(state.tool_preview_scroll, 4);
    state.scroll_tool_preview_up(2);
    assert_eq!(state.tool_preview_scroll, 2);
    state.set_tool_preview_max_scroll(1);
    assert_eq!(state.tool_preview_scroll, 1);
}

#[test]
fn request_timing_captures_first_and_last_token() {
    let mut state = AppState::default();
    state.processing_started_at = Some(
        Instant::now()
            .checked_sub(Duration::from_millis(12))
            .unwrap(),
    );
    state.note_text_token();
    let first = state
        .current_request_first_token_ms
        .expect("first token should be recorded");
    assert_eq!(state.current_request_last_token_ms, Some(first));
    state.processing_started_at = Some(
        Instant::now()
            .checked_sub(Duration::from_millis(34))
            .unwrap(),
    );
    state.note_text_token();
    let last = state
        .current_request_last_token_ms
        .expect("last token should be recorded");
    assert!(last >= first);
    state.complete_request_timing();
    assert_eq!(state.last_request_first_token_ms, Some(first));
    assert_eq!(state.last_request_last_token_ms, Some(last));
    assert!(state.processing_started_at.is_none());
    assert!(state.current_request_first_token_ms.is_none());
    assert!(state.current_request_last_token_ms.is_none());
}