a3s-effect 0.2.0

Effect-style actor runtime for projecting an A3S Code harness from an immutable event log
Documentation
#![cfg(test)]

use super::*;
use serde_json::json;

fn fact(seq: u64, kind: &str, payload: Value) -> Fact {
    Fact {
        seq,
        kind: kind.into(),
        key: format!("history:{seq}"),
        cause: None,
        payload,
    }
}
fn fold(facts: &[Fact]) -> SchedulerState {
    let config = HarnessConfig::new(16, 1_000_000, 8, 1, vec![], vec![]).unwrap();
    facts.iter().fold(SchedulerState::new(), |state, fact| {
        step(state, fact, &config)
    })
}

#[test]
fn ordinary_assistant_reply_survives_the_next_user_turn() {
    let facts = [
        fact(0, "user.message", json!({"text":"first request"})),
        fact(
            1,
            "model.turn",
            json!({"kind":"text","text":"first response"}),
        ),
        fact(2, "user.message", json!({"text":"next request"})),
    ];
    let state = fold(&facts);
    assert_eq!(
        state.messages,
        [
            "user\nfirst request",
            "assistant\nfirst response",
            "user\nnext request"
        ]
    );
    assert_eq!(state.assistant, None, "the current answer must be fresh");
    assert_eq!(state.phase, CodingPhase::Infer);
    assert_eq!(
        fold(&facts).messages,
        state.messages,
        "immutable replay agrees"
    );
}

#[test]
fn model_question_survives_the_original_answer_fact() {
    let facts = [
        fact(0, "user.message", json!({"text":"choose a target"})),
        fact(
            1,
            "model.turn",
            json!({"kind":"question","question_id":"q1","question":"Which target?","allow_free_text":true,"options":[]}),
        ),
        fact(2, "question.answered", json!({"text":"staging"})),
    ];
    assert_eq!(
        fold(&facts).messages,
        [
            "user\nchoose a target",
            "assistant\nWhich target?",
            "user\nstaging"
        ]
    );
}

#[test]
fn compaction_prefix_contains_the_actual_assistant_fact() {
    let facts = [
        fact(0, "user.message", json!({"text":"inspect the source"})),
        fact(
            1,
            "model.turn",
            json!({"kind":"text","text":"observed answer 7319"}),
        ),
        fact(2, "user.message", json!({"text":"continue"})),
    ];
    let state = fold(&facts);
    let (prefix, kept) = split_compact_tail(&state.messages, 1);
    assert!(prefix
        .iter()
        .any(|line| line == "assistant\nobserved answer 7319"));
    assert_eq!(kept, ["user\ncontinue"]);
}

#[test]
fn compact_tail_keeps_a_closed_tool_batch_together() {
    let messages = vec![
        "user\nolder".to_string(),
        "assistant\ntool read\n{\"file_path\":\"a.rs\"}".to_string(),
        "assistant\ntool read\n{\"file_path\":\"b.rs\"}".to_string(),
        "tool\na".to_string(),
        "tool-error\nb".to_string(),
        "user\nnext".to_string(),
    ];
    let batch: usize = messages[1..5]
        .iter()
        .map(|line| estimate_tokens(line))
        .sum();
    let next = estimate_tokens(&messages[5]);
    let (prefix, kept) = split_compact_tail(&messages, batch + next);
    assert_eq!(prefix, vec!["user\nolder".to_string()]);
    assert_eq!(kept, messages[1..].to_vec());
}

#[test]
fn compact_tail_drops_a_closed_batch_that_exceeds_the_budget() {
    let messages = vec![
        "assistant\ntool read\n{\"file_path\":\"a.rs\"}".to_string(),
        "assistant\ntool read\n{\"file_path\":\"b.rs\"}".to_string(),
        "tool\na".to_string(),
        "tool\nb".to_string(),
        "user\nnext".to_string(),
    ];
    let next = estimate_tokens(&messages[4]);
    let (prefix, kept) = split_compact_tail(&messages, next);
    assert_eq!(kept, vec!["user\nnext".to_string()]);
    assert_eq!(prefix, messages[..4].to_vec());
}

#[test]
fn compact_tail_keeps_one_adjacent_pair_and_drops_the_earlier_pair() {
    let earlier = [
        "assistant\ntool read\n{\"file_path\":\"a.rs\"}".to_string(),
        "tool\na".to_string(),
    ];
    let later = [
        "assistant\ntool read\n{\"file_path\":\"b.rs\"}".to_string(),
        "tool\nb".to_string(),
    ];
    let messages = [earlier.clone(), later.clone()].concat();
    let later_tokens: usize = later.iter().map(|line| estimate_tokens(line)).sum();
    let (prefix, kept) = split_compact_tail(&messages, later_tokens);
    assert_eq!(prefix, earlier);
    assert_eq!(kept, later);
}

#[test]
fn compact_tail_keeps_a_short_result_run_together() {
    let region = [
        "assistant\ntool read\n{\"file_path\":\"a.rs\"}".to_string(),
        "assistant\ntool read\n{\"file_path\":\"b.rs\"}".to_string(),
        "tool\nonly".to_string(),
    ];
    let mut messages = vec!["user\nolder".to_string()];
    messages.extend(region.iter().cloned());
    messages.push("user\nnext".to_string());
    let region_tokens: usize = region.iter().map(|line| estimate_tokens(line)).sum();
    let next = estimate_tokens(messages.last().unwrap());
    let (prefix, kept) = split_compact_tail(&messages, region_tokens + next);
    assert_eq!(prefix, vec!["user\nolder".to_string()]);
    assert_eq!(kept, messages[1..].to_vec());

    let (small_prefix, small_kept) = split_compact_tail(&messages, next);
    assert_eq!(small_kept, vec!["user\nnext".to_string()]);
    assert_eq!(small_prefix, messages[..messages.len() - 1].to_vec());
    assert!(!small_kept
        .iter()
        .any(|line| line.starts_with("assistant\ntool ")));
}

#[test]
fn compact_tail_does_not_keep_a_record_with_another_calls_result() {
    let messages = vec![
        "assistant\ntool read\n{\"file_path\":\"a.rs\"}".to_string(),
        "assistant\ntool read\n{\"file_path\":\"b.rs\"}".to_string(),
        "tool\na".to_string(),
        "tool-error\nb".to_string(),
    ];
    let (prefix, kept) = split_compact_tail(&messages, 0);
    assert_eq!(kept, vec!["tool-error\nb".to_string()]);
    assert!(prefix
        .iter()
        .any(|line| line.starts_with("assistant\ntool read")));
}