yog 0.0.40

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! Enumeration order, skipping, the in-progress query, and the live tail.

use super::{AGENT, write_msg, write_response};
use crate::git_tree::Stream;
use crate::transcript::{EntryKind, Transcript, build};
use tempfile::tempdir;

#[test]
fn entries_are_ordered_by_filename_counter() {
    let dir = tempdir().unwrap();
    for n in ["003-c.md", "001-a.md", "002-b.md"] {
        write_msg(dir.path(), n, b"x");
    }
    let t = build(dir.path(), AGENT);
    let names: Vec<&str> = t.entries.iter().map(|e| e.name.as_str()).collect();
    assert_eq!(names, ["001-a.md", "002-b.md", "003-c.md"]);
}

#[test]
fn non_file_entries_are_skipped() {
    let dir = tempdir().unwrap();
    write_msg(dir.path(), "001-a.md", b"x");
    std::fs::create_dir_all(
        dir.path()
            .join("agents")
            .join(AGENT)
            .join("messages")
            .join("subdir"),
    )
    .unwrap();
    assert_eq!(build(dir.path(), AGENT).entries.len(), 1);
}

#[test]
fn absent_messages_dir_yields_empty_transcript() {
    let dir = tempdir().unwrap();
    assert!(build(dir.path(), AGENT).entries.is_empty());
}

#[test]
fn tool_in_progress_true_until_result_committed() {
    let dir = tempdir().unwrap();
    write_msg(
        dir.path(),
        "001-m.json",
        br#"[{"type":"tool_use","id":"t1","name":"N","input":{}}]"#,
    );
    assert!(build(dir.path(), AGENT).tool_in_progress("t1"));
    write_msg(
        dir.path(),
        "002-tool.json",
        br#"{"tool_use_id":"t1","content":"ok"}"#,
    );
    assert!(!build(dir.path(), AGENT).tool_in_progress("t1"));
}

/// Is `id`'s call still in flight — a committed `tool_use` with no committed
/// `tool_result`? The query, not a stored flag (PRINCIPLES: single source of
/// truth), and the fact a seat renders its pulse from.
fn pulsing(t: &Transcript, id: &str) -> bool {
    t.tool_in_progress(id)
}

/// The operator's stuck `⚙ bash — running` (bl-47ec), byte-for-byte off disk:
/// litany commits a **bare array** of canonical blocks, and the id is
/// OpenAI-shaped (`call_…`). The result must classify as a `ToolResult` — a
/// Raw-bucket entry names no id, so it retires nothing — and the call's row
/// must then carry neither the word nor the pulse.
#[test]
fn array_shaped_result_with_opaque_id_retires_the_pulse() {
    let dir = tempdir().unwrap();
    write_msg(
        dir.path(),
        "020-gpt-5.4.json",
        br#"[{"type":"tool_use","id":"call_QxWh5oDZm5GNM4nbnIFVb7Ou","name":"bash","input":{"command":"pwd"}}]"#,
    );
    assert!(
        pulsing(&build(dir.path(), AGENT), "call_QxWh5oDZm5GNM4nbnIFVb7Ou"),
        "no result yet"
    );

    write_msg(
        dir.path(),
        "021-tool.json",
        br#"[{"type":"tool_result","tool_use_id":"call_QxWh5oDZm5GNM4nbnIFVb7Ou","content":[{"type":"text","text":"/ops\n"}],"is_error":false}]"#,
    );
    let t = build(dir.path(), AGENT);
    // The listing opens at `020`, so it also carries the compaction mark for
    // the counter values below it (bl-7bd2) — the result is the last entry.
    assert!(matches!(
        &t.entries.last().unwrap().kind,
        EntryKind::ToolResult { tool_use_id, content, is_error: false }
            if tool_use_id == "call_QxWh5oDZm5GNM4nbnIFVb7Ou" && content == "/ops\n"
    ));
    assert!(
        !pulsing(&t, "call_QxWh5oDZm5GNM4nbnIFVb7Ou"),
        "a committed result retires the in-flight query"
    );
    // And the call itself is still on the record, named, for the seat that
    // renders it: the result did not consume the entry it answered.
    assert!(
        t.entries.iter().any(|e| matches!(
            &e.kind,
            EntryKind::Model { blocks, .. }
                if blocks.iter().any(|b| matches!(b, crate::transcript::Block::ToolUse { name, .. } if name == "bash"))
        )),
        "got: {:?}",
        t.entries.iter().map(|e| e.name.clone()).collect::<Vec<_>>()
    );
}

#[test]
fn build_reads_only_the_committed_messages() {
    let dir = tempdir().unwrap();
    write_msg(dir.path(), "001-a.md", b"hi");
    write_response(
        dir.path(),
        1,
        b"{\"type\":\"content_delta\",\"index\":0,\"delta\":{\"text_delta\":\"streaming\"}}\n",
    );
    // The open response file is on disk and `build` does not touch it: the
    // live tail is the caller's fold, on the caller's clock (§7.2).
    assert_eq!(build(dir.path(), AGENT).entries.len(), 1);
}

#[test]
fn with_live_appends_the_stream_as_a_trailing_entry() {
    let dir = tempdir().unwrap();
    write_msg(dir.path(), "001-a.md", b"hi");
    let t = build(dir.path(), AGENT).with_live(&Stream {
        text: Some("streaming".into()),
        thinking: Some("pondering".into()),
        last_delta: Some(crate::git_tree::Delta::Text),
    });
    assert_eq!(t.entries.len(), 2);
    assert!(matches!(
        &t.entries[1].kind,
        EntryKind::Streaming { thinking, text } if text == "streaming" && thinking == "pondering"
    ));
    // The Raw toggle shows what was said, reasoning first — the same order the
    // rows paint in and the same order the committed blocks will land in.
    assert_eq!(t.entries[1].raw, b"ponderingstreaming");
}

#[test]
fn a_stream_that_has_said_nothing_appends_nothing() {
    let dir = tempdir().unwrap();
    write_msg(dir.path(), "001-a.md", b"hi");
    // The model call is open but no delta has landed: the §11 live mark says
    // "waiting", and a blank live row would be a second, worse spelling of it.
    let t = build(dir.path(), AGENT).with_live(&Stream::default());
    assert_eq!(t.entries.len(), 1);
}

/// **The newest fold wins, and it wins by replacement** (bl-73e7). The tail
/// reaches a seat by two routes at two cadences — the pull answer folds one on
/// at ask cadence, the follow lane delivers a newer one as the model writes —
/// so `with_live` is idempotent by replacement and the answer can never be
/// painted twice. A fold that has gone quiet takes the row away with it, which
/// is what makes the step boundary a swap rather than a duplication.
#[test]
fn a_second_live_fold_replaces_the_first_rather_than_standing_beside_it() {
    let dir = tempdir().unwrap();
    write_msg(dir.path(), "001-a.md", b"hi");
    let committed = build(dir.path(), AGENT);
    let said = |text: &str| Stream {
        text: Some(text.into()),
        thinking: None,
        last_delta: Some(crate::git_tree::Delta::Text),
    };

    let once = committed.with_live(&said("the first half."));
    let twice = committed
        .with_live(&said("the first "))
        .with_live(&said("the first half."));
    assert_eq!(once, twice, "a.with_live(x).with_live(y) == a.with_live(y)");
    assert_eq!(twice.entries.len(), 2, "the committed half plus ONE tail");

    // And an empty fold over a live one leaves the committed half alone.
    assert_eq!(
        committed
            .with_live(&said("said"))
            .with_live(&Stream::default()),
        committed
    );
}