mod.rs

   1use super::*;
   2use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId};
   3use action_log::ActionLog;
   4use agent_client_protocol::{self as acp};
   5use agent_settings::AgentProfileId;
   6use anyhow::Result;
   7use client::{Client, UserStore};
   8use fs::{FakeFs, Fs};
   9use futures::channel::mpsc::UnboundedReceiver;
  10use gpui::{
  11    App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
  12};
  13use indoc::indoc;
  14use language_model::{
  15    LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry,
  16    LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent,
  17    Role, StopReason, fake_provider::FakeLanguageModel,
  18};
  19use pretty_assertions::assert_eq;
  20use project::Project;
  21use prompt_store::ProjectContext;
  22use reqwest_client::ReqwestClient;
  23use schemars::JsonSchema;
  24use serde::{Deserialize, Serialize};
  25use serde_json::json;
  26use settings::SettingsStore;
  27use smol::stream::StreamExt;
  28use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
  29use util::path;
  30
  31mod test_tools;
  32use test_tools::*;
  33
  34#[gpui::test]
  35#[ignore = "can't run on CI yet"]
  36async fn test_echo(cx: &mut TestAppContext) {
  37    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
  38
  39    let events = thread
  40        .update(cx, |thread, cx| {
  41            thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx)
  42        })
  43        .unwrap()
  44        .collect()
  45        .await;
  46    thread.update(cx, |thread, _cx| {
  47        assert_eq!(
  48            thread.last_message().unwrap().to_markdown(),
  49            indoc! {"
  50                ## Assistant
  51
  52                Hello
  53            "}
  54        )
  55    });
  56    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
  57}
  58
  59#[gpui::test]
  60#[ignore = "can't run on CI yet"]
  61async fn test_thinking(cx: &mut TestAppContext) {
  62    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
  63
  64    let events = thread
  65        .update(cx, |thread, cx| {
  66            thread.send(
  67                UserMessageId::new(),
  68                [indoc! {"
  69                    Testing:
  70
  71                    Generate a thinking step where you just think the word 'Think',
  72                    and have your final answer be 'Hello'
  73                "}],
  74                cx,
  75            )
  76        })
  77        .unwrap()
  78        .collect()
  79        .await;
  80    thread.update(cx, |thread, _cx| {
  81        assert_eq!(
  82            thread.last_message().unwrap().to_markdown(),
  83            indoc! {"
  84                ## Assistant
  85
  86                <think>Think</think>
  87                Hello
  88            "}
  89        )
  90    });
  91    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
  92}
  93
  94#[gpui::test]
  95async fn test_system_prompt(cx: &mut TestAppContext) {
  96    let ThreadTest {
  97        model,
  98        thread,
  99        project_context,
 100        ..
 101    } = setup(cx, TestModel::Fake).await;
 102    let fake_model = model.as_fake();
 103
 104    project_context.borrow_mut().shell = "test-shell".into();
 105    thread.update(cx, |thread, _| thread.add_tool(EchoTool));
 106    thread
 107        .update(cx, |thread, cx| {
 108            thread.send(UserMessageId::new(), ["abc"], cx)
 109        })
 110        .unwrap();
 111    cx.run_until_parked();
 112    let mut pending_completions = fake_model.pending_completions();
 113    assert_eq!(
 114        pending_completions.len(),
 115        1,
 116        "unexpected pending completions: {:?}",
 117        pending_completions
 118    );
 119
 120    let pending_completion = pending_completions.pop().unwrap();
 121    assert_eq!(pending_completion.messages[0].role, Role::System);
 122
 123    let system_message = &pending_completion.messages[0];
 124    let system_prompt = system_message.content[0].to_str().unwrap();
 125    assert!(
 126        system_prompt.contains("test-shell"),
 127        "unexpected system message: {:?}",
 128        system_message
 129    );
 130    assert!(
 131        system_prompt.contains("## Fixing Diagnostics"),
 132        "unexpected system message: {:?}",
 133        system_message
 134    );
 135}
 136
 137#[gpui::test]
 138async fn test_prompt_caching(cx: &mut TestAppContext) {
 139    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
 140    let fake_model = model.as_fake();
 141
 142    // Send initial user message and verify it's cached
 143    thread
 144        .update(cx, |thread, cx| {
 145            thread.send(UserMessageId::new(), ["Message 1"], cx)
 146        })
 147        .unwrap();
 148    cx.run_until_parked();
 149
 150    let completion = fake_model.pending_completions().pop().unwrap();
 151    assert_eq!(
 152        completion.messages[1..],
 153        vec![LanguageModelRequestMessage {
 154            role: Role::User,
 155            content: vec!["Message 1".into()],
 156            cache: true
 157        }]
 158    );
 159    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
 160        "Response to Message 1".into(),
 161    ));
 162    fake_model.end_last_completion_stream();
 163    cx.run_until_parked();
 164
 165    // Send another user message and verify only the latest is cached
 166    thread
 167        .update(cx, |thread, cx| {
 168            thread.send(UserMessageId::new(), ["Message 2"], cx)
 169        })
 170        .unwrap();
 171    cx.run_until_parked();
 172
 173    let completion = fake_model.pending_completions().pop().unwrap();
 174    assert_eq!(
 175        completion.messages[1..],
 176        vec![
 177            LanguageModelRequestMessage {
 178                role: Role::User,
 179                content: vec!["Message 1".into()],
 180                cache: false
 181            },
 182            LanguageModelRequestMessage {
 183                role: Role::Assistant,
 184                content: vec!["Response to Message 1".into()],
 185                cache: false
 186            },
 187            LanguageModelRequestMessage {
 188                role: Role::User,
 189                content: vec!["Message 2".into()],
 190                cache: true
 191            }
 192        ]
 193    );
 194    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
 195        "Response to Message 2".into(),
 196    ));
 197    fake_model.end_last_completion_stream();
 198    cx.run_until_parked();
 199
 200    // Simulate a tool call and verify that the latest tool result is cached
 201    thread.update(cx, |thread, _| thread.add_tool(EchoTool));
 202    thread
 203        .update(cx, |thread, cx| {
 204            thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
 205        })
 206        .unwrap();
 207    cx.run_until_parked();
 208
 209    let tool_use = LanguageModelToolUse {
 210        id: "tool_1".into(),
 211        name: EchoTool.name().into(),
 212        raw_input: json!({"text": "test"}).to_string(),
 213        input: json!({"text": "test"}),
 214        is_input_complete: true,
 215    };
 216    fake_model
 217        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
 218    fake_model.end_last_completion_stream();
 219    cx.run_until_parked();
 220
 221    let completion = fake_model.pending_completions().pop().unwrap();
 222    let tool_result = LanguageModelToolResult {
 223        tool_use_id: "tool_1".into(),
 224        tool_name: EchoTool.name().into(),
 225        is_error: false,
 226        content: "test".into(),
 227        output: Some("test".into()),
 228    };
 229    assert_eq!(
 230        completion.messages[1..],
 231        vec![
 232            LanguageModelRequestMessage {
 233                role: Role::User,
 234                content: vec!["Message 1".into()],
 235                cache: false
 236            },
 237            LanguageModelRequestMessage {
 238                role: Role::Assistant,
 239                content: vec!["Response to Message 1".into()],
 240                cache: false
 241            },
 242            LanguageModelRequestMessage {
 243                role: Role::User,
 244                content: vec!["Message 2".into()],
 245                cache: false
 246            },
 247            LanguageModelRequestMessage {
 248                role: Role::Assistant,
 249                content: vec!["Response to Message 2".into()],
 250                cache: false
 251            },
 252            LanguageModelRequestMessage {
 253                role: Role::User,
 254                content: vec!["Use the echo tool".into()],
 255                cache: false
 256            },
 257            LanguageModelRequestMessage {
 258                role: Role::Assistant,
 259                content: vec![MessageContent::ToolUse(tool_use)],
 260                cache: false
 261            },
 262            LanguageModelRequestMessage {
 263                role: Role::User,
 264                content: vec![MessageContent::ToolResult(tool_result)],
 265                cache: true
 266            }
 267        ]
 268    );
 269}
 270
 271#[gpui::test]
 272#[ignore = "can't run on CI yet"]
 273async fn test_basic_tool_calls(cx: &mut TestAppContext) {
 274    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 275
 276    // Test a tool call that's likely to complete *before* streaming stops.
 277    let events = thread
 278        .update(cx, |thread, cx| {
 279            thread.add_tool(EchoTool);
 280            thread.send(
 281                UserMessageId::new(),
 282                ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."],
 283                cx,
 284            )
 285        })
 286        .unwrap()
 287        .collect()
 288        .await;
 289    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
 290
 291    // Test a tool calls that's likely to complete *after* streaming stops.
 292    let events = thread
 293        .update(cx, |thread, cx| {
 294            thread.remove_tool(&AgentTool::name(&EchoTool));
 295            thread.add_tool(DelayTool);
 296            thread.send(
 297                UserMessageId::new(),
 298                [
 299                    "Now call the delay tool with 200ms.",
 300                    "When the timer goes off, then you echo the output of the tool.",
 301                ],
 302                cx,
 303            )
 304        })
 305        .unwrap()
 306        .collect()
 307        .await;
 308    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
 309    thread.update(cx, |thread, _cx| {
 310        assert!(
 311            thread
 312                .last_message()
 313                .unwrap()
 314                .as_agent_message()
 315                .unwrap()
 316                .content
 317                .iter()
 318                .any(|content| {
 319                    if let AgentMessageContent::Text(text) = content {
 320                        text.contains("Ding")
 321                    } else {
 322                        false
 323                    }
 324                }),
 325            "{}",
 326            thread.to_markdown()
 327        );
 328    });
 329}
 330
 331#[gpui::test]
 332#[ignore = "can't run on CI yet"]
 333async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
 334    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 335
 336    // Test a tool call that's likely to complete *before* streaming stops.
 337    let mut events = thread
 338        .update(cx, |thread, cx| {
 339            thread.add_tool(WordListTool);
 340            thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
 341        })
 342        .unwrap();
 343
 344    let mut saw_partial_tool_use = false;
 345    while let Some(event) = events.next().await {
 346        if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
 347            thread.update(cx, |thread, _cx| {
 348                // Look for a tool use in the thread's last message
 349                let message = thread.last_message().unwrap();
 350                let agent_message = message.as_agent_message().unwrap();
 351                let last_content = agent_message.content.last().unwrap();
 352                if let AgentMessageContent::ToolUse(last_tool_use) = last_content {
 353                    assert_eq!(last_tool_use.name.as_ref(), "word_list");
 354                    if tool_call.status == acp::ToolCallStatus::Pending {
 355                        if !last_tool_use.is_input_complete
 356                            && last_tool_use.input.get("g").is_none()
 357                        {
 358                            saw_partial_tool_use = true;
 359                        }
 360                    } else {
 361                        last_tool_use
 362                            .input
 363                            .get("a")
 364                            .expect("'a' has streamed because input is now complete");
 365                        last_tool_use
 366                            .input
 367                            .get("g")
 368                            .expect("'g' has streamed because input is now complete");
 369                    }
 370                } else {
 371                    panic!("last content should be a tool use");
 372                }
 373            });
 374        }
 375    }
 376
 377    assert!(
 378        saw_partial_tool_use,
 379        "should see at least one partially streamed tool use in the history"
 380    );
 381}
 382
 383#[gpui::test]
 384async fn test_tool_authorization(cx: &mut TestAppContext) {
 385    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
 386    let fake_model = model.as_fake();
 387
 388    let mut events = thread
 389        .update(cx, |thread, cx| {
 390            thread.add_tool(ToolRequiringPermission);
 391            thread.send(UserMessageId::new(), ["abc"], cx)
 392        })
 393        .unwrap();
 394    cx.run_until_parked();
 395    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
 396        LanguageModelToolUse {
 397            id: "tool_id_1".into(),
 398            name: ToolRequiringPermission.name().into(),
 399            raw_input: "{}".into(),
 400            input: json!({}),
 401            is_input_complete: true,
 402        },
 403    ));
 404    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
 405        LanguageModelToolUse {
 406            id: "tool_id_2".into(),
 407            name: ToolRequiringPermission.name().into(),
 408            raw_input: "{}".into(),
 409            input: json!({}),
 410            is_input_complete: true,
 411        },
 412    ));
 413    fake_model.end_last_completion_stream();
 414    let tool_call_auth_1 = next_tool_call_authorization(&mut events).await;
 415    let tool_call_auth_2 = next_tool_call_authorization(&mut events).await;
 416
 417    // Approve the first
 418    tool_call_auth_1
 419        .response
 420        .send(tool_call_auth_1.options[1].id.clone())
 421        .unwrap();
 422    cx.run_until_parked();
 423
 424    // Reject the second
 425    tool_call_auth_2
 426        .response
 427        .send(tool_call_auth_1.options[2].id.clone())
 428        .unwrap();
 429    cx.run_until_parked();
 430
 431    let completion = fake_model.pending_completions().pop().unwrap();
 432    let message = completion.messages.last().unwrap();
 433    assert_eq!(
 434        message.content,
 435        vec![
 436            language_model::MessageContent::ToolResult(LanguageModelToolResult {
 437                tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
 438                tool_name: ToolRequiringPermission.name().into(),
 439                is_error: false,
 440                content: "Allowed".into(),
 441                output: Some("Allowed".into())
 442            }),
 443            language_model::MessageContent::ToolResult(LanguageModelToolResult {
 444                tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
 445                tool_name: ToolRequiringPermission.name().into(),
 446                is_error: true,
 447                content: "Permission to run tool denied by user".into(),
 448                output: None
 449            })
 450        ]
 451    );
 452
 453    // Simulate yet another tool call.
 454    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
 455        LanguageModelToolUse {
 456            id: "tool_id_3".into(),
 457            name: ToolRequiringPermission.name().into(),
 458            raw_input: "{}".into(),
 459            input: json!({}),
 460            is_input_complete: true,
 461        },
 462    ));
 463    fake_model.end_last_completion_stream();
 464
 465    // Respond by always allowing tools.
 466    let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
 467    tool_call_auth_3
 468        .response
 469        .send(tool_call_auth_3.options[0].id.clone())
 470        .unwrap();
 471    cx.run_until_parked();
 472    let completion = fake_model.pending_completions().pop().unwrap();
 473    let message = completion.messages.last().unwrap();
 474    assert_eq!(
 475        message.content,
 476        vec![language_model::MessageContent::ToolResult(
 477            LanguageModelToolResult {
 478                tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
 479                tool_name: ToolRequiringPermission.name().into(),
 480                is_error: false,
 481                content: "Allowed".into(),
 482                output: Some("Allowed".into())
 483            }
 484        )]
 485    );
 486
 487    // Simulate a final tool call, ensuring we don't trigger authorization.
 488    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
 489        LanguageModelToolUse {
 490            id: "tool_id_4".into(),
 491            name: ToolRequiringPermission.name().into(),
 492            raw_input: "{}".into(),
 493            input: json!({}),
 494            is_input_complete: true,
 495        },
 496    ));
 497    fake_model.end_last_completion_stream();
 498    cx.run_until_parked();
 499    let completion = fake_model.pending_completions().pop().unwrap();
 500    let message = completion.messages.last().unwrap();
 501    assert_eq!(
 502        message.content,
 503        vec![language_model::MessageContent::ToolResult(
 504            LanguageModelToolResult {
 505                tool_use_id: "tool_id_4".into(),
 506                tool_name: ToolRequiringPermission.name().into(),
 507                is_error: false,
 508                content: "Allowed".into(),
 509                output: Some("Allowed".into())
 510            }
 511        )]
 512    );
 513}
 514
 515#[gpui::test]
 516async fn test_tool_hallucination(cx: &mut TestAppContext) {
 517    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
 518    let fake_model = model.as_fake();
 519
 520    let mut events = thread
 521        .update(cx, |thread, cx| {
 522            thread.send(UserMessageId::new(), ["abc"], cx)
 523        })
 524        .unwrap();
 525    cx.run_until_parked();
 526    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
 527        LanguageModelToolUse {
 528            id: "tool_id_1".into(),
 529            name: "nonexistent_tool".into(),
 530            raw_input: "{}".into(),
 531            input: json!({}),
 532            is_input_complete: true,
 533        },
 534    ));
 535    fake_model.end_last_completion_stream();
 536
 537    let tool_call = expect_tool_call(&mut events).await;
 538    assert_eq!(tool_call.title, "nonexistent_tool");
 539    assert_eq!(tool_call.status, acp::ToolCallStatus::Pending);
 540    let update = expect_tool_call_update_fields(&mut events).await;
 541    assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed));
 542}
 543
 544#[gpui::test]
 545async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
 546    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
 547    let fake_model = model.as_fake();
 548
 549    let events = thread
 550        .update(cx, |thread, cx| {
 551            thread.add_tool(EchoTool);
 552            thread.send(UserMessageId::new(), ["abc"], cx)
 553        })
 554        .unwrap();
 555    cx.run_until_parked();
 556    let tool_use = LanguageModelToolUse {
 557        id: "tool_id_1".into(),
 558        name: EchoTool.name().into(),
 559        raw_input: "{}".into(),
 560        input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
 561        is_input_complete: true,
 562    };
 563    fake_model
 564        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
 565    fake_model.end_last_completion_stream();
 566
 567    cx.run_until_parked();
 568    let completion = fake_model.pending_completions().pop().unwrap();
 569    let tool_result = LanguageModelToolResult {
 570        tool_use_id: "tool_id_1".into(),
 571        tool_name: EchoTool.name().into(),
 572        is_error: false,
 573        content: "def".into(),
 574        output: Some("def".into()),
 575    };
 576    assert_eq!(
 577        completion.messages[1..],
 578        vec![
 579            LanguageModelRequestMessage {
 580                role: Role::User,
 581                content: vec!["abc".into()],
 582                cache: false
 583            },
 584            LanguageModelRequestMessage {
 585                role: Role::Assistant,
 586                content: vec![MessageContent::ToolUse(tool_use.clone())],
 587                cache: false
 588            },
 589            LanguageModelRequestMessage {
 590                role: Role::User,
 591                content: vec![MessageContent::ToolResult(tool_result.clone())],
 592                cache: true
 593            },
 594        ]
 595    );
 596
 597    // Simulate reaching tool use limit.
 598    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
 599        cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
 600    ));
 601    fake_model.end_last_completion_stream();
 602    let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
 603    assert!(
 604        last_event
 605            .unwrap_err()
 606            .is::<language_model::ToolUseLimitReachedError>()
 607    );
 608
 609    let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap();
 610    cx.run_until_parked();
 611    let completion = fake_model.pending_completions().pop().unwrap();
 612    assert_eq!(
 613        completion.messages[1..],
 614        vec![
 615            LanguageModelRequestMessage {
 616                role: Role::User,
 617                content: vec!["abc".into()],
 618                cache: false
 619            },
 620            LanguageModelRequestMessage {
 621                role: Role::Assistant,
 622                content: vec![MessageContent::ToolUse(tool_use)],
 623                cache: false
 624            },
 625            LanguageModelRequestMessage {
 626                role: Role::User,
 627                content: vec![MessageContent::ToolResult(tool_result)],
 628                cache: false
 629            },
 630            LanguageModelRequestMessage {
 631                role: Role::User,
 632                content: vec!["Continue where you left off".into()],
 633                cache: true
 634            }
 635        ]
 636    );
 637
 638    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into()));
 639    fake_model.end_last_completion_stream();
 640    events.collect::<Vec<_>>().await;
 641    thread.read_with(cx, |thread, _cx| {
 642        assert_eq!(
 643            thread.last_message().unwrap().to_markdown(),
 644            indoc! {"
 645                ## Assistant
 646
 647                Done
 648            "}
 649        )
 650    });
 651
 652    // Ensure we error if calling resume when tool use limit was *not* reached.
 653    let error = thread
 654        .update(cx, |thread, cx| thread.resume(cx))
 655        .unwrap_err();
 656    assert_eq!(
 657        error.to_string(),
 658        "can only resume after tool use limit is reached"
 659    )
 660}
 661
 662#[gpui::test]
 663async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
 664    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
 665    let fake_model = model.as_fake();
 666
 667    let events = thread
 668        .update(cx, |thread, cx| {
 669            thread.add_tool(EchoTool);
 670            thread.send(UserMessageId::new(), ["abc"], cx)
 671        })
 672        .unwrap();
 673    cx.run_until_parked();
 674
 675    let tool_use = LanguageModelToolUse {
 676        id: "tool_id_1".into(),
 677        name: EchoTool.name().into(),
 678        raw_input: "{}".into(),
 679        input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
 680        is_input_complete: true,
 681    };
 682    let tool_result = LanguageModelToolResult {
 683        tool_use_id: "tool_id_1".into(),
 684        tool_name: EchoTool.name().into(),
 685        is_error: false,
 686        content: "def".into(),
 687        output: Some("def".into()),
 688    };
 689    fake_model
 690        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
 691    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
 692        cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
 693    ));
 694    fake_model.end_last_completion_stream();
 695    let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
 696    assert!(
 697        last_event
 698            .unwrap_err()
 699            .is::<language_model::ToolUseLimitReachedError>()
 700    );
 701
 702    thread
 703        .update(cx, |thread, cx| {
 704            thread.send(UserMessageId::new(), vec!["ghi"], cx)
 705        })
 706        .unwrap();
 707    cx.run_until_parked();
 708    let completion = fake_model.pending_completions().pop().unwrap();
 709    assert_eq!(
 710        completion.messages[1..],
 711        vec![
 712            LanguageModelRequestMessage {
 713                role: Role::User,
 714                content: vec!["abc".into()],
 715                cache: false
 716            },
 717            LanguageModelRequestMessage {
 718                role: Role::Assistant,
 719                content: vec![MessageContent::ToolUse(tool_use)],
 720                cache: false
 721            },
 722            LanguageModelRequestMessage {
 723                role: Role::User,
 724                content: vec![MessageContent::ToolResult(tool_result)],
 725                cache: false
 726            },
 727            LanguageModelRequestMessage {
 728                role: Role::User,
 729                content: vec!["ghi".into()],
 730                cache: true
 731            }
 732        ]
 733    );
 734}
 735
 736async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
 737    let event = events
 738        .next()
 739        .await
 740        .expect("no tool call authorization event received")
 741        .unwrap();
 742    match event {
 743        ThreadEvent::ToolCall(tool_call) => return tool_call,
 744        event => {
 745            panic!("Unexpected event {event:?}");
 746        }
 747    }
 748}
 749
 750async fn expect_tool_call_update_fields(
 751    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
 752) -> acp::ToolCallUpdate {
 753    let event = events
 754        .next()
 755        .await
 756        .expect("no tool call authorization event received")
 757        .unwrap();
 758    match event {
 759        ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
 760            return update;
 761        }
 762        event => {
 763            panic!("Unexpected event {event:?}");
 764        }
 765    }
 766}
 767
 768async fn next_tool_call_authorization(
 769    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
 770) -> ToolCallAuthorization {
 771    loop {
 772        let event = events
 773            .next()
 774            .await
 775            .expect("no tool call authorization event received")
 776            .unwrap();
 777        if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event {
 778            let permission_kinds = tool_call_authorization
 779                .options
 780                .iter()
 781                .map(|o| o.kind)
 782                .collect::<Vec<_>>();
 783            assert_eq!(
 784                permission_kinds,
 785                vec![
 786                    acp::PermissionOptionKind::AllowAlways,
 787                    acp::PermissionOptionKind::AllowOnce,
 788                    acp::PermissionOptionKind::RejectOnce,
 789                ]
 790            );
 791            return tool_call_authorization;
 792        }
 793    }
 794}
 795
 796#[gpui::test]
 797#[ignore = "can't run on CI yet"]
 798async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
 799    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 800
 801    // Test concurrent tool calls with different delay times
 802    let events = thread
 803        .update(cx, |thread, cx| {
 804            thread.add_tool(DelayTool);
 805            thread.send(
 806                UserMessageId::new(),
 807                [
 808                    "Call the delay tool twice in the same message.",
 809                    "Once with 100ms. Once with 300ms.",
 810                    "When both timers are complete, describe the outputs.",
 811                ],
 812                cx,
 813            )
 814        })
 815        .unwrap()
 816        .collect()
 817        .await;
 818
 819    let stop_reasons = stop_events(events);
 820    assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]);
 821
 822    thread.update(cx, |thread, _cx| {
 823        let last_message = thread.last_message().unwrap();
 824        let agent_message = last_message.as_agent_message().unwrap();
 825        let text = agent_message
 826            .content
 827            .iter()
 828            .filter_map(|content| {
 829                if let AgentMessageContent::Text(text) = content {
 830                    Some(text.as_str())
 831                } else {
 832                    None
 833                }
 834            })
 835            .collect::<String>();
 836
 837        assert!(text.contains("Ding"));
 838    });
 839}
 840
 841#[gpui::test]
 842async fn test_profiles(cx: &mut TestAppContext) {
 843    let ThreadTest {
 844        model, thread, fs, ..
 845    } = setup(cx, TestModel::Fake).await;
 846    let fake_model = model.as_fake();
 847
 848    thread.update(cx, |thread, _cx| {
 849        thread.add_tool(DelayTool);
 850        thread.add_tool(EchoTool);
 851        thread.add_tool(InfiniteTool);
 852    });
 853
 854    // Override profiles and wait for settings to be loaded.
 855    fs.insert_file(
 856        paths::settings_file(),
 857        json!({
 858            "agent": {
 859                "profiles": {
 860                    "test-1": {
 861                        "name": "Test Profile 1",
 862                        "tools": {
 863                            EchoTool.name(): true,
 864                            DelayTool.name(): true,
 865                        }
 866                    },
 867                    "test-2": {
 868                        "name": "Test Profile 2",
 869                        "tools": {
 870                            InfiniteTool.name(): true,
 871                        }
 872                    }
 873                }
 874            }
 875        })
 876        .to_string()
 877        .into_bytes(),
 878    )
 879    .await;
 880    cx.run_until_parked();
 881
 882    // Test that test-1 profile (default) has echo and delay tools
 883    thread
 884        .update(cx, |thread, cx| {
 885            thread.set_profile(AgentProfileId("test-1".into()));
 886            thread.send(UserMessageId::new(), ["test"], cx)
 887        })
 888        .unwrap();
 889    cx.run_until_parked();
 890
 891    let mut pending_completions = fake_model.pending_completions();
 892    assert_eq!(pending_completions.len(), 1);
 893    let completion = pending_completions.pop().unwrap();
 894    let tool_names: Vec<String> = completion
 895        .tools
 896        .iter()
 897        .map(|tool| tool.name.clone())
 898        .collect();
 899    assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
 900    fake_model.end_last_completion_stream();
 901
 902    // Switch to test-2 profile, and verify that it has only the infinite tool.
 903    thread
 904        .update(cx, |thread, cx| {
 905            thread.set_profile(AgentProfileId("test-2".into()));
 906            thread.send(UserMessageId::new(), ["test2"], cx)
 907        })
 908        .unwrap();
 909    cx.run_until_parked();
 910    let mut pending_completions = fake_model.pending_completions();
 911    assert_eq!(pending_completions.len(), 1);
 912    let completion = pending_completions.pop().unwrap();
 913    let tool_names: Vec<String> = completion
 914        .tools
 915        .iter()
 916        .map(|tool| tool.name.clone())
 917        .collect();
 918    assert_eq!(tool_names, vec![InfiniteTool.name()]);
 919}
 920
 921#[gpui::test]
 922#[ignore = "can't run on CI yet"]
 923async fn test_cancellation(cx: &mut TestAppContext) {
 924    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 925
 926    let mut events = thread
 927        .update(cx, |thread, cx| {
 928            thread.add_tool(InfiniteTool);
 929            thread.add_tool(EchoTool);
 930            thread.send(
 931                UserMessageId::new(),
 932                ["Call the echo tool, then call the infinite tool, then explain their output"],
 933                cx,
 934            )
 935        })
 936        .unwrap();
 937
 938    // Wait until both tools are called.
 939    let mut expected_tools = vec!["Echo", "Infinite Tool"];
 940    let mut echo_id = None;
 941    let mut echo_completed = false;
 942    while let Some(event) = events.next().await {
 943        match event.unwrap() {
 944            ThreadEvent::ToolCall(tool_call) => {
 945                assert_eq!(tool_call.title, expected_tools.remove(0));
 946                if tool_call.title == "Echo" {
 947                    echo_id = Some(tool_call.id);
 948                }
 949            }
 950            ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
 951                acp::ToolCallUpdate {
 952                    id,
 953                    fields:
 954                        acp::ToolCallUpdateFields {
 955                            status: Some(acp::ToolCallStatus::Completed),
 956                            ..
 957                        },
 958                },
 959            )) if Some(&id) == echo_id.as_ref() => {
 960                echo_completed = true;
 961            }
 962            _ => {}
 963        }
 964
 965        if expected_tools.is_empty() && echo_completed {
 966            break;
 967        }
 968    }
 969
 970    // Cancel the current send and ensure that the event stream is closed, even
 971    // if one of the tools is still running.
 972    thread.update(cx, |thread, cx| thread.cancel(cx));
 973    let events = events.collect::<Vec<_>>().await;
 974    let last_event = events.last();
 975    assert!(
 976        matches!(
 977            last_event,
 978            Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled)))
 979        ),
 980        "unexpected event {last_event:?}"
 981    );
 982
 983    // Ensure we can still send a new message after cancellation.
 984    let events = thread
 985        .update(cx, |thread, cx| {
 986            thread.send(
 987                UserMessageId::new(),
 988                ["Testing: reply with 'Hello' then stop."],
 989                cx,
 990            )
 991        })
 992        .unwrap()
 993        .collect::<Vec<_>>()
 994        .await;
 995    thread.update(cx, |thread, _cx| {
 996        let message = thread.last_message().unwrap();
 997        let agent_message = message.as_agent_message().unwrap();
 998        assert_eq!(
 999            agent_message.content,
1000            vec![AgentMessageContent::Text("Hello".to_string())]
1001        );
1002    });
1003    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
1004}
1005
1006#[gpui::test]
1007async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
1008    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1009    let fake_model = model.as_fake();
1010
1011    let events_1 = thread
1012        .update(cx, |thread, cx| {
1013            thread.send(UserMessageId::new(), ["Hello 1"], cx)
1014        })
1015        .unwrap();
1016    cx.run_until_parked();
1017    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
1018    cx.run_until_parked();
1019
1020    let events_2 = thread
1021        .update(cx, |thread, cx| {
1022            thread.send(UserMessageId::new(), ["Hello 2"], cx)
1023        })
1024        .unwrap();
1025    cx.run_until_parked();
1026    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
1027    fake_model
1028        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
1029    fake_model.end_last_completion_stream();
1030
1031    let events_1 = events_1.collect::<Vec<_>>().await;
1032    assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]);
1033    let events_2 = events_2.collect::<Vec<_>>().await;
1034    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
1035}
1036
1037#[gpui::test]
1038async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
1039    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1040    let fake_model = model.as_fake();
1041
1042    let events_1 = thread
1043        .update(cx, |thread, cx| {
1044            thread.send(UserMessageId::new(), ["Hello 1"], cx)
1045        })
1046        .unwrap();
1047    cx.run_until_parked();
1048    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
1049    fake_model
1050        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
1051    fake_model.end_last_completion_stream();
1052    let events_1 = events_1.collect::<Vec<_>>().await;
1053
1054    let events_2 = thread
1055        .update(cx, |thread, cx| {
1056            thread.send(UserMessageId::new(), ["Hello 2"], cx)
1057        })
1058        .unwrap();
1059    cx.run_until_parked();
1060    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
1061    fake_model
1062        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
1063    fake_model.end_last_completion_stream();
1064    let events_2 = events_2.collect::<Vec<_>>().await;
1065
1066    assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]);
1067    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
1068}
1069
1070#[gpui::test]
1071async fn test_refusal(cx: &mut TestAppContext) {
1072    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1073    let fake_model = model.as_fake();
1074
1075    let events = thread
1076        .update(cx, |thread, cx| {
1077            thread.send(UserMessageId::new(), ["Hello"], cx)
1078        })
1079        .unwrap();
1080    cx.run_until_parked();
1081    thread.read_with(cx, |thread, _| {
1082        assert_eq!(
1083            thread.to_markdown(),
1084            indoc! {"
1085                ## User
1086
1087                Hello
1088            "}
1089        );
1090    });
1091
1092    fake_model.send_last_completion_stream_text_chunk("Hey!");
1093    cx.run_until_parked();
1094    thread.read_with(cx, |thread, _| {
1095        assert_eq!(
1096            thread.to_markdown(),
1097            indoc! {"
1098                ## User
1099
1100                Hello
1101
1102                ## Assistant
1103
1104                Hey!
1105            "}
1106        );
1107    });
1108
1109    // If the model refuses to continue, the thread should remove all the messages after the last user message.
1110    fake_model
1111        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::Refusal));
1112    let events = events.collect::<Vec<_>>().await;
1113    assert_eq!(stop_events(events), vec![acp::StopReason::Refusal]);
1114    thread.read_with(cx, |thread, _| {
1115        assert_eq!(thread.to_markdown(), "");
1116    });
1117}
1118
1119#[gpui::test]
1120async fn test_truncate(cx: &mut TestAppContext) {
1121    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1122    let fake_model = model.as_fake();
1123
1124    let message_id = UserMessageId::new();
1125    thread
1126        .update(cx, |thread, cx| {
1127            thread.send(message_id.clone(), ["Hello"], cx)
1128        })
1129        .unwrap();
1130    cx.run_until_parked();
1131    thread.read_with(cx, |thread, _| {
1132        assert_eq!(
1133            thread.to_markdown(),
1134            indoc! {"
1135                ## User
1136
1137                Hello
1138            "}
1139        );
1140    });
1141
1142    fake_model.send_last_completion_stream_text_chunk("Hey!");
1143    cx.run_until_parked();
1144    thread.read_with(cx, |thread, _| {
1145        assert_eq!(
1146            thread.to_markdown(),
1147            indoc! {"
1148                ## User
1149
1150                Hello
1151
1152                ## Assistant
1153
1154                Hey!
1155            "}
1156        );
1157    });
1158
1159    thread
1160        .update(cx, |thread, cx| thread.truncate(message_id, cx))
1161        .unwrap();
1162    cx.run_until_parked();
1163    thread.read_with(cx, |thread, _| {
1164        assert_eq!(thread.to_markdown(), "");
1165    });
1166
1167    // Ensure we can still send a new message after truncation.
1168    thread
1169        .update(cx, |thread, cx| {
1170            thread.send(UserMessageId::new(), ["Hi"], cx)
1171        })
1172        .unwrap();
1173    thread.update(cx, |thread, _cx| {
1174        assert_eq!(
1175            thread.to_markdown(),
1176            indoc! {"
1177                ## User
1178
1179                Hi
1180            "}
1181        );
1182    });
1183    cx.run_until_parked();
1184    fake_model.send_last_completion_stream_text_chunk("Ahoy!");
1185    cx.run_until_parked();
1186    thread.read_with(cx, |thread, _| {
1187        assert_eq!(
1188            thread.to_markdown(),
1189            indoc! {"
1190                ## User
1191
1192                Hi
1193
1194                ## Assistant
1195
1196                Ahoy!
1197            "}
1198        );
1199    });
1200}
1201
1202#[gpui::test]
1203async fn test_agent_connection(cx: &mut TestAppContext) {
1204    cx.update(settings::init);
1205    let templates = Templates::new();
1206
1207    // Initialize language model system with test provider
1208    cx.update(|cx| {
1209        gpui_tokio::init(cx);
1210        client::init_settings(cx);
1211
1212        let http_client = FakeHttpClient::with_404_response();
1213        let clock = Arc::new(clock::FakeSystemClock::new());
1214        let client = Client::new(clock, http_client, cx);
1215        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1216        language_model::init(client.clone(), cx);
1217        language_models::init(user_store.clone(), client.clone(), cx);
1218        Project::init_settings(cx);
1219        LanguageModelRegistry::test(cx);
1220        agent_settings::init(cx);
1221    });
1222    cx.executor().forbid_parking();
1223
1224    // Create a project for new_thread
1225    let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone()));
1226    fake_fs.insert_tree(path!("/test"), json!({})).await;
1227    let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await;
1228    let cwd = Path::new("/test");
1229
1230    // Create agent and connection
1231    let agent = NativeAgent::new(
1232        project.clone(),
1233        templates.clone(),
1234        None,
1235        fake_fs.clone(),
1236        &mut cx.to_async(),
1237    )
1238    .await
1239    .unwrap();
1240    let connection = NativeAgentConnection(agent.clone());
1241
1242    // Test model_selector returns Some
1243    let selector_opt = connection.model_selector();
1244    assert!(
1245        selector_opt.is_some(),
1246        "agent2 should always support ModelSelector"
1247    );
1248    let selector = selector_opt.unwrap();
1249
1250    // Test list_models
1251    let listed_models = cx
1252        .update(|cx| selector.list_models(cx))
1253        .await
1254        .expect("list_models should succeed");
1255    let AgentModelList::Grouped(listed_models) = listed_models else {
1256        panic!("Unexpected model list type");
1257    };
1258    assert!(!listed_models.is_empty(), "should have at least one model");
1259    assert_eq!(
1260        listed_models[&AgentModelGroupName("Fake".into())][0].id.0,
1261        "fake/fake"
1262    );
1263
1264    // Create a thread using new_thread
1265    let connection_rc = Rc::new(connection.clone());
1266    let acp_thread = cx
1267        .update(|cx| connection_rc.new_thread(project, cwd, cx))
1268        .await
1269        .expect("new_thread should succeed");
1270
1271    // Get the session_id from the AcpThread
1272    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
1273
1274    // Test selected_model returns the default
1275    let model = cx
1276        .update(|cx| selector.selected_model(&session_id, cx))
1277        .await
1278        .expect("selected_model should succeed");
1279    let model = cx
1280        .update(|cx| agent.read(cx).models().model_from_id(&model.id))
1281        .unwrap();
1282    let model = model.as_fake();
1283    assert_eq!(model.id().0, "fake", "should return default model");
1284
1285    let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx));
1286    cx.run_until_parked();
1287    model.send_last_completion_stream_text_chunk("def");
1288    cx.run_until_parked();
1289    acp_thread.read_with(cx, |thread, cx| {
1290        assert_eq!(
1291            thread.to_markdown(cx),
1292            indoc! {"
1293                ## User
1294
1295                abc
1296
1297                ## Assistant
1298
1299                def
1300
1301            "}
1302        )
1303    });
1304
1305    // Test cancel
1306    cx.update(|cx| connection.cancel(&session_id, cx));
1307    request.await.expect("prompt should fail gracefully");
1308
1309    // Ensure that dropping the ACP thread causes the native thread to be
1310    // dropped as well.
1311    cx.update(|_| drop(acp_thread));
1312    let result = cx
1313        .update(|cx| {
1314            connection.prompt(
1315                Some(acp_thread::UserMessageId::new()),
1316                acp::PromptRequest {
1317                    session_id: session_id.clone(),
1318                    prompt: vec!["ghi".into()],
1319                },
1320                cx,
1321            )
1322        })
1323        .await;
1324    assert_eq!(
1325        result.as_ref().unwrap_err().to_string(),
1326        "Session not found",
1327        "unexpected result: {:?}",
1328        result
1329    );
1330}
1331
1332#[gpui::test]
1333async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
1334    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
1335    thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool));
1336    let fake_model = model.as_fake();
1337
1338    let mut events = thread
1339        .update(cx, |thread, cx| {
1340            thread.send(UserMessageId::new(), ["Think"], cx)
1341        })
1342        .unwrap();
1343    cx.run_until_parked();
1344
1345    // Simulate streaming partial input.
1346    let input = json!({});
1347    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1348        LanguageModelToolUse {
1349            id: "1".into(),
1350            name: ThinkingTool.name().into(),
1351            raw_input: input.to_string(),
1352            input,
1353            is_input_complete: false,
1354        },
1355    ));
1356
1357    // Input streaming completed
1358    let input = json!({ "content": "Thinking hard!" });
1359    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1360        LanguageModelToolUse {
1361            id: "1".into(),
1362            name: "thinking".into(),
1363            raw_input: input.to_string(),
1364            input,
1365            is_input_complete: true,
1366        },
1367    ));
1368    fake_model.end_last_completion_stream();
1369    cx.run_until_parked();
1370
1371    let tool_call = expect_tool_call(&mut events).await;
1372    assert_eq!(
1373        tool_call,
1374        acp::ToolCall {
1375            id: acp::ToolCallId("1".into()),
1376            title: "Thinking".into(),
1377            kind: acp::ToolKind::Think,
1378            status: acp::ToolCallStatus::Pending,
1379            content: vec![],
1380            locations: vec![],
1381            raw_input: Some(json!({})),
1382            raw_output: None,
1383        }
1384    );
1385    let update = expect_tool_call_update_fields(&mut events).await;
1386    assert_eq!(
1387        update,
1388        acp::ToolCallUpdate {
1389            id: acp::ToolCallId("1".into()),
1390            fields: acp::ToolCallUpdateFields {
1391                title: Some("Thinking".into()),
1392                kind: Some(acp::ToolKind::Think),
1393                raw_input: Some(json!({ "content": "Thinking hard!" })),
1394                ..Default::default()
1395            },
1396        }
1397    );
1398    let update = expect_tool_call_update_fields(&mut events).await;
1399    assert_eq!(
1400        update,
1401        acp::ToolCallUpdate {
1402            id: acp::ToolCallId("1".into()),
1403            fields: acp::ToolCallUpdateFields {
1404                status: Some(acp::ToolCallStatus::InProgress),
1405                ..Default::default()
1406            },
1407        }
1408    );
1409    let update = expect_tool_call_update_fields(&mut events).await;
1410    assert_eq!(
1411        update,
1412        acp::ToolCallUpdate {
1413            id: acp::ToolCallId("1".into()),
1414            fields: acp::ToolCallUpdateFields {
1415                content: Some(vec!["Thinking hard!".into()]),
1416                ..Default::default()
1417            },
1418        }
1419    );
1420    let update = expect_tool_call_update_fields(&mut events).await;
1421    assert_eq!(
1422        update,
1423        acp::ToolCallUpdate {
1424            id: acp::ToolCallId("1".into()),
1425            fields: acp::ToolCallUpdateFields {
1426                status: Some(acp::ToolCallStatus::Completed),
1427                raw_output: Some("Finished thinking.".into()),
1428                ..Default::default()
1429            },
1430        }
1431    );
1432}
1433
1434/// Filters out the stop events for asserting against in tests
1435fn stop_events(result_events: Vec<Result<ThreadEvent>>) -> Vec<acp::StopReason> {
1436    result_events
1437        .into_iter()
1438        .filter_map(|event| match event.unwrap() {
1439            ThreadEvent::Stop(stop_reason) => Some(stop_reason),
1440            _ => None,
1441        })
1442        .collect()
1443}
1444
1445struct ThreadTest {
1446    model: Arc<dyn LanguageModel>,
1447    thread: Entity<Thread>,
1448    project_context: Rc<RefCell<ProjectContext>>,
1449    fs: Arc<FakeFs>,
1450}
1451
1452enum TestModel {
1453    Sonnet4,
1454    Sonnet4Thinking,
1455    Fake,
1456}
1457
1458impl TestModel {
1459    fn id(&self) -> LanguageModelId {
1460        match self {
1461            TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()),
1462            TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()),
1463            TestModel::Fake => unreachable!(),
1464        }
1465    }
1466}
1467
1468async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
1469    cx.executor().allow_parking();
1470
1471    let fs = FakeFs::new(cx.background_executor.clone());
1472    fs.create_dir(paths::settings_file().parent().unwrap())
1473        .await
1474        .unwrap();
1475    fs.insert_file(
1476        paths::settings_file(),
1477        json!({
1478            "agent": {
1479                "default_profile": "test-profile",
1480                "profiles": {
1481                    "test-profile": {
1482                        "name": "Test Profile",
1483                        "tools": {
1484                            EchoTool.name(): true,
1485                            DelayTool.name(): true,
1486                            WordListTool.name(): true,
1487                            ToolRequiringPermission.name(): true,
1488                            InfiniteTool.name(): true,
1489                        }
1490                    }
1491                }
1492            }
1493        })
1494        .to_string()
1495        .into_bytes(),
1496    )
1497    .await;
1498
1499    cx.update(|cx| {
1500        settings::init(cx);
1501        Project::init_settings(cx);
1502        agent_settings::init(cx);
1503        gpui_tokio::init(cx);
1504        let http_client = ReqwestClient::user_agent("agent tests").unwrap();
1505        cx.set_http_client(Arc::new(http_client));
1506
1507        client::init_settings(cx);
1508        let client = Client::production(cx);
1509        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1510        language_model::init(client.clone(), cx);
1511        language_models::init(user_store.clone(), client.clone(), cx);
1512
1513        watch_settings(fs.clone(), cx);
1514    });
1515
1516    let templates = Templates::new();
1517
1518    fs.insert_tree(path!("/test"), json!({})).await;
1519    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1520
1521    let model = cx
1522        .update(|cx| {
1523            if let TestModel::Fake = model {
1524                Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
1525            } else {
1526                let model_id = model.id();
1527                let models = LanguageModelRegistry::read_global(cx);
1528                let model = models
1529                    .available_models(cx)
1530                    .find(|model| model.id() == model_id)
1531                    .unwrap();
1532
1533                let provider = models.provider(&model.provider_id()).unwrap();
1534                let authenticated = provider.authenticate(cx);
1535
1536                cx.spawn(async move |_cx| {
1537                    authenticated.await.unwrap();
1538                    model
1539                })
1540            }
1541        })
1542        .await;
1543
1544    let project_context = Rc::new(RefCell::new(ProjectContext::default()));
1545    let context_server_registry =
1546        cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1547    let action_log = cx.new(|_| ActionLog::new(project.clone()));
1548    let thread = cx.new(|cx| {
1549        Thread::new(
1550            generate_session_id(),
1551            project,
1552            project_context.clone(),
1553            context_server_registry,
1554            action_log,
1555            templates,
1556            Some(model.clone()),
1557            None,
1558            cx,
1559        )
1560    });
1561    ThreadTest {
1562        model,
1563        thread,
1564        project_context,
1565        fs,
1566    }
1567}
1568
1569#[cfg(test)]
1570#[ctor::ctor]
1571fn init_logger() {
1572    if std::env::var("RUST_LOG").is_ok() {
1573        env_logger::init();
1574    }
1575}
1576
1577fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
1578    let fs = fs.clone();
1579    cx.spawn({
1580        async move |cx| {
1581            let mut new_settings_content_rx = settings::watch_config_file(
1582                cx.background_executor(),
1583                fs,
1584                paths::settings_file().clone(),
1585            );
1586
1587            while let Some(new_settings_content) = new_settings_content_rx.next().await {
1588                cx.update(|cx| {
1589                    SettingsStore::update_global(cx, |settings, cx| {
1590                        settings.set_user_settings(&new_settings_content, cx)
1591                    })
1592                })
1593                .ok();
1594            }
1595        }
1596    })
1597    .detach();
1598}