typeduck-codex-web 0.4.0

A standalone browser interface and Codex runtime
Documentation
use std::sync::atomic::AtomicU64;

use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::sync::Mutex;
use tokio::sync::broadcast;

use super::EventRenderer;
use super::LiveEventHub;
use super::ReplayState;
use super::truncated_append;

#[tokio::test]
async fn item_ids_are_scoped_by_thread_and_turn() {
    let mut renderer = EventRenderer::default();
    let first = renderer
        .render(json!({
            "method": "item/agentMessage/delta",
            "params": { "threadId": "thread-a", "turnId": "turn-a", "itemId": "same", "delta": "A" }
        }))
        .await
        .expect("first event");
    let second = renderer
        .render(json!({
            "method": "item/agentMessage/delta",
            "params": { "threadId": "thread-b", "turnId": "turn-b", "itemId": "same", "delta": "B" }
        }))
        .await
        .expect("second event");

    assert_eq!(first.get("threadId"), Some(&json!("thread-a")));
    assert_eq!(second.get("threadId"), Some(&json!("thread-b")));
    assert!(
        first
            .get("html")
            .and_then(serde_json::Value::as_str)
            .is_some_and(|html| html.contains("A"))
    );
    assert!(
        second
            .get("html")
            .and_then(serde_json::Value::as_str)
            .is_some_and(|html| html.contains("B"))
    );
}

#[tokio::test]
async fn plan_actions_wait_for_item_and_turn_completion() {
    let mut renderer = EventRenderer::default();
    let started = renderer
        .render(json!({
            "method": "item/started",
            "params": {
                "threadId": "thread-a",
                "turnId": "turn-a",
                "item": { "id": "plan-a", "type": "plan", "text": "# Draft" }
            }
        }))
        .await
        .expect("started plan");
    let completed = renderer
        .render(json!({
            "method": "item/completed",
            "params": {
                "threadId": "thread-a",
                "turnId": "turn-a",
                "item": { "id": "plan-a", "type": "plan", "text": "# Final" }
            }
        }))
        .await
        .expect("completed plan");
    let started_html = started.get("html").and_then(serde_json::Value::as_str);
    let completed_html = completed.get("html").and_then(serde_json::Value::as_str);

    assert!(started_html.is_some_and(|html| {
        html.contains("data-plan-complete=\"false\"")
            && html.contains("data-plan-actionable=\"false\"")
    }));
    assert!(completed_html.is_some_and(|html| {
        html.contains("data-plan-complete=\"true\"")
            && html.contains("data-plan-actionable=\"false\"")
    }));
    assert_eq!(completed.get("turnId"), Some(&json!("turn-a")));
}

#[tokio::test]
async fn turn_events_preserve_the_nested_turn_id() {
    let mut renderer = EventRenderer::default();
    let event = renderer
        .render(json!({
            "method": "turn/started",
            "params": {
                "threadId": "thread-a",
                "turn": { "id": "turn-a" }
            }
        }))
        .await
        .expect("turn event");

    assert_eq!(
        event,
        json!({
            "op": "turn",
            "threadId": "thread-a",
            "turnId": "turn-a",
            "status": "inProgress"
        })
    );
}

#[tokio::test]
async fn thread_settings_updates_project_permission_state() {
    let mut renderer = EventRenderer::default();
    let event = renderer
        .render(json!({
            "method": "thread/settings/updated",
            "params": {
                "threadId": "thread-a",
                "threadSettings": {
                    "activePermissionProfile": { "id": ":danger-full-access" },
                    "approvalPolicy": "never",
                    "sandboxPolicy": { "type": "danger-full-access" }
                }
            }
        }))
        .await
        .expect("settings event");

    assert_eq!(
        event,
        json!({
            "op": "threadSettings",
            "activePermissionProfile": { "id": ":danger-full-access" },
            "approvalPolicy": "never",
            "sandboxPolicy": { "type": "danger-full-access" },
            "threadId": "thread-a",
            "turnId": ""
        })
    );
}

#[test]
fn streamed_output_has_a_hard_cap() {
    let existing = "a".repeat(256 * 1024);
    let combined = truncated_append(&existing, "tail");

    assert!(combined.len() < existing.len() + 64);
    assert!(combined.ends_with("tail"));
    assert!(combined.starts_with("… output truncated …"));
}

#[tokio::test]
async fn replay_returns_only_events_after_the_client_watermark() {
    let (events, _) = broadcast::channel(8);
    let hub = LiveEventHub {
        next_seq: AtomicU64::new(1),
        replay: Mutex::new(ReplayState::default()),
        events,
    };
    hub.publish(json!({ "op": "first" })).await;
    hub.publish(json!({ "op": "second" })).await;

    let replay = hub.replay_after(Some(1)).await;

    assert!(!replay.gap);
    assert_eq!(
        replay
            .events
            .iter()
            .map(|event| event.seq)
            .collect::<Vec<_>>(),
        vec![2]
    );
}