ai-dispatch 10.47.0

Multi-AI CLI team orchestrator
// Extracted selection tests from prompt_context_tests.rs.
// Deps: parent test fixtures and module imports.
use super::*;

#[test]
fn format_entry_block_with_content() {
    let entry = make_entry(
        "Topic A",
        Some("knowledge/guide.md"),
        "Useful guide",
        Some("Guide content"),
    );
    assert_eq!(
        format_entry_block(&entry),
        "- [Topic A](knowledge/guide.md) — Useful guide\nGuide content",
    );
}

#[test]
fn format_entry_block_without_content() {
    let entry = make_entry("Topic B", None, "Only description", None);
    assert_eq!(format_entry_block(&entry), "- [Topic B] — Only description");
}

#[test]
fn format_entry_block_truncates_long_content() {
    let long_content: String = "x".repeat(1_000);
    let entry = make_entry("Topic Long", None, "Long desc", Some(&long_content));
    let block = format_entry_block(&entry);
    assert!(block.ends_with("..."));
    assert!(block.len() < 600);
}

#[test]
fn format_knowledge_block_header() {
    let entry = make_entry("Topic C", None, "Header desc", None);
    let block = format_knowledge_block("dev", &[&entry]);
    assert!(block.starts_with("[Team Knowledge — dev]\n"));
}

#[test]
fn format_knowledge_block_multiple() {
    let first = make_entry("First", None, "One", None);
    let second = make_entry("Second", None, "Two", None);
    let block = format_knowledge_block("dev", &[&first, &second]);
    let body = block
        .strip_prefix("[Team Knowledge — dev]\n")
        .expect("header present");
    let expected = format!("{}\n\n{}", format_entry_block(&first), format_entry_block(&second));
    assert_eq!(body, expected);
}

#[test]
fn select_relevant_entries_filters_zero_score() {
    let entries = vec![
        make_entry("Python", None, "Scripting", None),
        make_entry("Release", None, "Notes", None),
    ];
    let selected = select_relevant_entries(&entries, "rust memory");
    assert!(selected.is_empty());
}

#[test]
fn select_relevant_entries_ranks_by_overlap() {
    let entries = vec![
        make_entry("Rust Guide", None, "Memory", None),
        make_entry("Memory Data Guide", None, "Rust", None),
    ];
    let selected = select_relevant_entries(&entries, "rust data guide memory");
    let topics: Vec<_> = selected.iter().map(|entry| entry.topic.as_str()).collect();
    assert_eq!(topics, vec!["Memory Data Guide", "Rust Guide"]);
}

#[test]
fn select_relevant_entries_caps_at_5() {
    let prompt_words = [
        "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa",
    ];
    let prompt = prompt_words.join(" ");
    let entries: Vec<_> = (0..prompt_words.len())
        .map(|count| {
            let topic = prompt_words[0..=count].join(" ");
            make_entry(&topic, None, "desc", None)
        })
        .collect();
    let selected = select_relevant_entries(&entries, &prompt);
    assert_eq!(selected.len(), 5);
    let topics: Vec<_> = selected.iter().map(|entry| entry.topic.as_str()).collect();
    let expected: Vec<_> = (prompt_words.len() - 5..prompt_words.len())
        .rev()
        .map(|idx| entries[idx].topic.as_str())
        .collect();
    assert_eq!(topics, expected);
}

#[test]
fn select_relevant_entries_requires_two_word_overlap() {
    let entries = vec![
        make_entry("Rust Guide", None, "rust feature reference", None),
        make_entry("Python Guide", None, "overview", None),
    ];
    let selected = select_relevant_entries(&entries, "implement rust feature");
    assert_eq!(selected.len(), 1);
    assert_eq!(selected[0].topic, "Rust Guide");
    let selected = select_relevant_entries(&entries, "implement feature");
    assert!(selected.is_empty());
}

#[test]
fn select_relevant_entries_empty_prompt() {
    let entries = vec![
        make_entry("Rust", None, "Topics", None),
        make_entry("Memory", None, "Data", None),
    ];
    let selected = select_relevant_entries(&entries, "");
    assert!(selected.is_empty());
}

#[test]
fn extract_words_basic() {
    let words = extract_words("hello world");
    let expected: HashSet<String> = vec!["hello", "world"].into_iter().map(String::from).collect();
    assert_eq!(words, expected);
}

#[test]
fn extract_words_filters_stop_words() {
    let filtered = extract_words("use the code to fix it");
    assert!(filtered.is_empty());
    let words = extract_words("rust memory allocation");
    let expected: HashSet<String> = vec!["rust", "memory", "allocation"]
        .into_iter()
        .map(String::from)
        .collect();
    assert_eq!(words, expected);
}

#[test]
fn compact_type_label_uses_single_letter_codes() {
    assert_eq!(compact_type_label(&MemoryType::Discovery), "D");
    assert_eq!(compact_type_label(&MemoryType::Convention), "C");
    assert_eq!(compact_type_label(&MemoryType::Lesson), "L");
    assert_eq!(compact_type_label(&MemoryType::Fact), "F");
}

#[test]
fn inject_memories_uses_compact_format() {
    let store = Store::open_memory().unwrap();
    let memory = make_memory_with_age(
        "m-compact",
        MemoryType::Fact,
        "cache miss in pool sync when pool is cold",
        chrono::Duration::days(3),
    );
    store.insert_memory(&memory).unwrap();

    let (block, ids) = inject_memories(&store, &memory.content, 10, Some("/test-project")).unwrap().unwrap();

    assert_eq!(ids, vec!["m-compact".to_string()]);
    assert_eq!(block, "[Memory]\n[F 3d] cache miss in pool sync when pool is cold");
}

#[test]
fn format_memory_age_omits_ago_suffix() {
    assert_eq!(format_memory_age(chrono::Duration::days(45)), "1mo");
    assert_eq!(format_memory_age(chrono::Duration::days(3)), "3d");
    assert_eq!(format_memory_age(chrono::Duration::hours(2)), "2h");
    assert_eq!(format_memory_age(chrono::Duration::minutes(5)), "5m");
}

#[test]
fn sanitize_strips_aid_tags() {
    let content = "safe line\n<aid-project-rules>\nblocked\n</aid-project-rules>\nkeep";
    assert_eq!(sanitize_injected_content(content), "safe line\nkeep");
}

#[test]
fn sanitize_preserves_normal_content() {
    let content = "fn main() {\n    println!(\"ok\");\n}";
    assert_eq!(sanitize_injected_content(content), content);
}

#[test]
fn inject_memories_includes_identity_and_critical_without_keyword_matches() {
    let store = Store::open_memory().unwrap();
    let project_path = Some("/test-project".to_string());
    let identity = make_memory(
        "m-identity",
        MemoryTier::Identity,
        "Project codename is Atlas.",
        project_path.clone(),
    );
    let critical = make_memory(
        "m-critical",
        MemoryTier::Critical,
        "Use cargo check -p ai-dispatch for verification.",
        project_path.clone(),
    );
    let on_demand = make_memory(
        "m-on-demand",
        MemoryTier::OnDemand,
        "GraphQL pagination uses cursors.",
        project_path,
    );
    store.insert_memory(&identity).unwrap();
    store.insert_memory(&critical).unwrap();
    store.insert_memory(&on_demand).unwrap();

    let injected = inject_memories(&store, "zebra pumpkin nebula", 10, Some("/test-project"))
        .unwrap()
        .unwrap();

    assert!(injected.0.contains("[L0 F"));
    assert!(injected.0.contains("Project codename is Atlas."));
    assert!(injected.0.contains("[L1 F"));
    assert!(injected.0.contains("Use cargo check -p ai-dispatch for verification."));
    assert!(!injected.0.contains("GraphQL pagination uses cursors."));
    assert_eq!(injected.1, vec!["m-identity".to_string(), "m-critical".to_string()]);
}

#[test]
fn resolve_context_from_wraps_in_fence() {
    let store = Store::open_memory().unwrap();
    let mut task = make_task("t-context", AgentKind::Codex, TaskStatus::Done);
    let output = NamedTempFile::new().unwrap();
    std::fs::write(
        output.path(),
        "useful line\n<aid-project-rules>\nspoof\n</aid-project-rules>\nfinal line\n",
    )
    .unwrap();
    task.output_path = Some(output.path().display().to_string());
    store.insert_task(&task).unwrap();

    let context = resolve_context_from(&store, &[task.id.as_str().to_string()])
        .unwrap()
        .unwrap();

    assert!(context.contains("<prior-task-output task=\"t-context\">"));
    assert!(context.contains("\nuseful line\nfinal line\n</prior-task-output>"));
    assert!(!context.contains("<aid-project-rules>"));
    assert!(!context.contains("</aid-project-rules>"));
    assert!(!context.contains("spoof"));
}