mentra 0.18.2

An agent runtime for tool-using LLM applications
Documentation
//! Public-API tests for the transcript entry tree.
//!
//! Branching is what lets a conversation return to an earlier point and
//! continue differently — "undo that exchange and try another instruction",
//! editing a message and re-running, or exploring two approaches from a shared
//! prefix — without starting a new session and replaying a prefix by hand.

use mentra::{
    AgentTranscript, ContentBlock, Message,
    transcript::{EntryId, TranscriptItem},
};

fn user(text: &str) -> TranscriptItem {
    TranscriptItem::user_turn(Message::user(ContentBlock::text(text)))
}

fn assistant(text: &str) -> TranscriptItem {
    TranscriptItem::assistant_turn(Message::assistant(ContentBlock::text(text)))
}

fn transcript_of(texts: &[&str]) -> AgentTranscript {
    let mut transcript = AgentTranscript::default();
    for (index, text) in texts.iter().enumerate() {
        if index % 2 == 0 {
            transcript.push(user(text));
        } else {
            transcript.push(assistant(text));
        }
    }
    transcript
}

fn texts(transcript: &AgentTranscript) -> Vec<String> {
    transcript
        .items()
        .iter()
        .map(TranscriptItem::text)
        .collect()
}

#[test]
fn appending_hangs_each_entry_from_the_leaf() {
    let transcript = transcript_of(&["one", "two", "three"]);
    let items = transcript.items();

    assert_eq!(items[0].parent_id, None, "the first entry is a root");
    assert_eq!(items[1].parent_id.as_ref(), Some(&items[0].id));
    assert_eq!(items[2].parent_id.as_ref(), Some(&items[1].id));
    assert_eq!(transcript.leaf(), Some(&items[2].id));
}

#[test]
fn branching_rewinds_the_active_path_without_deleting_anything() {
    let mut transcript = transcript_of(&["ask", "answer", "follow up"]);
    let rewind_to = transcript.items()[0].id.clone();

    let moved = transcript
        .branch_from(&rewind_to)
        .expect("entry is on path");

    assert_eq!(moved, 2, "two entries leave the active path");
    assert_eq!(texts(&transcript), vec!["ask"]);
    assert_eq!(transcript.leaf(), Some(&rewind_to));
    assert_eq!(
        transcript.archived().len(),
        2,
        "abandoned entries stay in the transcript"
    );
}

#[test]
fn a_new_turn_after_branching_becomes_a_sibling() {
    let mut transcript = transcript_of(&["ask", "first answer"]);
    let fork_point = transcript.items()[0].id.clone();

    transcript.branch_from(&fork_point).expect("on path");
    transcript.push(assistant("second answer"));

    let children = transcript.children(&fork_point);
    assert_eq!(children.len(), 2, "the fork point now has two paths");

    let child_texts: Vec<String> = children.iter().map(|item| item.text()).collect();
    assert!(child_texts.contains(&"first answer".to_string()));
    assert!(child_texts.contains(&"second answer".to_string()));

    // Only the new path is live.
    assert_eq!(texts(&transcript), vec!["ask", "second answer"]);
}

#[test]
fn an_abandoned_branch_can_be_returned_to() {
    let mut transcript = transcript_of(&["ask", "first answer"]);
    let fork_point = transcript.items()[0].id.clone();
    let first_answer = transcript.items()[1].id.clone();

    transcript.branch_from(&fork_point).expect("on path");
    transcript.push(assistant("second answer"));

    // The abandoned entry is still addressable, which is what makes this a
    // branch rather than a truncation.
    let recovered = transcript.entry(&first_answer).expect("still present");
    assert_eq!(recovered.text(), "first answer");
}

#[test]
fn branching_to_an_unknown_entry_is_refused() {
    let mut transcript = transcript_of(&["ask"]);

    let error = transcript
        .branch_from(&EntryId::new())
        .expect_err("an entry that was never appended is not a branch point");

    assert!(error.to_string().contains("no entry"));
}

#[test]
fn branching_to_the_leaf_changes_nothing() {
    let mut transcript = transcript_of(&["ask", "answer"]);
    let leaf = transcript.leaf().cloned().expect("a leaf");

    let moved = transcript.branch_from(&leaf).expect("the leaf is on path");

    assert_eq!(moved, 0);
    assert_eq!(texts(&transcript), vec!["ask", "answer"]);
    assert!(transcript.archived().is_empty());
}

#[test]
fn the_tree_survives_a_round_trip() {
    let mut transcript = transcript_of(&["ask", "first answer"]);
    let fork_point = transcript.items()[0].id.clone();
    transcript.branch_from(&fork_point).expect("on path");
    transcript.push(assistant("second answer"));

    let encoded = serde_json::to_string(&transcript).expect("serializes");
    let decoded: AgentTranscript = serde_json::from_str(&encoded).expect("deserializes");

    assert_eq!(decoded, transcript);
    assert_eq!(
        decoded.children(&fork_point).len(),
        2,
        "both paths survive persistence"
    );
}

#[test]
fn a_transcript_written_before_entries_had_ids_still_loads_linked() {
    // Build the shape mentra persisted previously by stripping the tree
    // fields back out of a current transcript, so the fixture cannot drift
    // from the real serialization format.
    let modern = transcript_of(&["ask", "answer"]);
    let mut encoded: serde_json::Value = serde_json::to_value(&modern).expect("serializes");
    for item in encoded["items"]
        .as_array_mut()
        .expect("items is an array")
        .iter_mut()
    {
        let object = item.as_object_mut().expect("each item is an object");
        object.remove("id");
        object.remove("parent_id");
    }

    let transcript: AgentTranscript =
        serde_json::from_value(encoded).expect("a pre-tree transcript still deserializes");
    let items = transcript.items();

    assert_eq!(items.len(), 2);
    assert_eq!(texts(&transcript), vec!["ask", "answer"]);
    assert_eq!(items[0].parent_id, None, "the first entry is a root");
    assert_eq!(
        items[1].parent_id.as_ref(),
        Some(&items[0].id),
        "migration links the chain, so a legacy transcript can be branched"
    );
}