rho-coding-agent 1.18.0

A lightweight agent harness inspired by Pi
Documentation
use rho_tools::tool_card::{ToolCard, ToolFamily, ToolHeader, ToolStatus};

use super::*;

fn call_id(value: &str) -> ToolCallId {
    ToolCallId::from_string(value).unwrap()
}

fn card(label: &str) -> ToolCard {
    ToolCard::new(
        ToolStatus::Running,
        ToolFamily::Default,
        ToolHeader::call(label, None),
    )
}

fn live_labels(batch: &ToolCallBatch) -> Vec<String> {
    batch
        .live_entries()
        .map(|entry| entry.card.header_text())
        .collect()
}

#[test]
fn promotion_preserves_model_order_instead_of_call_id_order() {
    let mut batch = ToolCallBatch::default();
    let first = call_id("z-model-first");
    let second = call_id("a-model-second");
    batch.preview(0, Some(first.clone()), card("first preview"));
    batch.preview(1, Some(second.clone()), card("second preview"));

    batch.started(second, card("second running"));
    assert_eq!(live_labels(&batch), ["● first preview", "● second running"]);

    batch.started(first, card("first running"));
    assert_eq!(live_labels(&batch), ["● first running", "● second running"]);
}

#[test]
fn proposal_reuses_stream_preview_slot_when_call_id_matches() {
    let mut batch = ToolCallBatch::default();
    let call = call_id("call-agent");
    // Responses output_index can land past non-tool items (reasoning, etc.).
    batch.preview(3, Some(call.clone()), card("reviewer starting"));
    // Proposal is call-id keyed and must not mint a second card.
    batch.preview_call(call.clone(), card("reviewer proposed"));
    batch.started(call, card("reviewer running"));

    assert_eq!(batch.live_entries().count(), 1);
    assert_eq!(live_labels(&batch), ["● reviewer running"]);
    assert!(batch.previews.is_empty());
}

#[test]
fn proposal_without_stream_appends_in_arrival_order() {
    let mut batch = ToolCallBatch::default();
    let first = call_id("call-a");
    let second = call_id("call-b");
    batch.preview_call(first.clone(), card("first starting"));
    batch.preview_call(second.clone(), card("second starting"));
    batch.started(first, card("first running"));
    batch.started(second, card("second running"));

    assert_eq!(live_labels(&batch), ["● first running", "● second running"]);
}

#[test]
fn late_stream_preview_is_ignored_after_start() {
    let mut batch = ToolCallBatch::default();
    let call = call_id("call-agent");
    batch.preview(3, Some(call.clone()), card("starting"));
    batch.started(call.clone(), card("running"));
    batch.preview(3, Some(call), card("stale starting"));
    batch.preview(3, None, card("index only stale"));

    assert_eq!(live_labels(&batch), ["● running"]);
    assert!(batch.previews.is_empty());
}

#[test]
fn latest_is_last_model_order_entry_when_later_entry_is_still_a_preview() {
    let mut batch = ToolCallBatch::default();
    let first = call_id("z-model-first");
    let second = call_id("a-model-second");
    batch.preview(0, Some(first.clone()), card("first"));
    batch.preview(1, Some(second), card("second"));
    batch.started(first.clone(), card("first running"));

    batch.latest_mut().unwrap().expanded = true;

    assert!(!batch.running[&first].expanded);
    assert!(batch.previews[&1].expanded);
}

#[test]
fn latest_is_last_model_order_entry_after_promotion() {
    let mut batch = ToolCallBatch::default();
    let first = call_id("z-model-first");
    let second = call_id("a-model-second");
    batch.preview(0, Some(first.clone()), card("first"));
    batch.preview(1, Some(second.clone()), card("second"));
    batch.started(first, card("first running"));
    batch.started(second.clone(), card("second running"));

    batch.latest_mut().unwrap().expanded = true;

    assert!(!batch.running[&call_id("z-model-first")].expanded);
    assert!(batch.running[&second].expanded);
}

#[test]
fn finished_removes_running_and_returns_expanded() {
    let mut batch = ToolCallBatch::default();
    let call = call_id("call-1");
    batch.started(call.clone(), card("running"));
    batch.latest_mut().unwrap().expanded = true;
    assert!(batch.finished(&call));
    assert!(!batch.is_running());
    assert!(batch.live_entries().next().is_none());
}