collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Unit tests for coordinator internals.

use super::*;

#[test]
fn test_parse_task_json_valid() {
    let json = r#"[
        {"id": "t1", "prompt": "Fix the bug", "role": "worker", "dependencies": [], "target_files": ["src/main.rs"]},
        {"id": "t2", "prompt": "Add tests", "role": "worker", "dependencies": ["t1"], "target_files": ["tests/test.rs"]}
    ]"#;
    let tasks = parse_task_json(json).unwrap();
    assert_eq!(tasks.len(), 2);
    assert_eq!(tasks[0].id, "t1");
    assert_eq!(tasks[1].dependencies, vec!["t1"]);
}

#[test]
fn test_parse_task_json_wrapped() {
    let text = "Here are the subtasks:\n```json\n[\n{\"id\": \"t1\", \"prompt\": \"Do work\", \"role\": \"worker\"}]\n```";
    let tasks = parse_task_json(text).unwrap();
    assert_eq!(tasks.len(), 1);
}

#[test]
fn test_parse_task_json_empty() {
    let tasks = parse_task_json("No JSON here").unwrap();
    assert!(tasks.is_empty());
}

#[test]
fn test_truncate() {
    assert_eq!(truncate("hello", 10), "hello");
    assert_eq!(truncate("hello world", 5), "hello...");
}

#[test]
fn test_is_trivially_sequential() {
    // Short messages → trivially sequential (any language)
    assert!(is_trivially_sequential("fix the bug"));
    assert!(is_trivially_sequential("what does this do?"));
    assert!(is_trivially_sequential("버그 수정해줘"));
    assert!(is_trivially_sequential("バグを直して"));

    // Explicit sequential-ordering keywords → trivially sequential
    assert!(is_trivially_sequential(
        "먼저 auth.rs를 고치고 그다음 config를 업데이트해"
    ));
    assert!(is_trivially_sequential(
        "step 1: read file, step 2: edit it"
    ));
    assert!(is_trivially_sequential("do these one by one"));
    assert!(is_trivially_sequential(
        "まず認証を修正して、次にテストを追加して"
    ));

    // Single file with short message → trivially sequential
    assert!(is_trivially_sequential("fix routes.rs"));
    assert!(is_trivially_sequential(
        "add a method to auth.rs that checks token expiry"
    ));

    // Korean implementation request — NOT trivially sequential (delegate to LLM)
    // Previously misclassified as complex by keyword; now correctly deferred to LLM
    assert!(!is_trivially_sequential(
        "현재 start가 foreground로 뜨게 되어 있는데, foreground는 serve로 변경. \
         start,stop,restart는 background 프로세스로 처리 \
         enable, disable은 로그인 아이템으로 등록하도록"
    ));

    // Long multi-file task → delegate to LLM
    assert!(!is_trivially_sequential(
        "refactor the authentication system: update auth middleware, update \
         all API route handlers, update the user service, and add new tests"
    ));

    // Multi-file reference → delegate to LLM
    assert!(!is_trivially_sequential(
        "update auth.rs and api.rs and config.rs to use the new token format"
    ));
}

fn make_task(id: &str, deps: &[&str]) -> SwarmTask {
    SwarmTask {
        id: id.to_string(),
        prompt: format!("task {id}"),
        role: "worker".to_string(),
        dependencies: deps.iter().map(|s| s.to_string()).collect(),
        target_files: Vec::new(),
        agent_name: None,
    }
}

#[test]
fn test_enforce_file_disjoint() {
    let mut tasks = vec![
        SwarmTask {
            id: "t1".to_string(),
            prompt: "task 1".to_string(),
            role: "worker".to_string(),
            dependencies: vec![],
            target_files: vec!["src/lib.rs".to_string()],
            agent_name: None,
        },
        SwarmTask {
            id: "t2".to_string(),
            prompt: "task 2".to_string(),
            role: "worker".to_string(),
            dependencies: vec![],
            target_files: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()],
            agent_name: None,
        },
    ];

    enforce_file_disjoint(&mut tasks);

    // t2 should now depend on t1 because both touch src/lib.rs
    assert!(tasks[1].dependencies.contains(&"t1".to_string()));
}

#[test]
fn test_enforce_file_disjoint_no_overlap() {
    let mut tasks = vec![make_task("t1", &[]), make_task("t2", &[])];
    // Different target_files → no forced dependency
    tasks[0].target_files = vec!["a.rs".to_string()];
    tasks[1].target_files = vec!["b.rs".to_string()];

    enforce_file_disjoint(&mut tasks);
    assert!(tasks[1].dependencies.is_empty());
}