aether-wisp 0.7.16

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use super::support::*;

#[test]
fn completed_large_reply_keeps_its_tail_in_the_viewport() {
    let mut ui = TestUi::new();
    commit_overflowing_reply(&mut ui);
    ui.assert_viewport_contains("finished reply");
    let conversation = ui.conversation_text();
    assert_eq!(ui.conversation_text(), conversation);
}

#[test]
fn committed_message_replacements_publish_a_corrected_transcript_once() {
    let mut ui = TestUi::new();
    commit_overflowing_reply(&mut ui);
    ui.acp_event(session_update(acp::SessionUpdate::AgentMessage(
        acp::AgentMessage::new("overflow-reply").content(vec![acp::ContentBlock::from("corrected answer")]),
    )));
    ui.assert_history_contains("Transcript updated;");
    ui.assert_viewport_contains("corrected answer");
    let history = ui.history_text();
    assert_eq!(ui.history_text(), history);
    ui.acp_event(text_chunk_with_id("overflow-reply", " plus more"));
    ui.assert_viewport_contains("corrected answer plus more");
}

#[test]
fn late_chunk_for_a_fully_committed_message_is_not_lost() {
    let mut ui = TestUi::new();
    commit_overflowing_reply(&mut ui);
    ui.acp_event(text_chunk_with_id("next-reply", &"later response\n\n".repeat(30)));
    ui.assert_history_contains("finished reply");
    ui.acp_event(text_chunk_with_id("overflow-reply", "\n\nlate appendix"));
    let history = ui.history_text();
    let corrected = history.split("superseded.").last().unwrap();
    assert!(corrected.contains("late appendix"));
    assert_eq!(corrected.matches("overflow-line-0").count(), 1);
    assert_eq!(ui.history_text(), history);
}

#[test]
fn committed_message_clear_publishes_a_corrected_transcript() {
    let mut ui = TestUi::new();
    commit_overflowing_reply(&mut ui);
    ui.acp_event(session_update(acp::SessionUpdate::AgentMessage(
        acp::AgentMessage::new("overflow-reply").content(agent_client_protocol::schema::MaybeUndefined::Null),
    )));
    ui.assert_history_contains("Transcript updated;");
    ui.assert_viewport_not_contains("overflow-line");
}

#[test]
fn live_message_upserts_replace_clear_and_append_without_a_history_correction() {
    let mut ui = TestUi::new();
    ui.acp_event(text_chunk_with_id("mutable", "a longer original"));
    ui.draw();
    for replacement in [Some("é"), None, Some(""), Some("new")] {
        let content = replacement.map_or(agent_client_protocol::schema::MaybeUndefined::Null, |text| {
            agent_client_protocol::schema::MaybeUndefined::Value(vec![acp::ContentBlock::from(text)])
        });
        ui.acp_event(session_update(acp::SessionUpdate::AgentMessage(
            acp::AgentMessage::new("mutable").content(content),
        )));
        ui.assert_viewport_not_contains("original");
        ui.acp_event(text_chunk_with_id("mutable", " tail"));
        ui.assert_viewport_contains(&format!("{} tail", replacement.unwrap_or_default()));
    }
    ui.assert_history_not_contains("Transcript updated;");
}

#[test]
fn session_changes_never_purge_native_scrollback() {
    let changes: [(&str, Step); 7] = [
        ("a context clear", |ui| ui.acp_event(context_cleared())),
        ("/clear", |ui| {
            ui.run_command("/clear");
            assert_command(ui, |c| matches!(c, AgentCommand::NewSession { .. }), "/clear");
            ui.deliver_result(new_session_created("fresh-session", Vec::new()));
        }),
        ("a failed /clear", |ui| {
            ui.run_command("/clear");
            ui.deliver_result(CommandResult::NewSession(Err("refused".to_string())));
        }),
        ("a session switch", |ui| {
            ui.begin_resume("other", "/tmp/elsewhere");
            ui.deliver_result(session_loaded("other", Vec::new()));
        }),
        ("a workspace move", |ui| {
            move_to_other_workspace(ui);
            ui.deliver_result(workspace_moved("/home/user/code/other"));
        }),
        ("a failed workspace listing", |ui| {
            ui.run_command("/move");
            ui.deliver_result(workspace_list_failed("network error"));
        }),
        ("a failed workspace move", |ui| {
            move_to_other_workspace(ui);
            ui.deliver_result(workspace_move_failed("permission denied"));
        }),
    ];
    for (change, apply) in changes {
        let mut ui = TestUiBuilder::new().workspace_move().build();
        commit_overflowing_reply(&mut ui);
        apply(&mut ui);
        assert!(ui.history_text().contains("overflow-line-0"), "{change} purged native scrollback");
    }
}

fn assert_command(ui: &mut TestUi, expected: impl Fn(&AgentCommand) -> bool, label: &str) {
    let command = ui.next_agent_command().unwrap_or_else(|| panic!("{label} should have sent a command"));
    assert!(expected(&command), "{label} sent {command:?}");
}

fn commit_overflowing_reply(ui: &mut TestUi) {
    ui.submit("talk at length");
    assert_command(ui, |c| matches!(c, AgentCommand::Prompt { .. }), "the overflowing reply");
    let reply = format!("{}finished reply", numbered_lines("overflow-line", 30));
    ui.acp_event(text_chunk_with_id("overflow-reply", &reply));
    ui.complete_prompt(acp::StopReason::EndTurn);
    ui.assert_history_contains("overflow-line-0");
}