kcode-k1-chat-chatend 0.8.0

Append-only open-format ChatBox state transitions for K1 chat
Documentation
use super::*;

fn call(value: u8) -> ProviderCall {
    ProviderCall {
        tool_call_id: ToolCallId::new([value; 12], u64::from(value)),
        name: "WebSearch".into(),
        arguments: "{}".into(),
    }
}

#[test]
fn empty_response_is_appended_at_call_wave_and_done() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    let dispatched = chat
        .append_stage(String::new(), vec![ProviderGenerated::ToolCall(call(1))])
        .unwrap();
    assert_eq!(chat.boxes()[0].box_type(), AGENT_RESPONSE_TYPE);
    assert_eq!(chat.boxes()[0].contents(), "");
    assert_eq!(dispatched[0].call_box_id, BoxId::new(2));
    assert!(chat.flush_active_arrivals().unwrap().is_empty());
    let appended = chat.done(String::new()).unwrap();
    assert_eq!(appended[0].id(), BoxId::new(3));
    assert_eq!(appended[0].box_type(), AGENT_RESPONSE_TYPE);
    assert_eq!(appended[0].contents(), "");
}

#[test]
fn promised_response_ids_precede_active_arrivals_at_stage_and_done() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    assert_eq!(chat.accept_user("during stage".into()).unwrap(), None);
    chat.append_stage("first".into(), vec![]).unwrap();
    assert_eq!(chat.boxes()[0].id(), BoxId::new(1));
    let flushed = chat.flush_active_arrivals().unwrap();
    assert_eq!(flushed[0].id(), BoxId::new(2));
    assert_eq!(chat.accept_user("during done".into()).unwrap(), None);
    let done = chat.done("last".into()).unwrap();
    assert_eq!(done[0].id(), BoxId::new(3));
    assert_eq!(done[0].box_type(), AGENT_RESPONSE_TYPE);
    assert_eq!(done[1].id(), BoxId::new(4));
    assert_eq!(done[1].contents(), "during done");
}

#[test]
fn mixed_provider_values_keep_order_and_dispatch_only_calls() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    let dispatched = chat
        .append_stage(
            "response".into(),
            vec![
                ProviderGenerated::AgentMessage {
                    contents: "notice one".into(),
                },
                ProviderGenerated::ToolCall(call(1)),
                ProviderGenerated::AgentMessage {
                    contents: "notice two".into(),
                },
                ProviderGenerated::ToolCall(call(2)),
            ],
        )
        .unwrap();
    assert_eq!(
        chat.boxes()
            .iter()
            .map(ChatBox::box_type)
            .collect::<Vec<_>>(),
        vec![
            AGENT_RESPONSE_TYPE,
            AGENT_MESSAGE_TYPE,
            TOOL_CALL_TYPE,
            AGENT_MESSAGE_TYPE,
            TOOL_CALL_TYPE,
        ]
    );
    assert_eq!(dispatched.len(), 2);
    assert_eq!(dispatched[0].call_box_id, BoxId::new(3));
    assert_eq!(dispatched[1].call_box_id, BoxId::new(5));
}

#[test]
fn invalid_flush_before_stage_is_transactional() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    chat.accept_user("queued".into()).unwrap();
    assert!(matches!(
        chat.flush_active_arrivals(),
        Err(TransitionError::InvalidPhase)
    ));
    assert!(chat.boxes().is_empty());
    let appended = chat.done("response".into()).unwrap();
    assert_eq!(appended.len(), 2);
    assert_eq!(appended[0].box_type(), AGENT_RESPONSE_TYPE);
    assert_eq!(appended[1].contents(), "queued");
}

#[test]
fn marker_only_flush_reenters_generating() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    chat.append_stage("one".into(), vec![]).unwrap();
    assert!(matches!(
        chat.append_stage("invalid".into(), vec![]),
        Err(TransitionError::InvalidPhase)
    ));
    assert!(chat.flush_active_arrivals().unwrap().is_empty());
    chat.append_stage("two".into(), vec![]).unwrap();
    assert_eq!(chat.boxes()[1].contents(), "two");
}

#[test]
fn abort_preserves_arrivals_without_fabricating_a_response() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    chat.accept_user("first".into()).unwrap();
    assert_eq!(chat.abort().unwrap()[0].contents(), "first");
    assert!(!chat.round_active());

    chat.start_round().unwrap();
    chat.append_stage("only response".into(), vec![]).unwrap();
    chat.accept_user("second".into()).unwrap();
    assert_eq!(chat.abort().unwrap()[0].contents(), "second");
    assert_eq!(
        chat.boxes()
            .iter()
            .filter(|value| value.box_type() == AGENT_RESPONSE_TYPE)
            .count(),
        1
    );
}

#[test]
fn duplicate_generated_calls_fail_without_mutation() {
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    assert!(matches!(
        chat.append_stage(
            "discarded".into(),
            vec![
                ProviderGenerated::ToolCall(call(3)),
                ProviderGenerated::ToolCall(call(3)),
            ],
        ),
        Err(TransitionError::DuplicateToolCall)
    ));
    assert!(chat.boxes().is_empty());
    assert_eq!(chat.done(String::new()).unwrap()[0].id(), BoxId::new(1));
}

#[test]
fn correlated_message_and_v2_result_recover() {
    let id = ToolCallId::new([9; 12], 9);
    let mut chat = Chatend::new();
    chat.start_round().unwrap();
    chat.append_stage(String::new(), vec![ProviderGenerated::ToolCall(call(9))])
        .unwrap();
    chat.accept_tool_message(id, "working".into()).unwrap();
    chat.accept_async_return_v2(
        id,
        Ok("done".into()),
        "k1.web-search-result/v1".into(),
        "{}".into(),
    )
    .unwrap();
    assert_eq!(chat.flush_active_arrivals().unwrap().len(), 2);
    assert_eq!(
        chat.accept_tool_message(id, "late".into()),
        Err(TransitionError::ToolMessageAfterResult)
    );
    chat.abort().unwrap();
    let recovered = Chatend::recover(chat.boxes().to_vec()).unwrap();
    assert!(!recovered.round_active());
}

#[test]
fn recovery_preserves_historical_agent_message_and_unknown_box() {
    let original = vec![
        ChatBox::new(
            BoxId::new(1),
            AGENT_MESSAGE_TYPE.into(),
            "historical".into(),
            String::new(),
            String::new(),
        ),
        ChatBox::new(
            BoxId::new(2),
            "Future Provider Value".into(),
            "opaque".into(),
            "future/v1".into(),
            "bytes".into(),
        ),
    ];
    let recovered = Chatend::recover(original.clone()).unwrap();
    assert_eq!(recovered.boxes(), original);
    assert!(!recovered.round_active());
}