kcode-k1-chat-state 0.1.0

Synchronous chat actor state and ordered job tracking
Documentation
use std::sync::atomic::Ordering;

use kcode_k1_chat_core::{ChatError, ChatView, PendingAction};

use crate::ActorState;

#[test]
fn initial_view_and_jobs_follow_monotonic_order() {
    let mut state = ActorState::new("primary".into(), false);
    assert_eq!(
        state.view(),
        ChatView {
            primary: "primary".into(),
            pending: String::new(),
            history: vec![],
            actions: vec![],
        }
    );
    assert_eq!(state.begin_worker("worker".into()), 1);
    assert_eq!(state.begin_tool("tool".into()), 2);
    let (job, frozen) = state.begin_compaction();
    assert_eq!((job, frozen), (3, "primary".into()));
    assert_eq!(
        state.view().actions,
        vec![
            PendingAction::Worker {
                llm: "worker".into()
            },
            PendingAction::Tool {
                name: "tool".into()
            },
            PendingAction::Compaction,
        ]
    );
}

#[test]
fn inference_delta_attempt_and_output_boundary_are_exact() {
    let mut state = ActorState::new("base".into(), false);
    state.append_pending("+ask".into());
    let (job, delta, attempt) = state.begin_inference().unwrap();
    assert_eq!((job, delta), (1, "base+ask".into()));
    attempt.store(4, Ordering::Relaxed);
    assert_eq!(
        state.view().actions,
        vec![PendingAction::Inference { attempt: 4 }]
    );
    assert!(!state.finish_inference(8));
    state.commit_output("+output");
    assert!(state.finish_inference(job));
    state.append_pending("+next".into());
    let (next, delta, _) = state.begin_inference().unwrap();
    assert_eq!((next, delta), (2, "+next".into()));
}

#[test]
fn halt_is_first_write_and_restart_observes_state() {
    let mut state = ActorState::new("p".into(), false);
    assert_eq!(state.restart(), Err(ChatError::NotStalled));
    assert!(state.halt("first".into()));
    assert!(!state.halt("second".into()));
    assert!(state.halted());
    assert_eq!(state.take_halt(), Some("first".into()));
    assert_eq!(state.restart(), Err(ChatError::NotStalled));
    assert!(state.halt("again".into()));
    state.append_pending("q".into());
    let tool = state.begin_tool("t".into());
    assert_eq!(state.restart(), Err(ChatError::Busy));
    state.complete_action(tool, "r".into());
    assert_eq!(state.restart(), Ok(()));
    assert!(!state.halted());
    let (_, delta, _) = state.begin_inference().unwrap();
    assert_eq!(delta, "pqr");
}

#[test]
fn batched_replies_sort_prepend_chunk_and_track_live_jobs() {
    let mut state = ActorState::new(String::new(), false);
    state.append_pending("å°¾".into());
    let first = state.begin_tool("a".into());
    let second = state.begin_tool("b".into());
    let third = state.begin_tool("c".into());
    state.begin_batch();
    state.apply_tool_replies(
        vec![(2, second, "B".into(), false), (1, first, "A".into(), true)],
        false,
    );
    assert_eq!(state.view().pending, "ABå°¾");
    assert_eq!(
        state.view().actions,
        vec![
            PendingAction::Tool { name: "b".into() },
            PendingAction::Tool { name: "c".into() },
        ]
    );
    state.apply_tool_replies(
        vec![(3, third, "C".into(), true), (2, second, "β".into(), true)],
        true,
    );
    assert_eq!(state.view().pending, "ABβC尾");
    assert!(state.view().actions.is_empty());
    state.apply_tool_replies(vec![(0, 99, "ignored".into(), true)], true);
    assert_eq!(state.view().pending, "ABβC尾");
}

#[test]
fn updates_deduplicate_and_stale_actions_do_nothing() {
    let mut state = ActorState::new(String::new(), false);
    let job = state.begin_worker("llm".into());
    state.apply_append_update(job, 7, "a".into());
    state.apply_append_update(job, 7, "duplicate".into());
    assert!(state.accept_activity_update(job, 8));
    assert!(!state.accept_activity_update(job, 8));
    state.complete_action(job + 1, "stale".into());
    state.complete_action(job, "z".into());
    state.apply_append_update(job, 9, "late".into());
    assert!(!state.accept_activity_update(job, 10));
    state.complete_action(job, "late".into());
    assert_eq!(state.view().pending, "az");
}

#[test]
fn rejected_compaction_prepends_exact_text() {
    let mut state = ActorState::new("p".into(), false);
    state.append_pending("å°¾".into());
    state.reject_compaction_batch();
    assert_eq!(state.view().pending, "compaction must be the sole callå°¾");
    let (_, delta, _) = state.begin_inference().unwrap();
    assert_eq!(delta, "pcompaction must be the sole callå°¾");
}

#[test]
fn compaction_success_records_history_pending_and_resets_delta() {
    let mut state = ActorState::new("old".into(), false);
    state.append_pending("+queued".into());
    let (job, frozen) = state.begin_compaction();
    state.complete_compaction(job + 1, frozen.clone(), Ok("ignored".into()));
    assert_eq!(state.view().primary, "old");
    state.complete_compaction(job, frozen, Ok("new".into()));
    let view = state.view();
    assert_eq!(view.history, vec!["old"]);
    assert_eq!(view.primary, "new+queued");
    assert_eq!(view.pending, "");
    let (_, delta, _) = state.begin_inference().unwrap();
    assert_eq!(delta, "new+queued");
}

#[test]
fn compaction_failure_preserves_history_and_sent_boundary() {
    let mut state = ActorState::new("first".into(), true);
    let (first, frozen) = state.begin_compaction();
    state.complete_compaction(first, frozen, Ok("second".into()));
    let (inference, delta, _) = state.begin_inference().unwrap();
    assert_eq!(delta, "second");
    state.commit_output("+answer");
    assert!(state.finish_inference(inference));
    let (job, frozen) = state.begin_compaction();
    state.append_pending("+queued".into());
    state.complete_compaction(job, frozen, Err("+error".into()));
    let view = state.view();
    assert_eq!(view.history, vec!["first"]);
    assert_eq!(view.primary, "second+answer+error+queued");
    let (_, delta, _) = state.begin_inference().unwrap();
    assert_eq!(delta, "+error+queued");
}

#[test]
fn quiet_tracks_jobs_pending_force_and_batches_but_not_halt() {
    let mut state = ActorState::new("p".into(), false);
    assert!(state.quiet());
    state.halt("halt".into());
    assert!(state.quiet());
    state.take_halt();
    state.force_inference();
    assert!(!state.quiet());
    let (job, _, _) = state.begin_inference().unwrap();
    assert!(!state.quiet());
    assert!(state.finish_inference(job));
    assert!(state.quiet());
    state.append_pending("x".into());
    assert!(!state.quiet());
    let (job, _, _) = state.begin_inference().unwrap();
    assert!(state.finish_inference(job));
    assert!(state.quiet());
    state.begin_batch();
    assert!(!state.quiet());
    state.apply_tool_replies(vec![], true);
    assert!(!state.quiet());
    let (job, _, _) = state.begin_inference().unwrap();
    assert!(state.finish_inference(job));
    assert!(state.quiet());
}