codex-mobile-bridge 0.3.2

Remote bridge and service manager for codex-mobile.
Documentation
use super::*;

#[test]
fn timeline_entry_from_thread_item_classifies_command_execution_semantics_from_actions() {
    let cases = vec![
        (
            "read",
            "cat bridge/src/state/timeline.rs",
            json!([{ "type": "read", "path": "bridge/src/state/timeline.rs" }]),
            "explored",
            Some("read"),
            1_i64,
            Some("bridge/src/state/timeline.rs"),
            None,
        ),
        (
            "list",
            "ls bridge/src/state",
            json!([{ "type": "listFiles", "path": "bridge/src/state" }]),
            "explored",
            Some("list"),
            1_i64,
            Some("bridge/src/state"),
            None,
        ),
        (
            "search",
            "rg semantic bridge/src/state",
            json!([{ "type": "search", "path": "bridge/src/state", "query": "semantic" }]),
            "explored",
            Some("search"),
            1_i64,
            Some("bridge/src/state"),
            Some("semantic"),
        ),
        (
            "mixed",
            "bash -lc 'cat bridge/src/state/timeline.rs && rg semantic bridge/src/state'",
            json!([
                { "type": "read", "path": "bridge/src/state/timeline.rs" },
                { "type": "search", "path": "bridge/src/state", "query": "semantic" }
            ]),
            "explored",
            Some("mixed"),
            2_i64,
            Some("bridge/src/state/timeline.rs"),
            Some("semantic"),
        ),
        (
            "unknown",
            "printf 'done'",
            json!([{ "type": "write", "path": "bridge/src/state/timeline.rs" }]),
            "ran",
            Some("run"),
            0_i64,
            None,
            None,
        ),
    ];

    for (
        index,
        (
            _label,
            command,
            actions,
            expected_kind,
            expected_detail,
            expected_target_count,
            expected_primary_target,
            expected_query,
        ),
    ) in cases.into_iter().enumerate()
    {
        let entry = timeline_entry_from_thread_item(
            PRIMARY_RUNTIME_ID,
            "thread-1",
            Some("turn-1"),
            &json!({
                "id": format!("item-command-{index}"),
                "type": "commandExecution",
                "command": command,
                "aggregatedOutput": "ok",
                "commandActions": actions,
            }),
            "thread_item",
            false,
            true,
        )
        .expect("应创建 commandExecution 时间线条目");

        assert_eq!(
            Some(expected_kind),
            entry
                .metadata
                .get("semantic")
                .and_then(|value| value.get("kind"))
                .and_then(Value::as_str),
        );
        assert_eq!(
            expected_detail,
            entry
                .metadata
                .get("semantic")
                .and_then(|value| value.get("detail"))
                .and_then(Value::as_str),
        );
        assert_eq!(
            Some("high"),
            entry
                .metadata
                .get("semantic")
                .and_then(|value| value.get("confidence"))
                .and_then(Value::as_str),
        );
        assert_eq!(
            Some(command),
            entry
                .metadata
                .get("summary")
                .and_then(|value| value.get("command"))
                .and_then(Value::as_str),
        );
        assert_eq!(
            Some(expected_target_count),
            entry
                .metadata
                .get("summary")
                .and_then(|value| value.get("targets"))
                .and_then(Value::as_array)
                .map(|items| items.len() as i64),
        );
        assert_eq!(
            expected_primary_target,
            entry
                .metadata
                .get("summary")
                .and_then(|value| value.get("targets"))
                .and_then(Value::as_array)
                .and_then(|items| items.first())
                .and_then(Value::as_str),
        );
        assert_eq!(
            expected_query,
            entry
                .metadata
                .get("summary")
                .and_then(|value| value.get("query"))
                .and_then(Value::as_str),
        );
    }
}

#[test]
fn timeline_entry_from_thread_item_summarizes_edited_and_waited_semantics() {
    let file_change_entry = timeline_entry_from_thread_item(
        PRIMARY_RUNTIME_ID,
        "thread-1",
        Some("turn-2"),
        &json!({
            "id": "item-file-change",
            "type": "fileChange",
            "changes": [
                {
                    "path": "bridge/src/state/timeline.rs",
                    "kind": "modified",
                    "diff": "diff --git a/bridge/src/state/timeline.rs b/bridge/src/state/timeline.rs\n--- a/bridge/src/state/timeline.rs\n+++ b/bridge/src/state/timeline.rs\n@@ -1 +1,2 @@\n-old\n+new\n+extra\n"
                },
                {
                    "path": "docs/timeline.md",
                    "kind": "deleted",
                    "content": "line-1\nline-2"
                }
            ]
        }),
        "thread_item",
        false,
        true,
    )
    .expect("应创建 fileChange 时间线条目");

    assert_eq!(
        Some("edited"),
        file_change_entry
            .metadata
            .get("semantic")
            .and_then(|value| value.get("kind"))
            .and_then(Value::as_str),
    );
    assert_eq!(
        Some("bridge/src/state/timeline.rs"),
        file_change_entry
            .metadata
            .get("summary")
            .and_then(|value| value.get("primaryPath"))
            .and_then(Value::as_str),
    );
    assert_eq!(
        Some(2),
        file_change_entry
            .metadata
            .get("summary")
            .and_then(|value| value.get("fileCount"))
            .and_then(Value::as_i64),
    );
    assert_eq!(
        Some(2),
        file_change_entry
            .metadata
            .get("summary")
            .and_then(|value| value.get("addLines"))
            .and_then(Value::as_i64),
    );
    assert_eq!(
        Some(3),
        file_change_entry
            .metadata
            .get("summary")
            .and_then(|value| value.get("removeLines"))
            .and_then(Value::as_i64),
    );

    let waited_entry = timeline_entry_from_thread_item(
        PRIMARY_RUNTIME_ID,
        "thread-1",
        Some("turn-2"),
        &json!({
            "id": "item-wait",
            "type": "collabAgentToolCall",
            "tool": "Wait",
            "receiverThreadIds": ["thread-a", "thread-b"]
        }),
        "thread_item",
        false,
        true,
    )
    .expect("应创建 collabAgentToolCall 时间线条目");

    assert_eq!(
        Some("waited"),
        waited_entry
            .metadata
            .get("semantic")
            .and_then(|value| value.get("kind"))
            .and_then(Value::as_str),
    );
    assert_eq!(
        Some("wait_agents"),
        waited_entry
            .metadata
            .get("semantic")
            .and_then(|value| value.get("detail"))
            .and_then(Value::as_str),
    );
    assert_eq!(
        Some(2),
        waited_entry
            .metadata
            .get("summary")
            .and_then(|value| value.get("waitCount"))
            .and_then(Value::as_i64),
    );
}