lash-core 0.1.0-alpha.1

Sans-IO turn machine and runtime kernel for the lash agent runtime.
Documentation
use super::*;

#[test]
fn session_usage_report_aggregates_sources_and_models() {
    let entries = vec![
        TokenLedgerEntry {
            source: "turn".to_string(),
            model: "gpt-5.4-mini".to_string(),
            usage: TokenUsage {
                input_tokens: 10,
                output_tokens: 2,
                cached_input_tokens: 3,
                reasoning_tokens: 1,
            },
        },
        TokenLedgerEntry {
            source: "observer".to_string(),
            model: "gpt-5.4-mini".to_string(),
            usage: TokenUsage {
                input_tokens: 7,
                output_tokens: 1,
                cached_input_tokens: 0,
                reasoning_tokens: 0,
            },
        },
        TokenLedgerEntry {
            source: "turn".to_string(),
            model: "gpt-5.4".to_string(),
            usage: TokenUsage {
                input_tokens: 20,
                output_tokens: 4,
                cached_input_tokens: 5,
                reasoning_tokens: 2,
            },
        },
    ];

    let report = SessionUsageReport::from_entries(&entries);

    assert_eq!(report.entry_count, 3);
    assert_eq!(report.usage.input_tokens, 37);
    assert_eq!(report.usage.output_tokens, 7);
    assert_eq!(report.usage.cached_input_tokens, 8);
    assert_eq!(report.usage.reasoning_tokens, 3);
    assert_eq!(report.usage.total_tokens, 47);
    assert_eq!(report.usage.context_total_tokens, 55);
    assert_eq!(report.by_source["turn"].input_tokens, 30);
    assert_eq!(report.by_source["observer"].output_tokens, 1);
    assert_eq!(report.by_model["gpt-5.4-mini"].input_tokens, 17);
    assert_eq!(report.by_model["gpt-5.4"].reasoning_tokens, 2);

    let delta = diff_token_ledger(
        &[TokenLedgerEntry {
            source: "turn".to_string(),
            model: "gpt-5.4-mini".to_string(),
            usage: TokenUsage {
                input_tokens: 10,
                output_tokens: 2,
                cached_input_tokens: 3,
                reasoning_tokens: 1,
            },
        }],
        &entries,
    )
    .expect("delta");
    assert_eq!(delta.len(), 2);
    assert_eq!(delta[0].source, "observer");
    assert_eq!(delta[1].model, "gpt-5.4");
}

#[tokio::test]
async fn await_background_work_waits_for_registered_jobs() {
    let runtime = standard_runtime_with_transport_and_background(mock_provider(Vec::new())).await;
    let manager = runtime.session_manager().expect("session manager");
    let observed = Arc::new(AtomicBool::new(false));
    let observed_task = Arc::clone(&observed);

    manager
        .spawn_hidden_task(
            "root",
            "test",
            Box::pin(async move {
                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
                observed_task.store(true, Ordering::SeqCst);
                Ok(())
            }),
        )
        .await
        .expect("spawn background job");

    let mut runtime = runtime;
    runtime
        .await_background_work()
        .await
        .expect("await background work");
    assert!(observed.load(Ordering::SeqCst));
}

#[tokio::test]
async fn await_background_work_does_not_cross_runtime_sessions_with_same_logical_id() {
    let executor: Arc<dyn BackgroundTaskHost> = Arc::new(LocalBackgroundTaskHost::default());
    let runtime_one = standard_runtime_with_shared_background_executor(
        mock_provider(Vec::new()),
        Arc::clone(&executor),
    )
    .await;
    let runtime_two = standard_runtime_with_shared_background_executor(
        mock_provider(Vec::new()),
        Arc::clone(&executor),
    )
    .await;
    let manager_one = runtime_one.session_manager().expect("session manager");
    let observed = Arc::new(AtomicBool::new(false));
    let observed_task = Arc::clone(&observed);
    manager_one
        .spawn_hidden_task(
            "root",
            "test",
            Box::pin(async move {
                tokio::time::sleep(std::time::Duration::from_millis(40)).await;
                observed_task.store(true, Ordering::SeqCst);
                Ok(())
            }),
        )
        .await
        .expect("spawn background job");

    let mut runtime_two = runtime_two;
    tokio::time::timeout(
        std::time::Duration::from_millis(10),
        runtime_two.await_background_work(),
    )
    .await
    .expect("second runtime should not block on first runtime jobs")
    .expect("await background work");
    assert!(!observed.load(Ordering::SeqCst));

    let mut runtime_one = runtime_one;
    runtime_one
        .await_background_work()
        .await
        .expect("first runtime await background work");
    assert!(observed.load(Ordering::SeqCst));
}