tinychannels 0.1.0

Pluggable channel and messaging primitives for OpenHuman harness communication.
Documentation
use super::*;

fn strip_indicators(chunks: &[String]) -> Vec<String> {
    chunks
        .iter()
        .map(|chunk| {
            chunk
                .rsplit_once(" (")
                .map(|(body, _)| body.to_string())
                .unwrap_or_else(|| chunk.clone())
        })
        .collect()
}

fn fences_balanced(chunk: &str) -> bool {
    chunk.matches("```").count().is_multiple_of(2)
}

#[test]
fn measures_utf16_units_for_astral_characters() {
    assert_eq!(measure_text("hello", LengthUnit::Utf16Units), 5);
    assert_eq!(measure_text("你好", LengthUnit::Utf16Units), 2);
    assert_eq!(measure_text("😀", LengthUnit::Utf16Units), 2);
    assert_eq!(measure_text("hi😀", LengthUnit::Utf16Units), 4);
    assert_eq!(measure_text("𝄞", LengthUnit::Utf16Units), 2);
}

#[test]
fn prefix_within_utf16_limit_does_not_split_surrogate_pairs() {
    assert_eq!(
        prefix_within_limit("hello world", 5, LengthUnit::Utf16Units),
        "hello"
    );
    assert_eq!(prefix_within_limit("a😀b", 2, LengthUnit::Utf16Units), "a");
    assert_eq!(prefix_within_limit("😀x", 2, LengthUnit::Utf16Units), "😀");
    assert_eq!(
        prefix_within_limit(&"😀".repeat(10), 6, LengthUnit::Utf16Units),
        "😀😀😀"
    );
}

#[test]
fn utf16_chunking_splits_emoji_case_that_character_count_would_pass() {
    let text = "😀".repeat(2049);
    assert_eq!(measure_text(&text, LengthUnit::Characters), 2049);
    assert_eq!(measure_text(&text, LengthUnit::Utf16Units), 4098);

    let char_chunks = chunk_text_with_options(
        &text,
        TextChunkOptions {
            limit: 4096,
            length_unit: LengthUnit::Characters,
            indicators: false,
            ..Default::default()
        },
    );
    assert_eq!(char_chunks.len(), 1);

    let utf16_chunks = chunk_text_with_options(
        &text,
        TextChunkOptions {
            limit: 4096,
            length_unit: LengthUnit::Utf16Units,
            indicators: true,
            ..Default::default()
        },
    );
    assert!(utf16_chunks.len() > 1);
    for chunk in &utf16_chunks {
        assert!(measure_text(chunk, LengthUnit::Utf16Units) <= 4096);
    }
}

#[test]
fn length_chunking_prefers_newline_then_space_boundaries() {
    let chunks = chunk_text_with_options(
        "paragraph one line\n\nparagraph two starts here and continues",
        TextChunkOptions {
            limit: 40,
            indicators: false,
            ..Default::default()
        },
    );
    assert_eq!(
        chunks.join(""),
        "paragraph one line\n\nparagraph two starts here and continues"
    );
    assert_eq!(chunks[0].trim_end(), "paragraph one line");

    let chunks = chunk_text_with_options(
        "This is a message that should break nicely near a word boundary.",
        TextChunkOptions {
            limit: 30,
            indicators: false,
            ..Default::default()
        },
    );
    assert!(!chunks[0].ends_with("nic"));
    assert!(chunks.iter().all(|chunk| chunk.len() <= 30));
    assert_eq!(
        chunks.join(""),
        "This is a message that should break nicely near a word boundary."
    );
}

#[test]
fn length_chunking_preserves_boundary_whitespace() {
    let text = "  test ".repeat(120);
    let chunks = chunk_text_with_options(
        &text,
        TextChunkOptions {
            limit: 37,
            indicators: false,
            ..Default::default()
        },
    );
    assert!(chunks.len() > 1);
    assert!(chunks.iter().all(|chunk| chunk.len() <= 37));
    assert_eq!(chunks.join(""), text);
}

#[test]
fn plain_text_chunking_uses_full_limit_without_fence_reserve() {
    let text = "a".repeat(41);
    let chunks = chunk_text_with_options(
        &text,
        TextChunkOptions {
            limit: 40,
            indicators: false,
            markdown: true,
            ..Default::default()
        },
    );
    assert_eq!(chunks.len(), 2);
    assert_eq!(chunks[0].len(), 40);
    assert_eq!(chunks[1].len(), 1);
}

#[test]
fn chunking_avoids_inline_code_span_when_possible() {
    let chunks = chunk_text_with_options(
        "prefix words `inline code has spaces` suffix",
        TextChunkOptions {
            limit: 28,
            indicators: false,
            ..Default::default()
        },
    );
    assert_eq!(chunks[0], "prefix words");
    assert!(chunks[1].trim_start().starts_with("`inline code"));
}

#[test]
fn markdown_fences_are_closed_and_reopened_across_chunks() {
    let text = format!(
        "Before\n```python\n{}{}\nAfter",
        "x = '😀'\n".repeat(80),
        "```"
    );
    let chunks = chunk_text_with_options(
        &text,
        TextChunkOptions {
            limit: 220,
            length_unit: LengthUnit::Utf16Units,
            markdown: true,
            indicators: true,
            ..Default::default()
        },
    );
    assert!(chunks.len() > 1);
    for chunk in &chunks {
        assert!(fences_balanced(chunk), "unbalanced chunk: {chunk:?}");
        assert!(measure_text(chunk, LengthUnit::Utf16Units) <= 220);
    }
    let bodies = strip_indicators(&chunks);
    assert!(bodies[1].starts_with("```python\n"));
    assert!(bodies[0].trim_end().ends_with("```"));
}

#[test]
fn newline_mode_packs_paragraphs_then_rechunks_long_paragraphs() {
    let text = "one\n\nsecond paragraph\n\nthird paragraph";
    let chunks = chunk_text_with_mode(text, 24, LengthUnit::Characters, ChunkMode::Newline);
    let bodies = strip_indicators(&chunks);
    assert_eq!(bodies, vec!["one\n\nsecond paragraph", "third paragraph"]);

    let long = "alpha\n\n".to_string() + &"x".repeat(50);
    let chunks = chunk_text_with_options(
        &long,
        TextChunkOptions {
            limit: 20,
            mode: ChunkMode::Newline,
            indicators: false,
            ..Default::default()
        },
    );
    assert_eq!(chunks[0], "alpha");
    assert!(chunks[1..].iter().all(|chunk| chunk.len() <= 20));
}

#[test]
fn continuation_indicators_are_added_for_multi_chunk_messages() {
    let chunks = chunk_text("aa bb cc dd ee", 12);
    assert!(chunks.len() > 1);
    let total = chunks.len();
    for (index, chunk) in chunks.iter().enumerate() {
        assert!(chunk.ends_with(&format!("({}/{total})", index + 1)));
    }
}

#[test]
fn resolves_text_chunk_limit_with_overrides() {
    assert_eq!(resolve_text_chunk_limit(Some(1234), None), 1234);
    assert_eq!(resolve_text_chunk_limit(Some(0), Some(2000)), 2000);
    assert_eq!(
        resolve_text_chunk_limit(None, None),
        DEFAULT_TEXT_CHUNK_LIMIT
    );
}

#[test]
fn truncate_with_ellipsis_only_marks_truncated_text() {
    assert_eq!(truncate_with_ellipsis("short", 10), "short");
    assert_eq!(truncate_with_ellipsis("abcdef", 3), "abc…");
}