lash-runtime 0.1.0-alpha.39

Durable agent runtime for Rust: sessions, turns, tools, plugins. Embeddable facade over lash-core.
Documentation
use super::super::*;
use super::contracts::{
    GraphContract, assert_all_processes_terminal, assert_failed_code_block_present,
    assert_graph_lineage_connected, assert_labeled_resource_operation,
    assert_no_duplicate_label_step, assert_no_false_submitted_success,
    assert_no_forbidden_error_text, assert_subagent_bridge_exec_graphs,
};
use super::harness::{
    ExpectedContracts, LashE2eCase, run_session_turn_process_case, run_turn_case,
    run_turn_case_without_success_assertions,
};

#[test]
fn lash_e2e_foreground_labeled_tool_call() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-foreground-tool", || async {
        let case = LashE2eCase {
            name: "foreground labeled tool call",
            session_id: "lash-e2e-foreground-tool",
            scripted_provider_responses: vec![
                r#"```lashlang
@label(title: "Lookup app state")
value = await tools.app_lookup({})?
submit value
```"#,
            ],
            root_prompt: "Call the app lookup tool and submit its value.",
            expected_submitted_value: Some(serde_json::json!({ "ok": true })),
            tool_provider: Some(Arc::new(AppTools)),
            install_subagents: false,
            max_turns: None,
            expected_contracts: ExpectedContracts {
                labeled_resource_titles: vec!["Lookup app state"],
                ..ExpectedContracts::default()
            },
        };

        let run = run_turn_case(case).await?;
        assert_eq!(run.prompt_captures.len(), 1);
        Ok(())
    })
}

#[test]
fn lash_e2e_started_process_labeled_tool_call() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-started-process-tool", || async {
        run_turn_case(LashE2eCase {
            name: "started process labeled tool call",
            session_id: "lash-e2e-process-tool",
            scripted_provider_responses: vec![
                r#"```lashlang
process lookup(tools: Tools) {
  @label(title: "Lookup app state in process")
  value = await tools.app_lookup({})?
  finish value
}
handle = start lookup(tools: tools)
result = (await handle)?
submit result
```"#,
            ],
            root_prompt: "Start a process that calls the app lookup tool.",
            expected_submitted_value: Some(serde_json::json!({ "ok": true })),
            tool_provider: Some(Arc::new(AppTools)),
            install_subagents: false,
            max_turns: None,
            expected_contracts: ExpectedContracts {
                labeled_resource_titles: vec!["Lookup app state in process"],
                completed_process_entries: vec!["lookup"],
                min_completed_process_graphs: 1,
                ..ExpectedContracts::default()
            },
        })
        .await?;
        Ok(())
    })
}

#[test]
fn lash_e2e_started_process_labeled_subagent_spawn() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-started-process-subagent", || async {
        run_turn_case(LashE2eCase {
            name: "started process labeled subagent spawn",
            session_id: "lash-e2e-process-subagent",
            scripted_provider_responses: vec![
                r#"```lashlang
process spawn_child() {
  @label(title: "Spawn subagent with web search")
  result = await agents.spawn({
    capability: "default",
    task: "Submit `{ len: len(chunk) }` using the seeded `chunk` variable.",
    seed: { chunk: ["a", "b"] },
    output: Type { len: int }
  })?
  finish result
}
handle = start spawn_child()
result = (await handle)?
submit result
```"#,
                "```lashlang\nsubmit { len: len(chunk) }\n```",
            ],
            root_prompt: "Run a Lashlang process that spawns a subagent and returns its value.",
            expected_submitted_value: Some(serde_json::json!({ "len": 2 })),
            tool_provider: None,
            install_subagents: true,
            max_turns: None,
            expected_contracts: ExpectedContracts {
                labeled_resource_titles: vec!["Spawn subagent with web search"],
                completed_process_entries: vec!["spawn_child"],
                min_completed_child_session_exec_graphs: 1,
                min_completed_process_graphs: 1,
                ..ExpectedContracts::default()
            },
        })
        .await?;
        Ok(())
    })
}

#[test]
fn lash_e2e_nested_process_start_await() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-nested-process", || async {
        run_turn_case(LashE2eCase {
            name: "nested process start await",
            session_id: "lash-e2e-nested-process",
            scripted_provider_responses: vec![
                r#"```lashlang
process child() {
  finish { child: "done" }
}
process parent() {
  @label(title: "Start nested child process")
  handle = start child()
  result = (await handle)?
  finish { parent: result.child }
}
handle = start parent()
result = (await handle)?
submit result
```"#,
            ],
            root_prompt: "Start a parent process that starts and awaits a child process.",
            expected_submitted_value: Some(serde_json::json!({ "parent": "done" })),
            tool_provider: None,
            install_subagents: false,
            max_turns: None,
            expected_contracts: ExpectedContracts {
                labeled_node_titles: vec!["Start nested child process"],
                completed_process_entries: vec!["parent", "child"],
                min_completed_process_graphs: 2,
                ..ExpectedContracts::default()
            },
        })
        .await?;
        Ok(())
    })
}

#[test]
fn lash_e2e_session_turn_process_child() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-session-turn-process", || async {
        run_session_turn_process_case().await
    })
}

#[test]
fn lash_e2e_failed_child_preserves_failure_graph() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-failed-child", || async {
        let run = run_turn_case_without_success_assertions(LashE2eCase {
            name: "failed child preserves failure graph",
            session_id: "lash-e2e-failed-child",
            scripted_provider_responses: vec![
                r#"```lashlang
@label(title: "Spawn failing subagent")
result = await agents.spawn({
  capability: "default",
  task: "Fail with reason child boom.",
  seed: {},
  output: Type { reason: str }
})?
submit result
```"#,
                "```lashlang\nawait tools.submit_error({ reason: \"child boom\" })?\n```",
            ],
            root_prompt: "Spawn a child that fails and preserve its execution graph.",
            expected_submitted_value: None,
            tool_provider: None,
            install_subagents: true,
            max_turns: Some(1),
            expected_contracts: ExpectedContracts::default(),
        })
        .await?;

        assert_failed_code_block_present(&run.streamed_events);
        assert_no_forbidden_error_text(&run.streamed_events);
        assert_no_false_submitted_success(&run);
        assert_all_processes_terminal(&run.final_process_list);
        let contract = GraphContract::from_graphs(&run.graph_snapshots);
        assert_labeled_resource_operation(
            &contract,
            "Spawn failing subagent",
            crate::tracing::TraceLashlangNodeStatus::Failed,
        );
        assert_no_duplicate_label_step(&contract, "Spawn failing subagent");
        assert_graph_lineage_connected(&contract, &run.final_process_list);
        assert_subagent_bridge_exec_graphs(&run, crate::tracing::TraceLashlangStatus::Completed);
        Ok(())
    })
}

#[test]
fn lash_e2e_parallel_spawn_and_join() -> Result<()> {
    run_async_test_on_large_stack("lash-e2e-parallel-spawn-join", || async {
        run_turn_case(LashE2eCase {
            name: "parallel process spawn and join",
            session_id: "lash-e2e-parallel-processes",
            scripted_provider_responses: vec![
                r#"```lashlang
process child(value: str) {
  finish value
}
@label(title: "Start left process")
left = start child(value: "left")
@label(title: "Start right process")
right = start child(value: "right")
left_value = (await left)?
right_value = (await right)?
submit { joined: [left_value, right_value] }
```"#,
            ],
            root_prompt: "Start two processes, await both, and submit their joined result.",
            expected_submitted_value: Some(serde_json::json!({ "joined": ["left", "right"] })),
            tool_provider: None,
            install_subagents: false,
            max_turns: None,
            expected_contracts: ExpectedContracts {
                labeled_node_titles: vec!["Start left process", "Start right process"],
                completed_process_entries: vec!["child"],
                min_completed_process_graphs: 2,
                ..ExpectedContracts::default()
            },
        })
        .await?;
        Ok(())
    })
}