defect-agent 0.1.0-alpha.2

Core agent runtime for defect: turn loop, context compaction, tools and session orchestration.
Documentation
use super::*;

use agent_client_protocol_schema::PermissionOptionId;

fn id(s: &str) -> ToolCallId {
    ToolCallId::new(s.to_string())
}

#[tokio::test]
async fn resolve_wakes_waiter() {
    let gate = PermissionGate::new();
    let cancel = CancellationToken::new();
    let id = id("call-1");

    let waiter = {
        let gate = &gate;
        let cancel = cancel.clone();
        let id = id.clone();
        async move { gate.wait(id, cancel).await }
    };
    let resolver = async {
        tokio::task::yield_now().await;
        gate.resolve(
            &id,
            PermissionResolution::Selected {
                option_id: PermissionOptionId::new("allow_once".to_string()),
            },
        );
    };

    let (outcome, _) = tokio::join!(waiter, resolver);
    match outcome {
        PermissionResolution::Selected { option_id } => {
            assert_eq!(option_id.0.as_ref(), "allow_once");
        }
        other => panic!("unexpected outcome: {other:?}"),
    }
}

#[tokio::test]
async fn cancel_returns_cancelled() {
    let gate = PermissionGate::new();
    let cancel = CancellationToken::new();
    let id = id("call-2");

    let waiter = {
        let gate = &gate;
        let cancel = cancel.clone();
        let id = id.clone();
        async move { gate.wait(id, cancel).await }
    };
    let canceller = async {
        tokio::task::yield_now().await;
        cancel.cancel();
    };

    let (outcome, _) = tokio::join!(waiter, canceller);
    assert!(matches!(outcome, PermissionResolution::Cancelled));
}

#[tokio::test]
async fn resolve_without_waiter_is_noop() {
    let gate = PermissionGate::new();
    gate.resolve(
        &id("ghost"),
        PermissionResolution::Selected {
            option_id: PermissionOptionId::new("x".to_string()),
        },
    );
}