rho-coding-agent 1.24.0

A lightweight agent harness inspired by Pi
Documentation
use std::collections::VecDeque;

use ratatui::text::Line;

use super::*;
use crate::tui::tests::test_app;

fn prompt(text: &str) -> QueuedPrompt {
    QueuedPrompt {
        prompt: text.into(),
        display_prompt: text.into(),
        paste_segments: Vec::new(),
    }
}

fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
    KeyEvent::new(code, modifiers)
}

fn line_text(line: &Line<'_>) -> String {
    line.spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect()
}

#[test]
fn alt_up_prioritizes_latest_local_steer_over_follow_up() {
    let mut app = test_app();
    app.pending.push_follow_up(prompt("future turn"));
    app.pending
        .steering_prompts_mut()
        .push_back(prompt("first steer"));
    app.pending
        .steering_prompts_mut()
        .push_back(prompt("latest steer"));

    assert!(app.handle_pending_input_key(key(KeyCode::Up, KeyModifiers::ALT)));

    assert_eq!(app.input_ui.text(), "latest steer");
    assert_eq!(
        *app.pending.steering_prompts(),
        VecDeque::from([prompt("first steer")])
    );
    assert_eq!(
        *app.pending.queued_prompts(),
        VecDeque::from([prompt("future turn")])
    );
}

#[test]
fn alt_up_requests_retraction_for_accepted_steer() {
    let mut app = test_app();
    let id = rho_sdk::SteeringId::new();
    app.pending
        .accepted_steering_mut()
        .push_back(AcceptedSteering {
            id: id.clone(),
            prompt: prompt("retract me"),
        });

    app.handle_pending_input_key(key(KeyCode::Up, KeyModifiers::ALT));

    assert!(matches!(
        app.pending.input_action(),
        Some(PendingInputAction::EditAccepted {
            id: ref action_id,
            ..
        }) if action_id == &id
    ));
    assert!(app.input_ui.text().is_empty());
    assert_eq!(app.pending.accepted_steering().len(), 1);
}

#[test]
fn alt_up_preserves_nonempty_composer() {
    let mut app = test_app();
    app.input_ui.set_text("draft".to_string());
    app.input_ui.set_cursor(app.input_char_len());
    app.pending.push_follow_up(prompt("future turn"));

    app.handle_pending_input_key(key(KeyCode::Up, KeyModifiers::ALT));

    assert_eq!(app.input_ui.text(), "draft");
    assert_eq!(
        *app.pending.queued_prompts(),
        VecDeque::from([prompt("future turn")])
    );
    assert_eq!(
        app.history.last_status_notice(),
        Some("clear the composer before editing pending input")
    );
}

#[test]
fn applied_event_preserves_selection_of_a_later_pending_item() {
    let mut app = test_app();
    let applied = rho_sdk::SteeringId::new();
    app.pending
        .accepted_steering_mut()
        .push_back(AcceptedSteering {
            id: applied.clone(),
            prompt: prompt("first steer"),
        });
    app.pending
        .accepted_steering_mut()
        .push_back(AcceptedSteering {
            id: rho_sdk::SteeringId::new(),
            prompt: prompt("second steer"),
        });
    app.pending.push_follow_up(prompt("future turn"));
    app.pending.input_panel_mut().selected = 2;

    app.mark_steering_applied(&[applied]);

    assert_eq!(app.pending.input_panel().selected, 1);
    let lines = app.pending_input_lines(80);
    assert!(line_text(&lines[2]).contains("▸ NEXT"));
}

#[test]
fn backspace_removes_the_selected_follow_up() {
    let mut app = test_app();
    app.pending.push_follow_up(prompt("first"));
    app.pending.push_follow_up(prompt("second"));
    app.select_pending_recall_target();

    app.handle_pending_input_key(key(KeyCode::Char('q'), KeyModifiers::ALT));
    app.handle_pending_input_key(key(KeyCode::Up, KeyModifiers::NONE));
    app.handle_pending_input_key(key(KeyCode::Backspace, KeyModifiers::NONE));

    assert_eq!(
        *app.pending.queued_prompts(),
        VecDeque::from([prompt("second")])
    );
}

#[test]
fn rejected_steering_acceptance_becomes_a_follow_up_without_failing_the_turn() {
    let mut app = test_app();
    let queued = prompt("continue after this turn");
    let request = PendingInputRequest::Accept {
        prompt: queued.clone(),
        receipt: Box::pin(std::future::pending()),
    };
    let completion = PendingInputCompletion::Accepted(Err(rho_sdk::Error::InvalidHostResponse {
        message: "run completed before accepting steering input".into(),
    }));

    let failure = app.finish_pending_input_request(request, completion);

    assert_eq!(failure, None);
    assert!(app.pending.steering_prompts().is_empty());
    assert_eq!(*app.pending.queued_prompts(), VecDeque::from([queued]));
    assert_eq!(
        app.history.last_status_notice(),
        Some(
            "steer queued as follow-up: invalid host response: run completed before accepting steering input"
        )
    );
}

#[test]
fn applied_event_removes_only_matching_steering() {
    let mut app = test_app();
    let applied = rho_sdk::SteeringId::new();
    let pending = rho_sdk::SteeringId::new();
    app.pending
        .accepted_steering_mut()
        .push_back(AcceptedSteering {
            id: applied.clone(),
            prompt: prompt("applied"),
        });
    app.pending
        .accepted_steering_mut()
        .push_back(AcceptedSteering {
            id: pending.clone(),
            prompt: prompt("pending"),
        });

    app.mark_steering_applied(&[applied]);

    assert_eq!(app.pending.accepted_steering().len(), 1);
    assert_eq!(app.pending.accepted_steering()[0].id, pending);
}