kcode-k1-chat-thread-actions 0.2.0

Synchronous CurrentTime dispatch for K1 chat threads
Documentation
use kcode_k1_chat_chatend::{ActionId, BoxContent, Chatend, DispatchCall, DispatchOutcome};

use super::dispatch;

fn action(sequence: u64) -> ActionId {
    ActionId::new([sequence as u8; 12], sequence)
}

fn calls(chat: &mut Chatend, specs: &[(&str, &str)]) -> Vec<DispatchCall> {
    chat.start_round().unwrap();
    for (name, arguments) in specs {
        chat.collect_provider_call((*name).into(), (*arguments).into())
            .unwrap();
    }
    let actions = (1..=specs.len() as u64).map(action).collect::<Vec<_>>();
    chat.complete_provider_output(&actions).unwrap()
}

fn terminal(outcome: &DispatchOutcome) -> &Result<String, String> {
    let DispatchOutcome::Terminal(result) = outcome else {
        panic!("dispatch must not return pending");
    };
    result
}

#[test]
fn current_time_call_returns_matching_return() {
    let mut chat = Chatend::new();
    let calls = calls(&mut chat, &[("CurrentTime", "{}")]);
    let call = calls[0].clone();
    let outcomes = dispatch(&calls);

    assert!(
        terminal(&outcomes[0])
            .as_ref()
            .is_ok_and(|time| !time.is_empty())
    );
    chat.complete_dispatch(outcomes).unwrap();
    assert!(matches!(
        chat.boxes().last().unwrap().content(),
        BoxContent::KtoolReturn {
            action_id,
            originating_call,
            result: Ok(_),
        } if *action_id == call.action_id && *originating_call == call.call_box_id
    ));
}

#[test]
fn calls_and_returns_preserve_order() {
    let mut chat = Chatend::new();
    let calls = calls(
        &mut chat,
        &[("CurrentTime", "{}"), ("CurrentTime", " { } ")],
    );
    let outcomes = dispatch(&calls);

    assert!(outcomes.iter().all(|outcome| terminal(outcome).is_ok()));
    chat.complete_dispatch(outcomes).unwrap();
    let boxes = chat.boxes();
    assert!(
        matches!(boxes[1].content(), BoxContent::KtoolCall { action_id, .. } if *action_id == calls[0].action_id)
    );
    assert!(
        matches!(boxes[2].content(), BoxContent::KtoolCall { action_id, .. } if *action_id == calls[1].action_id)
    );
    assert!(
        matches!(boxes[3].content(), BoxContent::KtoolReturn { action_id, .. } if *action_id == calls[0].action_id)
    );
    assert!(
        matches!(boxes[4].content(), BoxContent::KtoolReturn { action_id, .. } if *action_id == calls[1].action_id)
    );
}

#[test]
fn invalid_and_unknown_calls_remain_and_return_one_error_each() {
    let cases = ["", "null", "[]", "{\"zone\":\"UTC\"}"];
    for arguments in cases {
        let mut chat = Chatend::new();
        let calls = calls(&mut chat, &[("CurrentTime", arguments)]);
        let call_box = chat.boxes()[1].clone();
        let outcomes = dispatch(&calls);
        assert_eq!(
            terminal(&outcomes[0]),
            &Err("invalid CurrentTime arguments".into())
        );
        chat.complete_dispatch(outcomes).unwrap();
        assert_eq!(chat.boxes()[1], call_box);
        assert!(
            matches!(chat.boxes()[2].content(), BoxContent::KtoolReturn { result: Err(error), .. } if error == "invalid CurrentTime arguments")
        );
    }

    let mut chat = Chatend::new();
    let calls = calls(&mut chat, &[("NoSuchTool", "{}")]);
    let outcomes = dispatch(&calls);
    assert_eq!(terminal(&outcomes[0]), &Err("unknown Ktool".into()));
    chat.complete_dispatch(outcomes).unwrap();
    assert!(
        matches!(chat.boxes()[2].content(), BoxContent::KtoolReturn { result: Err(error), .. } if error == "unknown Ktool")
    );
}

#[test]
fn queued_user_drains_after_synchronous_returns_and_calls_stay_immutable() {
    let mut chat = Chatend::new();
    let calls = calls(&mut chat, &[("CurrentTime", "{}"), ("NoSuchTool", "{}")]);
    let call_boxes = chat.boxes()[1..3].to_vec();
    assert!(
        chat.accept_user("arrived during dispatch".into())
            .unwrap()
            .is_none()
    );

    chat.complete_dispatch(dispatch(&calls)).unwrap();
    assert_eq!(&chat.boxes()[1..3], call_boxes.as_slice());
    assert!(matches!(
        chat.boxes()[3].content(),
        BoxContent::KtoolReturn { .. }
    ));
    assert!(matches!(
        chat.boxes()[4].content(),
        BoxContent::KtoolReturn { .. }
    ));
    assert!(
        matches!(chat.boxes()[5].content(), BoxContent::User(text) if text == "arrived during dispatch")
    );
}