opencrabs 0.5.4

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
//! The `deliver_to` session grammar resolves a target session (fork #144).
//!
//! Cron jobs can now deliver results into a session's notify queue via
//! `session:<uuid|prefix>` — the target is resolved through the shared
//! session-id resolver (`crate::cli::session_resolve`). These tests pin the
//! pure resolution core (`resolve_session_target`) against in-memory session
//! sets: full UUIDs pass through, unambiguous prefixes of any length
//! resolve, garbage rejects, duplicate rows are ambiguous. The DB-loading wrapper (`parse_session_target`) is a thin
//! async shell over this core — prefix behavior against a live DB is the
//! resolver's own test-suite concern.

use crate::cron::scheduler::resolve_session_target;
use uuid::Uuid;

fn session_with_id(id: Uuid) -> crate::db::models::Session {
    crate::db::models::Session {
        id,
        title: None,
        model: None,
        provider_name: None,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        archived_at: None,
        token_count: 0,
        total_cost: 0.0,
        working_directory: None,
        auto_title_attempted: false,
        project_id: None,
        channel_chat_key: None,
    }
}

/// A full UUID passes through untouched — the resolver's fast path, no DB.
#[test]
fn full_uuid_resolves_without_db() {
    let id = Uuid::new_v4();
    let resolved = resolve_session_target(&[], &id.to_string()).unwrap();
    assert_eq!(resolved, id);
}

/// A full UUID resolves even when the session set is non-empty and doesn't
/// contain it (fast-path passthrough parity — presence is the caller's check).
#[test]
fn full_uuid_passthrough_ignores_set() {
    let id = Uuid::new_v4();
    let other = session_with_id(Uuid::new_v4());
    let resolved = resolve_session_target(&[other], &id.to_string()).unwrap();
    assert_eq!(resolved, id);
}

/// A valid prefix (8+ chars, exactly one match) resolves to the full id.
#[test]
fn valid_prefix_resolves() {
    let id = Uuid::new_v4();
    let sessions = vec![session_with_id(id)];
    let prefix = id.to_string()[..8].to_string();
    assert_eq!(resolve_session_target(&sessions, &prefix), Some(id));
}

/// A prefix matching no session is `None` (resolver: 0 matches -> Err).
#[test]
fn unknown_prefix_rejects() {
    let sessions = vec![session_with_id(Uuid::new_v4())];
    assert_eq!(resolve_session_target(&sessions, "zzzzzzzz"), None);
}

/// A prefix shorter than the 8 chars `session list` displays still resolves
/// when unambiguous — the resolver matches any-length prefixes; the 8-char
/// figure is a display convention, not a matching rule.
#[test]
fn short_unambiguous_prefix_resolves() {
    let id = Uuid::new_v4();
    let sessions = vec![session_with_id(id)];
    let short = id.to_string()[..4].to_string();
    assert_eq!(resolve_session_target(&sessions, &short), Some(id));
}

/// Garbage (not a UUID, not a prefix of anything) is `None`, not a panic.
#[test]
fn garbage_rejects() {
    assert_eq!(
        resolve_session_target(&[], "not-a-uuid-or-prefix-at-all"),
        None
    );
    assert_eq!(resolve_session_target(&[], ""), None);
}

/// The resolver is row-count-based: two rows sharing a prefix are ambiguous
/// even when the rows carry the SAME id — `None`, matching the shared
/// resolver's contract verbatim (row count, not distinct-id count).
#[test]
fn duplicate_rows_are_ambiguous() {
    let id = Uuid::new_v4();
    let sessions = vec![session_with_id(id), session_with_id(id)];
    let prefix = id.to_string()[..8].to_string();
    assert_eq!(resolve_session_target(&sessions, &prefix), None);
}

#[test]
fn session_target_recognition_and_extraction() {
    use crate::channels::target_resolver::{extract_session_target, is_session_target};

    let full_uuid = "12345678-1234-1234-1234-123456789abc";
    let url_target = format!("oc://session/{full_uuid}");
    let legacy_target = format!("session:{full_uuid}");
    let channel_target = "telegram:123456:78";
    let url_channel = "oc://telegram/123456/78";

    assert!(is_session_target(&url_target));
    assert!(is_session_target(&legacy_target));
    assert!(!is_session_target(channel_target));
    assert!(!is_session_target(url_channel));

    assert_eq!(extract_session_target(&url_target), Some(full_uuid));
    assert_eq!(extract_session_target(&legacy_target), Some(full_uuid));
    assert_eq!(extract_session_target(channel_target), None);
    assert_eq!(extract_session_target(url_channel), None);
}

#[tokio::test]
async fn bake_delivery_target_bakes_session_target_to_session_uuid() {
    use crate::brain::tools::ToolExecutionContext;
    use crate::brain::tools::cron_manage::bake_delivery_target;

    let full_uuid = "12345678-1234-1234-1234-123456789abc";
    let url_target = format!("oc://session/{full_uuid}");
    let legacy_target = format!("session:{full_uuid}");
    let ctx = ToolExecutionContext::new(Uuid::new_v4());

    let baked_url = bake_delivery_target(&url_target, &ctx).await.unwrap();
    let baked_legacy = bake_delivery_target(&legacy_target, &ctx).await.unwrap();

    assert_eq!(baked_url, format!("session:{full_uuid}"));
    assert_eq!(baked_legacy, format!("session:{full_uuid}"));
}

#[test]
fn resolved_target_deliver_to_session_format() {
    use crate::channels::target_resolver::{ResolvedTarget, TargetDestination};

    let id = Uuid::new_v4();
    let rt = ResolvedTarget {
        session: Some(id),
        destination: TargetDestination::Session(id),
    };

    assert_eq!(rt.deliver_to(), format!("session:{id}"));
}