Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:33:08.070Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

mod.rs

8701 lines · 297.9 KB · rust
1use super::*;
2use acp_thread::{
3    AgentConnection, AgentModelGroupName, AgentModelList, ClientUserMessageId, PermissionOptions,
4    ThreadStatus,
5};
6use agent_client_protocol::schema::v1 as acp;
7use agent_settings::AgentProfileId;
8use anyhow::Result;
9use client::{Client, RefreshLlmTokenListener, UserStore};
10use collections::IndexMap;
11use context_server::{ContextServer, ContextServerCommand, ContextServerId};
12use feature_flags::FeatureFlagAppExt as _;
13use fs::{FakeFs, Fs};
14use futures::{
15    FutureExt as _, StreamExt,
16    channel::{
17        mpsc::{self, UnboundedReceiver},
18        oneshot,
19    },
20    future::{Fuse, Shared},
21};
22use gpui::{
23    App, AppContext, AsyncApp, Entity, Task, TestAppContext, UpdateGlobal,
24    http_client::FakeHttpClient,
25};
26use indoc::indoc;
27use language_model::{
28    CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
29    LanguageModelId, LanguageModelImageExt, LanguageModelProviderId, LanguageModelProviderName,
30    LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
31    LanguageModelToolResult, LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent,
32    Role, StopReason, TokenUsage,
33    fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
34};
35use pretty_assertions::assert_eq;
36use project::{
37    Project, context_server_store::ContextServerStore, project_settings::ProjectSettings,
38};
39use prompt_store::ProjectContext;
40use reqwest_client::ReqwestClient;
41use schemars::JsonSchema;
42use serde::{Deserialize, Serialize};
43use serde_json::json;
44use settings::{LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore};
45use std::{
46    path::Path,
47    pin::Pin,
48    rc::Rc,
49    sync::{
50        Arc,
51        atomic::{AtomicBool, AtomicUsize, Ordering},
52    },
53    time::Duration,
54};
55use util::path;
56
57mod test_tools;
58use test_tools::*;
59
60pub(crate) fn init_test(cx: &mut TestAppContext) {
61    cx.update(|cx| {
62        let settings_store = SettingsStore::test(cx);
63        cx.set_global(settings_store);
64    });
65}
66
67pub(crate) struct FakeTerminalHandle {
68    killed: Arc<AtomicBool>,
69    stopped_by_user: Arc<AtomicBool>,
70    exit_sender: std::cell::RefCell<Option<futures::channel::oneshot::Sender<()>>>,
71    wait_for_exit: Shared<Task<acp::TerminalExitStatus>>,
72    output: acp::TerminalOutputResponse,
73    id: acp::TerminalId,
74}
75
76impl FakeTerminalHandle {
77    pub(crate) fn new_never_exits(cx: &mut App) -> Self {
78        let killed = Arc::new(AtomicBool::new(false));
79        let stopped_by_user = Arc::new(AtomicBool::new(false));
80
81        let (exit_sender, exit_receiver) = futures::channel::oneshot::channel();
82
83        let wait_for_exit = cx
84            .spawn(async move |_cx| {
85                // Wait for the exit signal (sent when kill() is called)
86                let _ = exit_receiver.await;
87                acp::TerminalExitStatus::new()
88            })
89            .shared();
90
91        Self {
92            killed,
93            stopped_by_user,
94            exit_sender: std::cell::RefCell::new(Some(exit_sender)),
95            wait_for_exit,
96            output: acp::TerminalOutputResponse::new("partial output".to_string(), false),
97            id: acp::TerminalId::new("fake_terminal".to_string()),
98        }
99    }
100
101    pub(crate) fn new_with_immediate_exit(cx: &mut App, exit_code: u32) -> Self {
102        let killed = Arc::new(AtomicBool::new(false));
103        let stopped_by_user = Arc::new(AtomicBool::new(false));
104        let (exit_sender, _exit_receiver) = futures::channel::oneshot::channel();
105
106        let wait_for_exit = cx
107            .spawn(async move |_cx| acp::TerminalExitStatus::new().exit_code(exit_code))
108            .shared();
109
110        Self {
111            killed,
112            stopped_by_user,
113            exit_sender: std::cell::RefCell::new(Some(exit_sender)),
114            wait_for_exit,
115            output: acp::TerminalOutputResponse::new("command output".to_string(), false),
116            id: acp::TerminalId::new("fake_terminal".to_string()),
117        }
118    }
119
120    pub(crate) fn with_output(mut self, output: acp::TerminalOutputResponse) -> Self {
121        self.output = output;
122        self
123    }
124
125    pub(crate) fn was_killed(&self) -> bool {
126        self.killed.load(Ordering::SeqCst)
127    }
128
129    pub(crate) fn set_stopped_by_user(&self, stopped: bool) {
130        self.stopped_by_user.store(stopped, Ordering::SeqCst);
131    }
132
133    pub(crate) fn signal_exit(&self) {
134        if let Some(sender) = self.exit_sender.borrow_mut().take() {
135            let _ = sender.send(());
136        }
137    }
138}
139
140impl crate::TerminalHandle for FakeTerminalHandle {
141    fn id(&self, _cx: &AsyncApp) -> Result<acp::TerminalId> {
142        Ok(self.id.clone())
143    }
144
145    fn current_output(&self, _cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
146        Ok(self.output.clone())
147    }
148
149    fn wait_for_exit(&self, _cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
150        Ok(self.wait_for_exit.clone())
151    }
152
153    fn kill(&self, _cx: &AsyncApp) -> Result<()> {
154        self.killed.store(true, Ordering::SeqCst);
155        self.signal_exit();
156        Ok(())
157    }
158
159    fn was_stopped_by_user(&self, _cx: &AsyncApp) -> Result<bool> {
160        Ok(self.stopped_by_user.load(Ordering::SeqCst))
161    }
162}
163
164struct FakeSubagentHandle {
165    session_id: acp::SessionId,
166    send_task: Shared<Task<String>>,
167}
168
169impl SubagentHandle for FakeSubagentHandle {
170    fn id(&self) -> acp::SessionId {
171        self.session_id.clone()
172    }
173
174    fn num_entries(&self, _cx: &App) -> usize {
175        unimplemented!()
176    }
177
178    fn send(&self, _message: String, cx: &AsyncApp) -> Task<Result<String>> {
179        let task = self.send_task.clone();
180        cx.background_spawn(async move { Ok(task.await) })
181    }
182}
183
184#[derive(Default)]
185pub(crate) struct FakeThreadEnvironment {
186    terminal_handle: Option<Rc<FakeTerminalHandle>>,
187    subagent_handle: Option<Rc<FakeSubagentHandle>>,
188    terminal_creations: Arc<AtomicUsize>,
189    terminal_output_limits: std::cell::RefCell<Vec<Option<u64>>>,
190}
191
192impl FakeThreadEnvironment {
193    pub(crate) fn with_terminal(self, terminal_handle: FakeTerminalHandle) -> Self {
194        Self {
195            terminal_handle: Some(terminal_handle.into()),
196            ..self
197        }
198    }
199
200    pub(crate) fn terminal_creation_count(&self) -> usize {
201        self.terminal_creations.load(Ordering::SeqCst)
202    }
203
204    pub(crate) fn terminal_output_limits(&self) -> Vec<Option<u64>> {
205        self.terminal_output_limits.borrow().clone()
206    }
207}
208
209impl crate::ThreadEnvironment for FakeThreadEnvironment {
210    fn create_terminal(
211        &self,
212        _command: String,
213        _extra_env: Vec<acp::EnvVariable>,
214        _cwd: Option<std::path::PathBuf>,
215        output_byte_limit: Option<u64>,
216        _sandbox_wrap: Option<acp_thread::SandboxWrap>,
217        _cx: &mut AsyncApp,
218    ) -> Task<Result<Rc<dyn crate::TerminalHandle>>> {
219        self.terminal_creations.fetch_add(1, Ordering::SeqCst);
220        self.terminal_output_limits
221            .borrow_mut()
222            .push(output_byte_limit);
223        let handle = self
224            .terminal_handle
225            .clone()
226            .expect("Terminal handle not available on FakeThreadEnvironment");
227        Task::ready(Ok(handle as Rc<dyn crate::TerminalHandle>))
228    }
229
230    fn create_subagent(&self, _label: String, _cx: &mut App) -> Result<Rc<dyn SubagentHandle>> {
231        Ok(self
232            .subagent_handle
233            .clone()
234            .expect("Subagent handle not available on FakeThreadEnvironment")
235            as Rc<dyn SubagentHandle>)
236    }
237}
238
239/// Environment that creates multiple independent terminal handles for testing concurrent terminals.
240struct MultiTerminalEnvironment {
241    handles: std::cell::RefCell<Vec<Rc<FakeTerminalHandle>>>,
242}
243
244impl MultiTerminalEnvironment {
245    fn new() -> Self {
246        Self {
247            handles: std::cell::RefCell::new(Vec::new()),
248        }
249    }
250
251    fn handles(&self) -> Vec<Rc<FakeTerminalHandle>> {
252        self.handles.borrow().clone()
253    }
254}
255
256impl crate::ThreadEnvironment for MultiTerminalEnvironment {
257    fn create_terminal(
258        &self,
259        _command: String,
260        _extra_env: Vec<acp::EnvVariable>,
261        _cwd: Option<std::path::PathBuf>,
262        _output_byte_limit: Option<u64>,
263        _sandbox_wrap: Option<acp_thread::SandboxWrap>,
264        cx: &mut AsyncApp,
265    ) -> Task<Result<Rc<dyn crate::TerminalHandle>>> {
266        let handle = Rc::new(cx.update(|cx| FakeTerminalHandle::new_never_exits(cx)));
267        self.handles.borrow_mut().push(handle.clone());
268        Task::ready(Ok(handle as Rc<dyn crate::TerminalHandle>))
269    }
270
271    fn create_subagent(&self, _label: String, _cx: &mut App) -> Result<Rc<dyn SubagentHandle>> {
272        unimplemented!()
273    }
274}
275
276fn always_allow_tools(cx: &mut TestAppContext) {
277    cx.update(|cx| {
278        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
279        settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
280        agent_settings::AgentSettings::override_global(settings, cx);
281    });
282}
283
284/// Turns terminal sandboxing off so the non-sandboxed `TerminalTool` is the
285/// variant exposed to the model as `terminal`. Tests that register
286/// `TerminalTool` directly need this because sandboxing is enabled by default
287/// for staff (and in debug builds), in which case `Thread::enabled_tools`
288/// would otherwise expose `SandboxedTerminalTool` under that name instead.
289fn disable_sandboxing(cx: &mut TestAppContext) {
290    cx.update(|cx| {
291        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
292        settings.sandbox_permissions.allow_unsandboxed = true;
293        agent_settings::AgentSettings::override_global(settings, cx);
294    });
295}
296
297#[gpui::test]
298async fn test_echo(cx: &mut TestAppContext) {
299    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
300    let fake_model = model.as_fake();
301
302    let events = thread
303        .update(cx, |thread, cx| {
304            thread.send(
305                ClientUserMessageId::new(),
306                ["Testing: Reply with 'Hello'"],
307                cx,
308            )
309        })
310        .unwrap();
311    cx.run_until_parked();
312    fake_model.send_last_completion_stream_text_chunk("Hello");
313    fake_model
314        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
315    fake_model.end_last_completion_stream();
316
317    let events = events.collect().await;
318    thread.update(cx, |thread, _cx| {
319        assert_eq!(
320            thread.last_received_or_pending_message().unwrap().role(),
321            Role::Assistant
322        );
323        assert_eq!(
324            thread
325                .last_received_or_pending_message()
326                .unwrap()
327                .to_markdown(),
328            "Hello\n"
329        )
330    });
331    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
332}
333
334#[gpui::test]
335async fn test_terminal_tool_timeout_kills_handle(cx: &mut TestAppContext) {
336    init_test(cx);
337    always_allow_tools(cx);
338
339    let fs = FakeFs::new(cx.executor());
340    let project = Project::test(fs, [], cx).await;
341
342    let environment = Rc::new(cx.update(|cx| {
343        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
344    }));
345    let handle = environment.terminal_handle.clone().unwrap();
346
347    #[allow(clippy::arc_with_non_send_sync)]
348    let tool = Arc::new(crate::TerminalTool::new(project, environment));
349    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
350
351    let task = cx.update(|cx| {
352        tool.run(
353            ToolInput::resolved(crate::TerminalToolInput {
354                command: "sleep 1000".to_string(),
355                cd: ".".to_string(),
356                timeout_ms: Some(5),
357                ..Default::default()
358            }),
359            event_stream,
360            cx,
361        )
362    });
363
364    let update = rx.expect_update_fields().await;
365    assert!(
366        update.content.iter().any(|blocks| {
367            blocks
368                .iter()
369                .any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
370        }),
371        "expected tool call update to include terminal content"
372    );
373
374    let mut task_future: Pin<Box<Fuse<Task<Result<String, String>>>>> = Box::pin(task.fuse());
375
376    let deadline = std::time::Instant::now() + Duration::from_millis(500);
377    loop {
378        if let Some(result) = task_future.as_mut().now_or_never() {
379            let result = result.expect("terminal tool task should complete");
380
381            assert!(
382                handle.was_killed(),
383                "expected terminal handle to be killed on timeout"
384            );
385            assert!(
386                result.contains("partial output"),
387                "expected result to include terminal output, got: {result}"
388            );
389            return;
390        }
391
392        if std::time::Instant::now() >= deadline {
393            panic!("timed out waiting for terminal tool task to complete");
394        }
395
396        cx.run_until_parked();
397        cx.background_executor.timer(Duration::from_millis(1)).await;
398    }
399}
400
401#[gpui::test]
402#[ignore]
403async fn test_terminal_tool_without_timeout_does_not_kill_handle(cx: &mut TestAppContext) {
404    init_test(cx);
405    always_allow_tools(cx);
406
407    let fs = FakeFs::new(cx.executor());
408    let project = Project::test(fs, [], cx).await;
409
410    let environment = Rc::new(cx.update(|cx| {
411        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
412    }));
413    let handle = environment.terminal_handle.clone().unwrap();
414
415    #[allow(clippy::arc_with_non_send_sync)]
416    let tool = Arc::new(crate::TerminalTool::new(project, environment));
417    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
418
419    let _task = cx.update(|cx| {
420        tool.run(
421            ToolInput::resolved(crate::TerminalToolInput {
422                command: "sleep 1000".to_string(),
423                cd: ".".to_string(),
424                timeout_ms: None,
425                ..Default::default()
426            }),
427            event_stream,
428            cx,
429        )
430    });
431
432    let update = rx.expect_update_fields().await;
433    assert!(
434        update.content.iter().any(|blocks| {
435            blocks
436                .iter()
437                .any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
438        }),
439        "expected tool call update to include terminal content"
440    );
441
442    cx.background_executor
443        .timer(Duration::from_millis(25))
444        .await;
445
446    assert!(
447        !handle.was_killed(),
448        "did not expect terminal handle to be killed without a timeout"
449    );
450}
451
452#[gpui::test]
453async fn test_thinking(cx: &mut TestAppContext) {
454    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
455    let fake_model = model.as_fake();
456
457    let events = thread
458        .update(cx, |thread, cx| {
459            thread.send(
460                ClientUserMessageId::new(),
461                [indoc! {"
462                    Testing:
463
464                    Generate a thinking step where you just think the word 'Think',
465                    and have your final answer be 'Hello'
466                "}],
467                cx,
468            )
469        })
470        .unwrap();
471    cx.run_until_parked();
472    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking {
473        text: "Think".to_string(),
474        signature: None,
475    });
476    fake_model.send_last_completion_stream_text_chunk("Hello");
477    fake_model
478        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
479    fake_model.end_last_completion_stream();
480
481    let events = events.collect().await;
482    thread.update(cx, |thread, _cx| {
483        assert_eq!(
484            thread.last_received_or_pending_message().unwrap().role(),
485            Role::Assistant
486        );
487        assert_eq!(
488            thread
489                .last_received_or_pending_message()
490                .unwrap()
491                .to_markdown(),
492            indoc! {"
493                <think>Think</think>
494                Hello
495            "}
496        )
497    });
498    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
499}
500
501#[gpui::test]
502async fn test_thinking_allowed_when_model_cannot_disable_thinking(cx: &mut TestAppContext) {
503    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
504    let fake_model = model.as_fake();
505    fake_model.set_supports_thinking(true);
506
507    // With thinking toggled off, a model that can disable thinking honors
508    // the toggle...
509    thread.update(cx, |thread, cx| {
510        thread.set_thinking_enabled(false, cx);
511        let request = thread
512            .build_completion_request(CompletionIntent::UserPrompt, cx)
513            .unwrap();
514        assert!(!request.thinking_allowed);
515    });
516
517    // ...but a model that always thinks ignores the stale toggle state.
518    fake_model.set_supports_disabling_thinking(false);
519    thread.update(cx, |thread, cx| {
520        let request = thread
521            .build_completion_request(CompletionIntent::UserPrompt, cx)
522            .unwrap();
523        assert!(request.thinking_allowed);
524    });
525}
526
527#[gpui::test]
528async fn test_system_prompt(cx: &mut TestAppContext) {
529    let ThreadTest {
530        model,
531        thread,
532        project_context,
533        ..
534    } = setup(cx, TestModel::Fake).await;
535    let fake_model = model.as_fake();
536
537    project_context.update(cx, |project_context, _cx| {
538        project_context.shell = "test-shell".into()
539    });
540    thread.update(cx, |thread, _| thread.add_tool(EchoTool));
541    thread
542        .update(cx, |thread, cx| {
543            thread.send(ClientUserMessageId::new(), ["abc"], cx)
544        })
545        .unwrap();
546    cx.run_until_parked();
547    let mut pending_completions = fake_model.pending_completions();
548    assert_eq!(
549        pending_completions.len(),
550        1,
551        "unexpected pending completions: {:?}",
552        pending_completions
553    );
554
555    let pending_completion = pending_completions.pop().unwrap();
556    assert_eq!(pending_completion.messages[0].role, Role::System);
557
558    let system_message = &pending_completion.messages[0];
559    let MessageContent::Text(system_prompt) = &system_message.content[0] else {
560        panic!("Expected text content");
561    };
562    assert!(
563        system_prompt.contains("test-shell"),
564        "unexpected system message: {:?}",
565        system_message
566    );
567    assert!(
568        system_prompt.contains("## Fixing Diagnostics"),
569        "unexpected system message: {:?}",
570        system_message
571    );
572}
573
574#[gpui::test]
575async fn test_system_prompt_without_tools(cx: &mut TestAppContext) {
576    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
577    let fake_model = model.as_fake();
578
579    thread
580        .update(cx, |thread, cx| {
581            thread.send(ClientUserMessageId::new(), ["abc"], cx)
582        })
583        .unwrap();
584    cx.run_until_parked();
585    let mut pending_completions = fake_model.pending_completions();
586    assert_eq!(
587        pending_completions.len(),
588        1,
589        "unexpected pending completions: {:?}",
590        pending_completions
591    );
592
593    let pending_completion = pending_completions.pop().unwrap();
594    assert_eq!(pending_completion.messages[0].role, Role::System);
595
596    let system_message = &pending_completion.messages[0];
597    let MessageContent::Text(system_prompt) = &system_message.content[0] else {
598        panic!("Expected text content");
599    };
600    assert!(
601        !system_prompt.contains("## Tool Use"),
602        "unexpected system message: {:?}",
603        system_message
604    );
605    assert!(
606        !system_prompt.contains("## Fixing Diagnostics"),
607        "unexpected system message: {:?}",
608        system_message
609    );
610}
611
612#[gpui::test]
613async fn test_prompt_caching(cx: &mut TestAppContext) {
614    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
615    let fake_model = model.as_fake();
616
617    // Send initial user message and verify it's cached
618    thread
619        .update(cx, |thread, cx| {
620            thread.send(ClientUserMessageId::new(), ["Message 1"], cx)
621        })
622        .unwrap();
623    cx.run_until_parked();
624
625    let completion = fake_model.pending_completions().pop().unwrap();
626    assert_eq!(
627        completion.messages[1..],
628        vec![LanguageModelRequestMessage {
629            role: Role::User,
630            content: vec!["Message 1".into()],
631            cache: true,
632            reasoning_details: None,
633        }]
634    );
635    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
636        "Response to Message 1".into(),
637    ));
638    fake_model.end_last_completion_stream();
639    cx.run_until_parked();
640
641    // Send another user message and verify only the latest is cached
642    thread
643        .update(cx, |thread, cx| {
644            thread.send(ClientUserMessageId::new(), ["Message 2"], cx)
645        })
646        .unwrap();
647    cx.run_until_parked();
648
649    let completion = fake_model.pending_completions().pop().unwrap();
650    assert_eq!(
651        completion.messages[1..],
652        vec![
653            LanguageModelRequestMessage {
654                role: Role::User,
655                content: vec!["Message 1".into()],
656                cache: false,
657                reasoning_details: None,
658            },
659            LanguageModelRequestMessage {
660                role: Role::Assistant,
661                content: vec!["Response to Message 1".into()],
662                cache: false,
663                reasoning_details: None,
664            },
665            LanguageModelRequestMessage {
666                role: Role::User,
667                content: vec!["Message 2".into()],
668                cache: true,
669                reasoning_details: None,
670            }
671        ]
672    );
673    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
674        "Response to Message 2".into(),
675    ));
676    fake_model.end_last_completion_stream();
677    cx.run_until_parked();
678
679    // Simulate a tool call and verify that the latest tool result is cached
680    thread.update(cx, |thread, _| thread.add_tool(EchoTool));
681    thread
682        .update(cx, |thread, cx| {
683            thread.send(ClientUserMessageId::new(), ["Use the echo tool"], cx)
684        })
685        .unwrap();
686    cx.run_until_parked();
687
688    let tool_use = LanguageModelToolUse {
689        id: "tool_1".into(),
690        name: EchoTool::NAME.into(),
691        raw_input: json!({"text": "test"}).to_string(),
692        input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
693        is_input_complete: true,
694        thought_signature: None,
695    };
696    fake_model
697        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
698    fake_model.end_last_completion_stream();
699    cx.run_until_parked();
700
701    let completion = fake_model.pending_completions().pop().unwrap();
702    let tool_result = LanguageModelToolResult {
703        tool_use_id: "tool_1".into(),
704        tool_name: EchoTool::NAME.into(),
705        is_error: false,
706        content: vec!["test".into()],
707        output: Some("test".into()),
708    };
709    assert_eq!(
710        completion.messages[1..],
711        vec![
712            LanguageModelRequestMessage {
713                role: Role::User,
714                content: vec!["Message 1".into()],
715                cache: false,
716                reasoning_details: None,
717            },
718            LanguageModelRequestMessage {
719                role: Role::Assistant,
720                content: vec!["Response to Message 1".into()],
721                cache: false,
722                reasoning_details: None,
723            },
724            LanguageModelRequestMessage {
725                role: Role::User,
726                content: vec!["Message 2".into()],
727                cache: false,
728                reasoning_details: None,
729            },
730            LanguageModelRequestMessage {
731                role: Role::Assistant,
732                content: vec!["Response to Message 2".into()],
733                cache: false,
734                reasoning_details: None,
735            },
736            LanguageModelRequestMessage {
737                role: Role::User,
738                content: vec!["Use the echo tool".into()],
739                cache: false,
740                reasoning_details: None,
741            },
742            LanguageModelRequestMessage {
743                role: Role::Assistant,
744                content: vec![MessageContent::ToolUse(tool_use)],
745                cache: false,
746                reasoning_details: None,
747            },
748            LanguageModelRequestMessage {
749                role: Role::User,
750                content: vec![MessageContent::ToolResult(tool_result)],
751                cache: true,
752                reasoning_details: None,
753            }
754        ]
755    );
756}
757
758#[gpui::test]
759#[cfg_attr(not(feature = "e2e"), ignore)]
760async fn test_basic_tool_calls(cx: &mut TestAppContext) {
761    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
762
763    // Test a tool call that's likely to complete *before* streaming stops.
764    let events = thread
765        .update(cx, |thread, cx| {
766            thread.add_tool(EchoTool);
767            thread.send(
768                ClientUserMessageId::new(),
769                ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."],
770                cx,
771            )
772        })
773        .unwrap()
774        .collect()
775        .await;
776    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
777
778    // Test a tool calls that's likely to complete *after* streaming stops.
779    let events = thread
780        .update(cx, |thread, cx| {
781            thread.remove_tool(&EchoTool::NAME);
782            thread.add_tool(DelayTool);
783            thread.send(
784                ClientUserMessageId::new(),
785                [
786                    "Now call the delay tool with 200ms.",
787                    "When the timer goes off, then you echo the output of the tool.",
788                ],
789                cx,
790            )
791        })
792        .unwrap()
793        .collect()
794        .await;
795    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
796    thread.update(cx, |thread, _cx| {
797        assert!(
798            thread
799                .last_received_or_pending_message()
800                .unwrap()
801                .as_agent_message()
802                .unwrap()
803                .content
804                .iter()
805                .any(|content| {
806                    if let AgentMessageContent::Text(text) = content {
807                        text.contains("Ding")
808                    } else {
809                        false
810                    }
811                }),
812            "{}",
813            thread.to_markdown()
814        );
815    });
816}
817
818#[gpui::test]
819#[cfg_attr(not(feature = "e2e"), ignore)]
820async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
821    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
822
823    // Test a tool call that's likely to complete *before* streaming stops.
824    let mut events = thread
825        .update(cx, |thread, cx| {
826            thread.add_tool(WordListTool);
827            thread.send(ClientUserMessageId::new(), ["Test the word_list tool."], cx)
828        })
829        .unwrap();
830
831    let mut saw_partial_tool_use = false;
832    while let Some(event) = events.next().await {
833        if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
834            thread.update(cx, |thread, _cx| {
835                // Look for a tool use in the thread's last message
836                let message = thread.last_received_or_pending_message().unwrap();
837                let agent_message = message.as_agent_message().unwrap();
838                let last_content = agent_message.content.last().unwrap();
839                if let AgentMessageContent::ToolUse(last_tool_use) = last_content {
840                    assert_eq!(last_tool_use.name.as_ref(), "word_list");
841                    if tool_call.status == acp::ToolCallStatus::Pending {
842                        if !last_tool_use.is_input_complete
843                            && last_tool_use
844                                .input
845                                .as_json()
846                                .and_then(|input| input.get("g"))
847                                .is_none()
848                        {
849                            saw_partial_tool_use = true;
850                        }
851                    } else {
852                        last_tool_use
853                            .input
854                            .as_json()
855                            .expect("tool input should be JSON")
856                            .get("a")
857                            .expect("'a' has streamed because input is now complete");
858                        last_tool_use
859                            .input
860                            .as_json()
861                            .expect("tool input should be JSON")
862                            .get("g")
863                            .expect("'g' has streamed because input is now complete");
864                    }
865                } else {
866                    panic!("last content should be a tool use");
867                }
868            });
869        }
870    }
871
872    assert!(
873        saw_partial_tool_use,
874        "should see at least one partially streamed tool use in the history"
875    );
876}
877
878#[gpui::test]
879async fn test_tool_authorization(cx: &mut TestAppContext) {
880    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
881    let fake_model = model.as_fake();
882
883    let mut events = thread
884        .update(cx, |thread, cx| {
885            thread.add_tool(ToolRequiringPermission);
886            thread.send(ClientUserMessageId::new(), ["abc"], cx)
887        })
888        .unwrap();
889    cx.run_until_parked();
890    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
891        LanguageModelToolUse {
892            id: "tool_id_1".into(),
893            name: ToolRequiringPermission::NAME.into(),
894            raw_input: "{}".into(),
895            input: language_model::LanguageModelToolUseInput::Json(json!({})),
896            is_input_complete: true,
897            thought_signature: None,
898        },
899    ));
900    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
901        LanguageModelToolUse {
902            id: "tool_id_2".into(),
903            name: ToolRequiringPermission::NAME.into(),
904            raw_input: "{}".into(),
905            input: language_model::LanguageModelToolUseInput::Json(json!({})),
906            is_input_complete: true,
907            thought_signature: None,
908        },
909    ));
910    fake_model.end_last_completion_stream();
911    let tool_call_auth_1 = next_tool_call_authorization(&mut events).await;
912    let tool_call_auth_2 = next_tool_call_authorization(&mut events).await;
913
914    // Approve the first - send "allow" option_id (UI transforms "once" to "allow")
915    tool_call_auth_1
916        .response
917        .send(acp_thread::SelectedPermissionOutcome::new(
918            acp::PermissionOptionId::new("allow"),
919            acp::PermissionOptionKind::AllowOnce,
920        ))
921        .unwrap();
922    cx.run_until_parked();
923
924    // Reject the second - send "deny" option_id directly since Deny is now a button
925    tool_call_auth_2
926        .response
927        .send(acp_thread::SelectedPermissionOutcome::new(
928            acp::PermissionOptionId::new("deny"),
929            acp::PermissionOptionKind::RejectOnce,
930        ))
931        .unwrap();
932    cx.run_until_parked();
933
934    let completion = fake_model.pending_completions().pop().unwrap();
935    let message = completion.messages.last().unwrap();
936    assert_eq!(
937        message.content,
938        vec![
939            language_model::MessageContent::ToolResult(LanguageModelToolResult {
940                tool_use_id: "tool_id_1".into(),
941                tool_name: ToolRequiringPermission::NAME.into(),
942                is_error: false,
943                content: vec!["Allowed".into()],
944                output: Some("Allowed".into())
945            }),
946            language_model::MessageContent::ToolResult(LanguageModelToolResult {
947                tool_use_id: "tool_id_2".into(),
948                tool_name: ToolRequiringPermission::NAME.into(),
949                is_error: true,
950                content: vec!["Permission to run tool denied by user".into()],
951                output: Some("Permission to run tool denied by user".into())
952            })
953        ]
954    );
955
956    // Simulate yet another tool call.
957    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
958        LanguageModelToolUse {
959            id: "tool_id_3".into(),
960            name: ToolRequiringPermission::NAME.into(),
961            raw_input: "{}".into(),
962            input: language_model::LanguageModelToolUseInput::Json(json!({})),
963            is_input_complete: true,
964            thought_signature: None,
965        },
966    ));
967    fake_model.end_last_completion_stream();
968
969    // Respond by always allowing tools - send transformed option_id
970    // (UI transforms "always:tool_requiring_permission" to "always_allow:tool_requiring_permission")
971    let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
972    tool_call_auth_3
973        .response
974        .send(acp_thread::SelectedPermissionOutcome::new(
975            acp::PermissionOptionId::new("always_allow:tool_requiring_permission"),
976            acp::PermissionOptionKind::AllowAlways,
977        ))
978        .unwrap();
979    cx.run_until_parked();
980    let completion = fake_model.pending_completions().pop().unwrap();
981    let message = completion.messages.last().unwrap();
982    assert_eq!(
983        message.content,
984        vec![language_model::MessageContent::ToolResult(
985            LanguageModelToolResult {
986                tool_use_id: "tool_id_3".into(),
987                tool_name: ToolRequiringPermission::NAME.into(),
988                is_error: false,
989                content: vec!["Allowed".into()],
990                output: Some("Allowed".into())
991            }
992        )]
993    );
994
995    // Simulate a final tool call, ensuring we don't trigger authorization.
996    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
997        LanguageModelToolUse {
998            id: "tool_id_4".into(),
999            name: ToolRequiringPermission::NAME.into(),
1000            raw_input: "{}".into(),
1001            input: language_model::LanguageModelToolUseInput::Json(json!({})),
1002            is_input_complete: true,
1003            thought_signature: None,
1004        },
1005    ));
1006    fake_model.end_last_completion_stream();
1007    cx.run_until_parked();
1008    let completion = fake_model.pending_completions().pop().unwrap();
1009    let message = completion.messages.last().unwrap();
1010    assert_eq!(
1011        message.content,
1012        vec![language_model::MessageContent::ToolResult(
1013            LanguageModelToolResult {
1014                tool_use_id: "tool_id_4".into(),
1015                tool_name: ToolRequiringPermission::NAME.into(),
1016                is_error: false,
1017                content: vec!["Allowed".into()],
1018                output: Some("Allowed".into())
1019            }
1020        )]
1021    );
1022}
1023
1024#[gpui::test]
1025async fn test_tool_hallucination(cx: &mut TestAppContext) {
1026    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1027    let fake_model = model.as_fake();
1028
1029    let mut events = thread
1030        .update(cx, |thread, cx| {
1031            thread.send(ClientUserMessageId::new(), ["abc"], cx)
1032        })
1033        .unwrap();
1034    cx.run_until_parked();
1035    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1036        LanguageModelToolUse {
1037            id: "tool_id_1".into(),
1038            name: "nonexistent_tool".into(),
1039            raw_input: "{}".into(),
1040            input: language_model::LanguageModelToolUseInput::Json(json!({})),
1041            is_input_complete: true,
1042            thought_signature: None,
1043        },
1044    ));
1045    fake_model.end_last_completion_stream();
1046
1047    let tool_call = expect_tool_call(&mut events).await;
1048    assert_eq!(tool_call.title, "nonexistent_tool");
1049    assert_eq!(tool_call.status, acp::ToolCallStatus::Pending);
1050    let update = expect_tool_call_update_fields(&mut events).await;
1051    assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed));
1052}
1053
1054/// Regression test: some providers (confirmed on Bedrock Mantle/GPT-5.x)
1055/// reset their raw `tool_use` id counter every request/response cycle, so
1056/// the same id (e.g. `call_1`) can recur within one turn. Used verbatim as
1057/// the ACP id, this let a later tool call overwrite an earlier, unrelated
1058/// one in `AcpThread::upsert_tool_call`.
1059#[gpui::test]
1060async fn test_tool_call_id_scoped_per_completion_request(cx: &mut TestAppContext) {
1061    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
1062    let fake_model = model.as_fake();
1063
1064    thread.update(cx, |thread, _cx| {
1065        thread.add_tool(EchoTool);
1066    });
1067
1068    let mut events = thread
1069        .update(cx, |thread, cx| {
1070            thread.send(ClientUserMessageId::new(), ["Use the echo tool twice"], cx)
1071        })
1072        .unwrap();
1073    cx.run_until_parked();
1074
1075    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1076        LanguageModelToolUse {
1077            id: "call_1".into(),
1078            name: EchoTool::NAME.into(),
1079            raw_input: json!({"text": "first"}).to_string(),
1080            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "first"})),
1081            is_input_complete: true,
1082            thought_signature: None,
1083        },
1084    ));
1085    fake_model.end_last_completion_stream();
1086    let first_tool_call = next_tool_call(&mut events).await;
1087    cx.run_until_parked();
1088
1089    // Same turn, second cycle: the id counter has reset, so "call_1" recurs
1090    // for a different tool call.
1091    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1092        LanguageModelToolUse {
1093            id: "call_1".into(),
1094            name: EchoTool::NAME.into(),
1095            raw_input: json!({"text": "second"}).to_string(),
1096            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "second"})),
1097            is_input_complete: true,
1098            thought_signature: None,
1099        },
1100    ));
1101    fake_model.end_last_completion_stream();
1102    let second_tool_call = next_tool_call(&mut events).await;
1103    cx.run_until_parked();
1104
1105    fake_model
1106        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
1107    fake_model.end_last_completion_stream();
1108    events.collect::<Vec<_>>().await;
1109
1110    assert_ne!(
1111        first_tool_call.tool_call_id, second_tool_call.tool_call_id,
1112        "raw tool_use ids that recur across separate completion requests within the same turn \
1113         must map to distinct ACP tool call ids, otherwise the second tool call would overwrite \
1114         the first instead of appearing as a new entry"
1115    );
1116}
1117
1118/// Same bug as `test_tool_call_id_scoped_per_completion_request`, but
1119/// exercised through persistence and `Thread::replay()` instead of live
1120/// streaming.
1121#[gpui::test]
1122async fn test_replayed_tool_call_ids_scoped_across_messages(cx: &mut TestAppContext) {
1123    let ThreadTest {
1124        model,
1125        thread,
1126        project_context,
1127        ..
1128    } = setup(cx, TestModel::Fake).await;
1129    let fake_model = model.as_fake();
1130
1131    thread.update(cx, |thread, _cx| {
1132        thread.add_tool(EchoTool);
1133    });
1134
1135    thread
1136        .update(cx, |thread, cx| {
1137            thread.send(ClientUserMessageId::new(), ["Use the echo tool"], cx)
1138        })
1139        .unwrap();
1140    cx.run_until_parked();
1141    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1142        LanguageModelToolUse {
1143            id: "call_1".into(),
1144            name: EchoTool::NAME.into(),
1145            raw_input: json!({"text": "first"}).to_string(),
1146            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "first"})),
1147            is_input_complete: true,
1148            thought_signature: None,
1149        },
1150    ));
1151    fake_model.end_last_completion_stream();
1152    cx.run_until_parked();
1153    fake_model.send_last_completion_stream_text_chunk("Done with first");
1154    fake_model.end_last_completion_stream();
1155    cx.run_until_parked();
1156
1157    // Different turn: the id counter has reset, so "call_1" recurs in a
1158    // different `AgentMessage`.
1159    thread
1160        .update(cx, |thread, cx| {
1161            thread.send(ClientUserMessageId::new(), ["Use the echo tool again"], cx)
1162        })
1163        .unwrap();
1164    cx.run_until_parked();
1165    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1166        LanguageModelToolUse {
1167            id: "call_1".into(),
1168            name: EchoTool::NAME.into(),
1169            raw_input: json!({"text": "second"}).to_string(),
1170            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "second"})),
1171            is_input_complete: true,
1172            thought_signature: None,
1173        },
1174    ));
1175    fake_model.end_last_completion_stream();
1176    cx.run_until_parked();
1177    fake_model.send_last_completion_stream_text_chunk("Done with second");
1178    fake_model.end_last_completion_stream();
1179    cx.run_until_parked();
1180
1181    let db_thread = thread.read_with(cx, |thread, cx| thread.to_db(cx)).await;
1182
1183    cx.update(|cx| {
1184        LanguageModelRegistry::test(cx);
1185    });
1186    let restored = cx.update(|cx| {
1187        let thread = thread.read(cx);
1188        let project = thread.project.clone();
1189        let context_server_registry = thread.context_server_registry.clone();
1190        let templates = thread.templates.clone();
1191        cx.new(|cx| {
1192            Thread::from_db(
1193                acp::SessionId::new("restored"),
1194                db_thread,
1195                project,
1196                project_context.clone(),
1197                context_server_registry,
1198                templates,
1199                cx,
1200            )
1201        })
1202    });
1203    restored.update(cx, |thread, _cx| {
1204        thread.add_tool(EchoTool);
1205    });
1206
1207    let mut replay_events = restored.update(cx, |thread, cx| thread.replay(cx));
1208    let mut tool_call_ids = Vec::new();
1209    while let Some(event) = replay_events.next().await {
1210        if let ThreadEvent::ToolCall(tool_call) = event.unwrap() {
1211            tool_call_ids.push(tool_call.tool_call_id);
1212        }
1213    }
1214
1215    assert_eq!(
1216        tool_call_ids.len(),
1217        2,
1218        "expected one replayed tool call per message"
1219    );
1220    assert_ne!(
1221        tool_call_ids[0], tool_call_ids[1],
1222        "raw tool_use ids that recur across separate AgentMessages must map to distinct ACP \
1223         tool call ids when replayed from a persisted thread, otherwise the second tool call \
1224         would overwrite the first instead of appearing as a new entry"
1225    );
1226}
1227
1228async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
1229    let event = events
1230        .next()
1231        .await
1232        .expect("no tool call authorization event received")
1233        .unwrap();
1234    match event {
1235        ThreadEvent::ToolCall(tool_call) => tool_call,
1236        event => {
1237            panic!("Unexpected event {event:?}");
1238        }
1239    }
1240}
1241
1242/// Like [`expect_tool_call`], but skips other events until a `ToolCall`
1243/// appears -- useful across multiple request/response cycles in one turn.
1244async fn next_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
1245    loop {
1246        let event = events
1247            .next()
1248            .await
1249            .expect("no tool call event received")
1250            .unwrap();
1251        if let ThreadEvent::ToolCall(tool_call) = event {
1252            return tool_call;
1253        }
1254    }
1255}
1256
1257async fn expect_tool_call_update_fields(
1258    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
1259) -> acp::ToolCallUpdate {
1260    let event = events
1261        .next()
1262        .await
1263        .expect("no tool call authorization event received")
1264        .unwrap();
1265    match event {
1266        ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update,
1267        event => {
1268            panic!("Unexpected event {event:?}");
1269        }
1270    }
1271}
1272
1273async fn next_tool_call_authorization(
1274    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
1275) -> ToolCallAuthorization {
1276    loop {
1277        let event = events
1278            .next()
1279            .await
1280            .expect("no tool call authorization event received")
1281            .unwrap();
1282        if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event {
1283            let permission_kinds = tool_call_authorization
1284                .options
1285                .first_option_of_kind(acp::PermissionOptionKind::AllowAlways)
1286                .map(|option| option.kind);
1287            let allow_once = tool_call_authorization
1288                .options
1289                .first_option_of_kind(acp::PermissionOptionKind::AllowOnce)
1290                .map(|option| option.kind);
1291
1292            assert_eq!(
1293                permission_kinds,
1294                Some(acp::PermissionOptionKind::AllowAlways)
1295            );
1296            assert_eq!(allow_once, Some(acp::PermissionOptionKind::AllowOnce));
1297            return tool_call_authorization;
1298        }
1299    }
1300}
1301
1302#[test]
1303fn test_permission_options_terminal_with_pattern() {
1304    let permission_options = ToolPermissionContext::new(
1305        TerminalTool::NAME,
1306        vec!["cargo build --release".to_string()],
1307    )
1308    .build_permission_options();
1309
1310    let PermissionOptions::Dropdown(choices) = permission_options else {
1311        panic!("Expected dropdown permission options");
1312    };
1313
1314    assert_eq!(choices.len(), 3);
1315    let labels: Vec<&str> = choices
1316        .iter()
1317        .map(|choice| choice.allow.name.as_ref())
1318        .collect();
1319    assert!(labels.contains(&"Always for terminal"));
1320    assert!(labels.contains(&"Always for `cargo build` commands"));
1321    assert!(labels.contains(&"Only this time"));
1322}
1323
1324#[test]
1325fn test_permission_options_terminal_command_with_flag_second_token() {
1326    let permission_options =
1327        ToolPermissionContext::new(TerminalTool::NAME, vec!["ls -la".to_string()])
1328            .build_permission_options();
1329
1330    let PermissionOptions::Dropdown(choices) = permission_options else {
1331        panic!("Expected dropdown permission options");
1332    };
1333
1334    assert_eq!(choices.len(), 3);
1335    let labels: Vec<&str> = choices
1336        .iter()
1337        .map(|choice| choice.allow.name.as_ref())
1338        .collect();
1339    assert!(labels.contains(&"Always for terminal"));
1340    assert!(labels.contains(&"Always for `ls` commands"));
1341    assert!(labels.contains(&"Only this time"));
1342}
1343
1344#[test]
1345fn test_permission_options_terminal_single_word_command() {
1346    let permission_options =
1347        ToolPermissionContext::new(TerminalTool::NAME, vec!["whoami".to_string()])
1348            .build_permission_options();
1349
1350    let PermissionOptions::Dropdown(choices) = permission_options else {
1351        panic!("Expected dropdown permission options");
1352    };
1353
1354    assert_eq!(choices.len(), 3);
1355    let labels: Vec<&str> = choices
1356        .iter()
1357        .map(|choice| choice.allow.name.as_ref())
1358        .collect();
1359    assert!(labels.contains(&"Always for terminal"));
1360    assert!(labels.contains(&"Always for `whoami` commands"));
1361    assert!(labels.contains(&"Only this time"));
1362}
1363
1364#[test]
1365fn test_permission_options_edit_file_with_path_pattern() {
1366    let permission_options =
1367        ToolPermissionContext::new(EditFileTool::NAME, vec!["src/main.rs".to_string()])
1368            .build_permission_options();
1369
1370    let PermissionOptions::Dropdown(choices) = permission_options else {
1371        panic!("Expected dropdown permission options");
1372    };
1373
1374    let labels: Vec<&str> = choices
1375        .iter()
1376        .map(|choice| choice.allow.name.as_ref())
1377        .collect();
1378    assert!(labels.contains(&"Always for edit file"));
1379    assert!(labels.contains(&"Always for `src/`"));
1380}
1381
1382#[test]
1383fn test_permission_options_fetch_with_domain_pattern() {
1384    let permission_options =
1385        ToolPermissionContext::new(FetchTool::NAME, vec!["https://docs.rs/gpui".to_string()])
1386            .build_permission_options();
1387
1388    let PermissionOptions::Dropdown(choices) = permission_options else {
1389        panic!("Expected dropdown permission options");
1390    };
1391
1392    let labels: Vec<&str> = choices
1393        .iter()
1394        .map(|choice| choice.allow.name.as_ref())
1395        .collect();
1396    assert!(labels.contains(&"Always for fetch"));
1397    assert!(labels.contains(&"Always for `docs.rs`"));
1398}
1399
1400#[test]
1401fn test_permission_options_without_pattern() {
1402    let permission_options = ToolPermissionContext::new(
1403        TerminalTool::NAME,
1404        vec!["./deploy.sh --production".to_string()],
1405    )
1406    .build_permission_options();
1407
1408    let PermissionOptions::Dropdown(choices) = permission_options else {
1409        panic!("Expected dropdown permission options");
1410    };
1411
1412    assert_eq!(choices.len(), 2);
1413    let labels: Vec<&str> = choices
1414        .iter()
1415        .map(|choice| choice.allow.name.as_ref())
1416        .collect();
1417    assert!(labels.contains(&"Always for terminal"));
1418    assert!(labels.contains(&"Only this time"));
1419    assert!(!labels.iter().any(|label| label.contains("commands")));
1420}
1421
1422#[test]
1423fn test_permission_options_symlink_target_are_flat_once_only() {
1424    let permission_options =
1425        ToolPermissionContext::symlink_target(EditFileTool::NAME, vec!["/outside/file.txt".into()])
1426            .build_permission_options();
1427
1428    let PermissionOptions::Flat(options) = permission_options else {
1429        panic!("Expected flat permission options for symlink target authorization");
1430    };
1431
1432    assert_eq!(options.len(), 2);
1433    assert!(options.iter().any(|option| {
1434        option.option_id.0.as_ref() == "allow"
1435            && option.kind == acp::PermissionOptionKind::AllowOnce
1436    }));
1437    assert!(options.iter().any(|option| {
1438        option.option_id.0.as_ref() == "deny"
1439            && option.kind == acp::PermissionOptionKind::RejectOnce
1440    }));
1441}
1442
1443#[test]
1444fn test_permission_option_ids_for_terminal() {
1445    let permission_options = ToolPermissionContext::new(
1446        TerminalTool::NAME,
1447        vec!["cargo build --release".to_string()],
1448    )
1449    .build_permission_options();
1450
1451    let PermissionOptions::Dropdown(choices) = permission_options else {
1452        panic!("Expected dropdown permission options");
1453    };
1454
1455    // Expect 3 choices: always-tool, always-pattern, once
1456    assert_eq!(choices.len(), 3);
1457
1458    // First two choices both use the tool-level option IDs
1459    assert_eq!(
1460        choices[0].allow.option_id.0.as_ref(),
1461        "always_allow:terminal"
1462    );
1463    assert_eq!(choices[0].deny.option_id.0.as_ref(), "always_deny:terminal");
1464    assert!(choices[0].sub_patterns.is_empty());
1465
1466    assert_eq!(
1467        choices[1].allow.option_id.0.as_ref(),
1468        "always_allow:terminal"
1469    );
1470    assert_eq!(choices[1].deny.option_id.0.as_ref(), "always_deny:terminal");
1471    assert_eq!(choices[1].sub_patterns, vec!["^cargo\\s+build(\\s|$)"]);
1472
1473    // Third choice is the one-time allow/deny
1474    assert_eq!(choices[2].allow.option_id.0.as_ref(), "allow");
1475    assert_eq!(choices[2].deny.option_id.0.as_ref(), "deny");
1476    assert!(choices[2].sub_patterns.is_empty());
1477}
1478
1479#[test]
1480fn test_permission_options_terminal_pipeline_produces_dropdown_with_patterns() {
1481    let permission_options = ToolPermissionContext::new(
1482        TerminalTool::NAME,
1483        vec!["cargo test 2>&1 | tail".to_string()],
1484    )
1485    .build_permission_options();
1486
1487    let PermissionOptions::DropdownWithPatterns {
1488        choices,
1489        patterns,
1490        tool_name,
1491    } = permission_options
1492    else {
1493        panic!("Expected DropdownWithPatterns permission options for pipeline command");
1494    };
1495
1496    assert_eq!(tool_name, TerminalTool::NAME);
1497
1498    // Should have "Always for terminal" and "Only this time" choices
1499    assert_eq!(choices.len(), 2);
1500    let labels: Vec<&str> = choices
1501        .iter()
1502        .map(|choice| choice.allow.name.as_ref())
1503        .collect();
1504    assert!(labels.contains(&"Always for terminal"));
1505    assert!(labels.contains(&"Only this time"));
1506
1507    // Should have per-command patterns for "cargo test" and "tail"
1508    assert_eq!(patterns.len(), 2);
1509    let pattern_names: Vec<&str> = patterns.iter().map(|cp| cp.display_name.as_str()).collect();
1510    assert!(pattern_names.contains(&"cargo test"));
1511    assert!(pattern_names.contains(&"tail"));
1512
1513    // Verify patterns are valid regex patterns
1514    let regex_patterns: Vec<&str> = patterns.iter().map(|cp| cp.pattern.as_str()).collect();
1515    assert!(regex_patterns.contains(&"^cargo\\s+test(\\s|$)"));
1516    assert!(regex_patterns.contains(&"^tail\\b"));
1517}
1518
1519#[test]
1520fn test_permission_options_terminal_pipeline_with_chaining() {
1521    let permission_options = ToolPermissionContext::new(
1522        TerminalTool::NAME,
1523        vec!["npm install && npm test | tail".to_string()],
1524    )
1525    .build_permission_options();
1526
1527    let PermissionOptions::DropdownWithPatterns { patterns, .. } = permission_options else {
1528        panic!("Expected DropdownWithPatterns for chained pipeline command");
1529    };
1530
1531    // With subcommand-aware patterns, "npm install" and "npm test" are distinct
1532    assert_eq!(patterns.len(), 3);
1533    let pattern_names: Vec<&str> = patterns.iter().map(|cp| cp.display_name.as_str()).collect();
1534    assert!(pattern_names.contains(&"npm install"));
1535    assert!(pattern_names.contains(&"npm test"));
1536    assert!(pattern_names.contains(&"tail"));
1537}
1538
1539#[gpui::test]
1540#[cfg_attr(not(feature = "e2e"), ignore)]
1541async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
1542    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
1543
1544    // Test concurrent tool calls with different delay times
1545    let events = thread
1546        .update(cx, |thread, cx| {
1547            thread.add_tool(DelayTool);
1548            thread.send(
1549                ClientUserMessageId::new(),
1550                [
1551                    "Call the delay tool twice in the same message.",
1552                    "Once with 100ms. Once with 300ms.",
1553                    "When both timers are complete, describe the outputs.",
1554                ],
1555                cx,
1556            )
1557        })
1558        .unwrap()
1559        .collect()
1560        .await;
1561
1562    let stop_reasons = stop_events(events);
1563    assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]);
1564
1565    thread.update(cx, |thread, _cx| {
1566        let last_message = thread.last_received_or_pending_message().unwrap();
1567        let agent_message = last_message.as_agent_message().unwrap();
1568        let text = agent_message
1569            .content
1570            .iter()
1571            .filter_map(|content| {
1572                if let AgentMessageContent::Text(text) = content {
1573                    Some(text.as_str())
1574                } else {
1575                    None
1576                }
1577            })
1578            .collect::<String>();
1579
1580        assert!(text.contains("Ding"));
1581    });
1582}
1583
1584#[gpui::test]
1585async fn test_profiles(cx: &mut TestAppContext) {
1586    let ThreadTest {
1587        model, thread, fs, ..
1588    } = setup(cx, TestModel::Fake).await;
1589    let fake_model = model.as_fake();
1590
1591    thread.update(cx, |thread, _cx| {
1592        thread.add_tool(DelayTool);
1593        thread.add_tool(EchoTool);
1594        thread.add_tool(InfiniteTool);
1595    });
1596
1597    // Override profiles and wait for settings to be loaded.
1598    fs.insert_file(
1599        paths::settings_file(),
1600        json!({
1601            "agent": {
1602                "profiles": {
1603                    "test-1": {
1604                        "name": "Test Profile 1",
1605                        "tools": {
1606                            EchoTool::NAME: true,
1607                            DelayTool::NAME: true,
1608                        }
1609                    },
1610                    "test-2": {
1611                        "name": "Test Profile 2",
1612                        "tools": {
1613                            InfiniteTool::NAME: true,
1614                        }
1615                    }
1616                }
1617            }
1618        })
1619        .to_string()
1620        .into_bytes(),
1621    )
1622    .await;
1623    cx.run_until_parked();
1624
1625    // Test that test-1 profile (default) has echo and delay tools
1626    thread
1627        .update(cx, |thread, cx| {
1628            thread.set_profile(AgentProfileId("test-1".into()), cx);
1629            thread.send(ClientUserMessageId::new(), ["test"], cx)
1630        })
1631        .unwrap();
1632    cx.run_until_parked();
1633
1634    let mut pending_completions = fake_model.pending_completions();
1635    assert_eq!(pending_completions.len(), 1);
1636    let completion = pending_completions.pop().unwrap();
1637    let tool_names: Vec<String> = completion
1638        .tools
1639        .iter()
1640        .map(|tool| tool.name.clone())
1641        .collect();
1642    assert_eq!(tool_names, vec![DelayTool::NAME, EchoTool::NAME]);
1643    fake_model.end_last_completion_stream();
1644
1645    // Switch to test-2 profile, and verify that it has only the infinite tool.
1646    thread
1647        .update(cx, |thread, cx| {
1648            thread.set_profile(AgentProfileId("test-2".into()), cx);
1649            thread.send(ClientUserMessageId::new(), ["test2"], cx)
1650        })
1651        .unwrap();
1652    cx.run_until_parked();
1653    let mut pending_completions = fake_model.pending_completions();
1654    assert_eq!(pending_completions.len(), 1);
1655    let completion = pending_completions.pop().unwrap();
1656    let tool_names: Vec<String> = completion
1657        .tools
1658        .iter()
1659        .map(|tool| tool.name.clone())
1660        .collect();
1661    assert_eq!(tool_names, vec![InfiniteTool::NAME]);
1662}
1663
1664#[gpui::test]
1665async fn test_mcp_tools(cx: &mut TestAppContext) {
1666    let ThreadTest {
1667        model,
1668        thread,
1669        context_server_store,
1670        fs,
1671        ..
1672    } = setup(cx, TestModel::Fake).await;
1673    let fake_model = model.as_fake();
1674
1675    // Override profiles and wait for settings to be loaded.
1676    fs.insert_file(
1677        paths::settings_file(),
1678        json!({
1679            "agent": {
1680                "tool_permissions": { "default": "allow" },
1681                "profiles": {
1682                    "test": {
1683                        "name": "Test Profile",
1684                        "enable_all_context_servers": true,
1685                        "tools": {
1686                            EchoTool::NAME: true,
1687                        }
1688                    },
1689                }
1690            }
1691        })
1692        .to_string()
1693        .into_bytes(),
1694    )
1695    .await;
1696    cx.run_until_parked();
1697    thread.update(cx, |thread, cx| {
1698        thread.set_profile(AgentProfileId("test".into()), cx)
1699    });
1700
1701    let mut mcp_tool_calls = setup_context_server(
1702        "test_server",
1703        vec![context_server::types::Tool {
1704            name: "echo".into(),
1705            title: None,
1706            description: None,
1707            input_schema: serde_json::to_value(EchoTool::input_schema(
1708                LanguageModelToolSchemaFormat::JsonSchema,
1709            ))
1710            .unwrap(),
1711            output_schema: None,
1712            annotations: None,
1713        }],
1714        &context_server_store,
1715        cx,
1716    );
1717
1718    let events = thread.update(cx, |thread, cx| {
1719        thread
1720            .send(ClientUserMessageId::new(), ["Hey"], cx)
1721            .unwrap()
1722    });
1723    cx.run_until_parked();
1724
1725    // Simulate the model calling the MCP tool.
1726    let completion = fake_model.pending_completions().pop().unwrap();
1727    assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
1728    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1729        LanguageModelToolUse {
1730            id: "tool_1".into(),
1731            name: "echo".into(),
1732            raw_input: json!({"text": "test"}).to_string(),
1733            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
1734            is_input_complete: true,
1735            thought_signature: None,
1736        },
1737    ));
1738    fake_model.end_last_completion_stream();
1739    cx.run_until_parked();
1740
1741    let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
1742    assert_eq!(tool_call_params.name, "echo");
1743    assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"})));
1744    tool_call_response
1745        .send(context_server::types::CallToolResponse {
1746            content: vec![context_server::types::ToolResponseContent::Text {
1747                text: "test".into(),
1748            }],
1749            is_error: None,
1750            meta: None,
1751            structured_content: None,
1752        })
1753        .unwrap();
1754    cx.run_until_parked();
1755
1756    assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
1757    fake_model.send_last_completion_stream_text_chunk("Done!");
1758    fake_model.end_last_completion_stream();
1759    events.collect::<Vec<_>>().await;
1760
1761    // Send again after adding the echo tool, ensuring the name collision is resolved.
1762    let events = thread.update(cx, |thread, cx| {
1763        thread.add_tool(EchoTool);
1764        thread.send(ClientUserMessageId::new(), ["Go"], cx).unwrap()
1765    });
1766    cx.run_until_parked();
1767    let completion = fake_model.pending_completions().pop().unwrap();
1768    assert_eq!(
1769        tool_names_for_completion(&completion),
1770        vec!["echo", "test_server_echo"]
1771    );
1772    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1773        LanguageModelToolUse {
1774            id: "tool_2".into(),
1775            name: "test_server_echo".into(),
1776            raw_input: json!({"text": "mcp"}).to_string(),
1777            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "mcp"})),
1778            is_input_complete: true,
1779            thought_signature: None,
1780        },
1781    ));
1782    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1783        LanguageModelToolUse {
1784            id: "tool_3".into(),
1785            name: "echo".into(),
1786            raw_input: json!({"text": "native"}).to_string(),
1787            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "native"})),
1788            is_input_complete: true,
1789            thought_signature: None,
1790        },
1791    ));
1792    fake_model.end_last_completion_stream();
1793    cx.run_until_parked();
1794
1795    let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
1796    assert_eq!(tool_call_params.name, "echo");
1797    assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"})));
1798    tool_call_response
1799        .send(context_server::types::CallToolResponse {
1800            content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }],
1801            is_error: None,
1802            meta: None,
1803            structured_content: None,
1804        })
1805        .unwrap();
1806    cx.run_until_parked();
1807
1808    // Ensure the tool results were inserted with the correct names.
1809    let completion = fake_model.pending_completions().pop().unwrap();
1810    assert_eq!(
1811        completion.messages.last().unwrap().content,
1812        vec![
1813            MessageContent::ToolResult(LanguageModelToolResult {
1814                tool_use_id: "tool_3".into(),
1815                tool_name: "echo".into(),
1816                is_error: false,
1817                content: vec!["native".into()],
1818                output: Some("native".into()),
1819            },),
1820            MessageContent::ToolResult(LanguageModelToolResult {
1821                tool_use_id: "tool_2".into(),
1822                tool_name: "test_server_echo".into(),
1823                is_error: false,
1824                content: vec!["mcp".into()],
1825                output: Some("mcp".into()),
1826            },),
1827        ]
1828    );
1829    fake_model.end_last_completion_stream();
1830    events.collect::<Vec<_>>().await;
1831}
1832
1833#[gpui::test]
1834async fn test_mcp_tool_names_are_sanitized_for_providers(cx: &mut TestAppContext) {
1835    let ThreadTest {
1836        model,
1837        thread,
1838        context_server_store,
1839        fs,
1840        ..
1841    } = setup(cx, TestModel::Fake).await;
1842    let fake_model = model.as_fake();
1843
1844    fs.insert_file(
1845        paths::settings_file(),
1846        json!({
1847            "agent": {
1848                "tool_permissions": { "default": "allow" },
1849                "profiles": {
1850                    "test": {
1851                        "name": "Test Profile",
1852                        "enable_all_context_servers": true,
1853                    },
1854                }
1855            }
1856        })
1857        .to_string()
1858        .into_bytes(),
1859    )
1860    .await;
1861    cx.run_until_parked();
1862    thread.update(cx, |thread, cx| {
1863        thread.set_profile(AgentProfileId("test".into()), cx)
1864    });
1865
1866    let mut mcp_tool_calls = setup_context_server(
1867        "Superluminal",
1868        vec![context_server::types::Tool {
1869            name: "snake_case.PascalCase".into(),
1870            title: None,
1871            description: None,
1872            input_schema: json!({"type": "object", "properties": {}}),
1873            output_schema: None,
1874            annotations: None,
1875        }],
1876        &context_server_store,
1877        cx,
1878    );
1879
1880    let events = thread.update(cx, |thread, cx| {
1881        thread
1882            .send(ClientUserMessageId::new(), ["Use the MCP tool"], cx)
1883            .unwrap()
1884    });
1885    cx.run_until_parked();
1886
1887    let completion = fake_model.pending_completions().pop().unwrap();
1888    assert_eq!(
1889        tool_names_for_completion(&completion),
1890        vec!["snake_case_PascalCase"]
1891    );
1892    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1893        LanguageModelToolUse {
1894            id: "tool_1".into(),
1895            name: "snake_case_PascalCase".into(),
1896            raw_input: json!({}).to_string(),
1897            input: language_model::LanguageModelToolUseInput::Json(json!({})),
1898            is_input_complete: true,
1899            thought_signature: None,
1900        },
1901    ));
1902    fake_model.end_last_completion_stream();
1903    cx.run_until_parked();
1904
1905    let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
1906    assert_eq!(tool_call_params.name, "snake_case.PascalCase");
1907    tool_call_response
1908        .send(context_server::types::CallToolResponse {
1909            content: vec![context_server::types::ToolResponseContent::Text {
1910                text: "done".into(),
1911            }],
1912            is_error: None,
1913            meta: None,
1914            structured_content: None,
1915        })
1916        .unwrap();
1917    cx.run_until_parked();
1918
1919    fake_model.send_last_completion_stream_text_chunk("Done!");
1920    fake_model.end_last_completion_stream();
1921    events.collect::<Vec<_>>().await;
1922}
1923
1924#[gpui::test]
1925async fn test_mcp_tool_multi_content_response(cx: &mut TestAppContext) {
1926    let ThreadTest {
1927        model,
1928        thread,
1929        context_server_store,
1930        fs,
1931        ..
1932    } = setup(cx, TestModel::Fake).await;
1933    let fake_model = model.as_fake();
1934    fake_model.set_supports_images(true);
1935
1936    fs.insert_file(
1937        paths::settings_file(),
1938        json!({
1939            "agent": {
1940                "tool_permissions": { "default": "allow" },
1941                "profiles": {
1942                    "test": {
1943                        "name": "Test Profile",
1944                        "enable_all_context_servers": true,
1945                        "tools": {}
1946                    },
1947                }
1948            }
1949        })
1950        .to_string()
1951        .into_bytes(),
1952    )
1953    .await;
1954    cx.run_until_parked();
1955    thread.update(cx, |thread, cx| {
1956        thread.set_profile(AgentProfileId("test".into()), cx)
1957    });
1958
1959    let mut mcp_tool_calls = setup_context_server(
1960        "screenshot_server",
1961        vec![context_server::types::Tool {
1962            name: "screenshot".into(),
1963            title: None,
1964            description: None,
1965            input_schema: json!({"type": "object", "properties": {}}),
1966            output_schema: None,
1967            annotations: None,
1968        }],
1969        &context_server_store,
1970        cx,
1971    );
1972
1973    let events = thread.update(cx, |thread, cx| {
1974        thread
1975            .send(ClientUserMessageId::new(), ["Take a screenshot"], cx)
1976            .unwrap()
1977    });
1978    cx.run_until_parked();
1979
1980    let completion = fake_model.pending_completions().pop().unwrap();
1981    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
1982        LanguageModelToolUse {
1983            id: "tool_1".into(),
1984            name: "screenshot".into(),
1985            raw_input: json!({}).to_string(),
1986            input: language_model::LanguageModelToolUseInput::Json(json!({})),
1987            is_input_complete: true,
1988            thought_signature: None,
1989        },
1990    ));
1991    fake_model.end_last_completion_stream();
1992    cx.run_until_parked();
1993    let _ = completion;
1994
1995    let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
1996    assert_eq!(tool_call_params.name, "screenshot");
1997    let image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
1998    tool_call_response
1999        .send(context_server::types::CallToolResponse {
2000            content: vec![
2001                context_server::types::ToolResponseContent::Text {
2002                    text: "Some text".into(),
2003                },
2004                context_server::types::ToolResponseContent::Image {
2005                    data: image_data.into(),
2006                    mime_type: "image/png".into(),
2007                },
2008                context_server::types::ToolResponseContent::Text {
2009                    text: "Some more text".into(),
2010                },
2011            ],
2012            is_error: None,
2013            meta: None,
2014            structured_content: None,
2015        })
2016        .unwrap();
2017    cx.run_until_parked();
2018
2019    // Verify the tool result round-trips back to the model as a multi-part Vec.
2020    let completion = fake_model.pending_completions().pop().unwrap();
2021    let tool_result = completion
2022        .messages
2023        .last()
2024        .unwrap()
2025        .content
2026        .iter()
2027        .find_map(|c| match c {
2028            MessageContent::ToolResult(r) => Some(r.clone()),
2029            _ => None,
2030        })
2031        .expect("expected a tool result");
2032    assert_eq!(tool_result.tool_use_id, "tool_1".into());
2033    assert_eq!(tool_result.content.len(), 3);
2034    assert_eq!(
2035        tool_result.content[0],
2036        language_model::LanguageModelToolResultContent::Text(Arc::from("Some text"))
2037    );
2038    let expected_image =
2039        language_model::LanguageModelImage::from_base64_image(image_data, "image/png")
2040            .expect("image conversion should not error")
2041            .expect("image conversion should succeed");
2042    assert_eq!(
2043        tool_result.content[0],
2044        language_model::LanguageModelToolResultContent::Text(Arc::from("Some text"))
2045    );
2046    assert_eq!(
2047        tool_result.content[1],
2048        language_model::LanguageModelToolResultContent::Image(expected_image)
2049    );
2050    assert_eq!(
2051        tool_result.content[2],
2052        language_model::LanguageModelToolResultContent::Text(Arc::from("Some more text"))
2053    );
2054    fake_model.end_last_completion_stream();
2055    events.collect::<Vec<_>>().await;
2056}
2057
2058#[gpui::test]
2059async fn test_mcp_tool_result_displayed_when_server_disconnected(cx: &mut TestAppContext) {
2060    let ThreadTest {
2061        model,
2062        thread,
2063        context_server_store,
2064        fs,
2065        ..
2066    } = setup(cx, TestModel::Fake).await;
2067    let fake_model = model.as_fake();
2068
2069    // Setup settings to allow MCP tools
2070    fs.insert_file(
2071        paths::settings_file(),
2072        json!({
2073            "agent": {
2074                "always_allow_tool_actions": true,
2075                "profiles": {
2076                    "test": {
2077                        "name": "Test Profile",
2078                        "enable_all_context_servers": true,
2079                        "tools": {}
2080                    },
2081                }
2082            }
2083        })
2084        .to_string()
2085        .into_bytes(),
2086    )
2087    .await;
2088    cx.run_until_parked();
2089    thread.update(cx, |thread, cx| {
2090        thread.set_profile(AgentProfileId("test".into()), cx)
2091    });
2092
2093    // Setup a context server with a tool
2094    let mut mcp_tool_calls = setup_context_server(
2095        "github_server",
2096        vec![context_server::types::Tool {
2097            name: "issue_read".into(),
2098            title: None,
2099            description: Some("Read a GitHub issue".into()),
2100            input_schema: json!({
2101                "type": "object",
2102                "properties": {
2103                    "issue_url": { "type": "string" }
2104                }
2105            }),
2106            output_schema: None,
2107            annotations: None,
2108        }],
2109        &context_server_store,
2110        cx,
2111    );
2112
2113    // Send a message and have the model call the MCP tool
2114    let events = thread.update(cx, |thread, cx| {
2115        thread
2116            .send(ClientUserMessageId::new(), ["Read issue #47404"], cx)
2117            .unwrap()
2118    });
2119    cx.run_until_parked();
2120
2121    // Verify the MCP tool is available to the model
2122    let completion = fake_model.pending_completions().pop().unwrap();
2123    assert_eq!(
2124        tool_names_for_completion(&completion),
2125        vec!["issue_read"],
2126        "MCP tool should be available"
2127    );
2128
2129    // Simulate the model calling the MCP tool
2130    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2131        LanguageModelToolUse {
2132            id: "tool_1".into(),
2133            name: "issue_read".into(),
2134            raw_input: json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"})
2135                .to_string(),
2136            input: language_model::LanguageModelToolUseInput::Json(
2137                json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"}),
2138            ),
2139            is_input_complete: true,
2140            thought_signature: None,
2141        },
2142    ));
2143    fake_model.end_last_completion_stream();
2144    cx.run_until_parked();
2145
2146    // The MCP server receives the tool call and responds with content
2147    let expected_tool_output = "Issue #47404: Tool call results are cleared upon app close";
2148    let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
2149    assert_eq!(tool_call_params.name, "issue_read");
2150    tool_call_response
2151        .send(context_server::types::CallToolResponse {
2152            content: vec![context_server::types::ToolResponseContent::Text {
2153                text: expected_tool_output.into(),
2154            }],
2155            is_error: None,
2156            meta: None,
2157            structured_content: None,
2158        })
2159        .unwrap();
2160    cx.run_until_parked();
2161
2162    // After tool completes, the model continues with a new completion request
2163    // that includes the tool results. We need to respond to this.
2164    let _completion = fake_model.pending_completions().pop().unwrap();
2165    fake_model.send_last_completion_stream_text_chunk("I found the issue!");
2166    fake_model
2167        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
2168    fake_model.end_last_completion_stream();
2169    events.collect::<Vec<_>>().await;
2170
2171    // Verify the tool result is stored in the thread by checking the markdown output.
2172    // The tool result is in the first assistant message (not the last one, which is
2173    // the model's response after the tool completed).
2174    thread.update(cx, |thread, _cx| {
2175        let markdown = thread.to_markdown();
2176        assert!(
2177            markdown.contains("**Tool Result**: issue_read"),
2178            "Thread should contain tool result header"
2179        );
2180        assert!(
2181            markdown.contains(expected_tool_output),
2182            "Thread should contain tool output: {}",
2183            expected_tool_output
2184        );
2185    });
2186
2187    // Simulate app restart: disconnect the MCP server.
2188    // After restart, the MCP server won't be connected yet when the thread is replayed.
2189    context_server_store.update(cx, |store, cx| {
2190        let _ = store.stop_server(&ContextServerId("github_server".into()), cx);
2191    });
2192    cx.run_until_parked();
2193
2194    // Replay the thread (this is what happens when loading a saved thread)
2195    let mut replay_events = thread.update(cx, |thread, cx| thread.replay(cx));
2196
2197    let mut found_tool_call = None;
2198    let mut found_tool_call_update_with_output = None;
2199    // The ACP id is scoped by message index (see `scoped_tool_call_id`), so
2200    // capture it instead of assuming it matches the raw provider id.
2201    let mut tool_call_id = None;
2202
2203    while let Some(event) = replay_events.next().await {
2204        let event = event.unwrap();
2205        match &event {
2206            ThreadEvent::ToolCall(tc) => {
2207                tool_call_id = Some(tc.tool_call_id.clone());
2208                found_tool_call = Some(tc.clone());
2209            }
2210            ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update))
2211                if tool_call_id.as_ref() == Some(&update.tool_call_id) =>
2212            {
2213                if update.fields.raw_output.is_some() {
2214                    found_tool_call_update_with_output = Some(update.clone());
2215                }
2216            }
2217            _ => {}
2218        }
2219    }
2220
2221    // The tool call should be found
2222    assert!(
2223        found_tool_call.is_some(),
2224        "Tool call should be emitted during replay"
2225    );
2226
2227    assert!(
2228        found_tool_call_update_with_output.is_some(),
2229        "ToolCallUpdate with raw_output should be emitted even when MCP server is disconnected."
2230    );
2231
2232    let update = found_tool_call_update_with_output.unwrap();
2233    assert_eq!(
2234        update.fields.raw_output,
2235        Some(expected_tool_output.into()),
2236        "raw_output should contain the saved tool result"
2237    );
2238
2239    // Also verify the status is correct (completed, not failed)
2240    assert_eq!(
2241        update.fields.status,
2242        Some(acp::ToolCallStatus::Completed),
2243        "Tool call status should reflect the original completion status"
2244    );
2245}
2246
2247#[gpui::test]
2248async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
2249    let ThreadTest {
2250        model,
2251        thread,
2252        context_server_store,
2253        fs,
2254        ..
2255    } = setup(cx, TestModel::Fake).await;
2256    let fake_model = model.as_fake();
2257
2258    // Set up a profile with all tools enabled
2259    fs.insert_file(
2260        paths::settings_file(),
2261        json!({
2262            "agent": {
2263                "profiles": {
2264                    "test": {
2265                        "name": "Test Profile",
2266                        "enable_all_context_servers": true,
2267                        "tools": {
2268                            EchoTool::NAME: true,
2269                            DelayTool::NAME: true,
2270                            WordListTool::NAME: true,
2271                            ToolRequiringPermission::NAME: true,
2272                            InfiniteTool::NAME: true,
2273                        }
2274                    },
2275                }
2276            }
2277        })
2278        .to_string()
2279        .into_bytes(),
2280    )
2281    .await;
2282    cx.run_until_parked();
2283
2284    thread.update(cx, |thread, cx| {
2285        thread.set_profile(AgentProfileId("test".into()), cx);
2286        thread.add_tool(EchoTool);
2287        thread.add_tool(DelayTool);
2288        thread.add_tool(WordListTool);
2289        thread.add_tool(ToolRequiringPermission);
2290        thread.add_tool(InfiniteTool);
2291    });
2292
2293    // Set up multiple context servers with some overlapping tool names
2294    let _server1_calls = setup_context_server(
2295        "xxx",
2296        vec![
2297            context_server::types::Tool {
2298                name: "echo".into(), // Conflicts with native EchoTool
2299                title: None,
2300                description: None,
2301                input_schema: serde_json::to_value(EchoTool::input_schema(
2302                    LanguageModelToolSchemaFormat::JsonSchema,
2303                ))
2304                .unwrap(),
2305                output_schema: None,
2306                annotations: None,
2307            },
2308            context_server::types::Tool {
2309                name: "unique_tool_1".into(),
2310                title: None,
2311                description: None,
2312                input_schema: json!({"type": "object", "properties": {}}),
2313                output_schema: None,
2314                annotations: None,
2315            },
2316        ],
2317        &context_server_store,
2318        cx,
2319    );
2320
2321    let _server2_calls = setup_context_server(
2322        "yyy",
2323        vec![
2324            context_server::types::Tool {
2325                name: "echo".into(), // Also conflicts with native EchoTool
2326                title: None,
2327                description: None,
2328                input_schema: serde_json::to_value(EchoTool::input_schema(
2329                    LanguageModelToolSchemaFormat::JsonSchema,
2330                ))
2331                .unwrap(),
2332                output_schema: None,
2333                annotations: None,
2334            },
2335            context_server::types::Tool {
2336                name: "unique_tool_2".into(),
2337                title: None,
2338                description: None,
2339                input_schema: json!({"type": "object", "properties": {}}),
2340                output_schema: None,
2341                annotations: None,
2342            },
2343            context_server::types::Tool {
2344                name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
2345                title: None,
2346                description: None,
2347                input_schema: json!({"type": "object", "properties": {}}),
2348                output_schema: None,
2349                annotations: None,
2350            },
2351            context_server::types::Tool {
2352                name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
2353                title: None,
2354                description: None,
2355                input_schema: json!({"type": "object", "properties": {}}),
2356                output_schema: None,
2357                annotations: None,
2358            },
2359        ],
2360        &context_server_store,
2361        cx,
2362    );
2363    let _server3_calls = setup_context_server(
2364        "zzz",
2365        vec![
2366            context_server::types::Tool {
2367                name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
2368                title: None,
2369                description: None,
2370                input_schema: json!({"type": "object", "properties": {}}),
2371                output_schema: None,
2372                annotations: None,
2373            },
2374            context_server::types::Tool {
2375                name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
2376                title: None,
2377                description: None,
2378                input_schema: json!({"type": "object", "properties": {}}),
2379                output_schema: None,
2380                annotations: None,
2381            },
2382            context_server::types::Tool {
2383                name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1),
2384                title: None,
2385                description: None,
2386                input_schema: json!({"type": "object", "properties": {}}),
2387                output_schema: None,
2388                annotations: None,
2389            },
2390        ],
2391        &context_server_store,
2392        cx,
2393    );
2394
2395    // Server with spaces in name - tests snake_case conversion for API compatibility
2396    let _server4_calls = setup_context_server(
2397        "Azure DevOps",
2398        vec![context_server::types::Tool {
2399            name: "echo".into(), // Also conflicts - will be disambiguated as azure_dev_ops_echo
2400            title: None,
2401            description: None,
2402            input_schema: serde_json::to_value(EchoTool::input_schema(
2403                LanguageModelToolSchemaFormat::JsonSchema,
2404            ))
2405            .unwrap(),
2406            output_schema: None,
2407            annotations: None,
2408        }],
2409        &context_server_store,
2410        cx,
2411    );
2412
2413    thread
2414        .update(cx, |thread, cx| {
2415            thread.send(ClientUserMessageId::new(), ["Go"], cx)
2416        })
2417        .unwrap();
2418    cx.run_until_parked();
2419    let completion = fake_model.pending_completions().pop().unwrap();
2420    assert_eq!(
2421        tool_names_for_completion(&completion),
2422        vec![
2423            "azure_dev_ops_echo",
2424            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2425            "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
2426            "delay",
2427            "echo",
2428            "infinite",
2429            "tool_requiring_permission",
2430            "unique_tool_1",
2431            "unique_tool_2",
2432            "word_list",
2433            "xxx_echo",
2434            "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2435            "yyy_echo",
2436            "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2437        ]
2438    );
2439}
2440
2441#[gpui::test]
2442#[cfg_attr(not(feature = "e2e"), ignore)]
2443async fn test_cancellation(cx: &mut TestAppContext) {
2444    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
2445
2446    let mut events = thread
2447        .update(cx, |thread, cx| {
2448            thread.add_tool(InfiniteTool);
2449            thread.add_tool(EchoTool);
2450            thread.send(
2451                ClientUserMessageId::new(),
2452                ["Call the echo tool, then call the infinite tool, then explain their output"],
2453                cx,
2454            )
2455        })
2456        .unwrap();
2457
2458    // Wait until both tools are called.
2459    let mut expected_tools = vec!["Echo", "Infinite Tool"];
2460    let mut echo_id = None;
2461    let mut echo_completed = false;
2462    while let Some(event) = events.next().await {
2463        match event.unwrap() {
2464            ThreadEvent::ToolCall(tool_call) => {
2465                assert_eq!(tool_call.title, expected_tools.remove(0));
2466                if tool_call.title == "Echo" {
2467                    echo_id = Some(tool_call.tool_call_id);
2468                }
2469            }
2470            ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
2471                acp::ToolCallUpdate {
2472                    tool_call_id,
2473                    fields:
2474                        acp::ToolCallUpdateFields {
2475                            status: Some(acp::ToolCallStatus::Completed),
2476                            ..
2477                        },
2478                    ..
2479                },
2480            )) if Some(&tool_call_id) == echo_id.as_ref() => {
2481                echo_completed = true;
2482            }
2483            _ => {}
2484        }
2485
2486        if expected_tools.is_empty() && echo_completed {
2487            break;
2488        }
2489    }
2490
2491    // Cancel the current send and ensure that the event stream is closed, even
2492    // if one of the tools is still running.
2493    thread.update(cx, |thread, cx| thread.cancel(cx)).await;
2494    let events = events.collect::<Vec<_>>().await;
2495    let last_event = events.last();
2496    assert!(
2497        matches!(
2498            last_event,
2499            Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
2500        ),
2501        "unexpected event {last_event:?}"
2502    );
2503
2504    // Ensure we can still send a new message after cancellation.
2505    let events = thread
2506        .update(cx, |thread, cx| {
2507            thread.send(
2508                ClientUserMessageId::new(),
2509                ["Testing: reply with 'Hello' then stop."],
2510                cx,
2511            )
2512        })
2513        .unwrap()
2514        .collect::<Vec<_>>()
2515        .await;
2516    thread.update(cx, |thread, _cx| {
2517        let message = thread.last_received_or_pending_message().unwrap();
2518        let agent_message = message.as_agent_message().unwrap();
2519        assert_eq!(
2520            agent_message.content,
2521            vec![AgentMessageContent::Text("Hello".to_string())]
2522        );
2523    });
2524    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
2525}
2526
2527#[gpui::test]
2528async fn test_terminal_tool_cancellation_captures_output(cx: &mut TestAppContext) {
2529    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
2530    always_allow_tools(cx);
2531    disable_sandboxing(cx);
2532    let fake_model = model.as_fake();
2533
2534    let environment = Rc::new(cx.update(|cx| {
2535        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
2536    }));
2537    let handle = environment.terminal_handle.clone().unwrap();
2538
2539    let mut events = thread
2540        .update(cx, |thread, cx| {
2541            thread.add_tool(crate::TerminalTool::new(
2542                thread.project().clone(),
2543                environment,
2544            ));
2545            thread.send(ClientUserMessageId::new(), ["run a command"], cx)
2546        })
2547        .unwrap();
2548
2549    cx.run_until_parked();
2550
2551    // Simulate the model calling the terminal tool
2552    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2553        LanguageModelToolUse {
2554            id: "terminal_tool_1".into(),
2555            name: TerminalTool::NAME.into(),
2556            raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
2557            input: language_model::LanguageModelToolUseInput::Json(
2558                json!({"command": "sleep 1000", "cd": "."}),
2559            ),
2560            is_input_complete: true,
2561            thought_signature: None,
2562        },
2563    ));
2564    fake_model.end_last_completion_stream();
2565
2566    // Wait for the terminal tool to start running
2567    wait_for_terminal_tool_started(&mut events, cx).await;
2568
2569    // Cancel the thread while the terminal is running
2570    thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
2571
2572    // Collect remaining events, driving the executor to let cancellation complete
2573    let remaining_events = collect_events_until_stop(&mut events, cx).await;
2574
2575    // Verify the terminal was killed
2576    assert!(
2577        handle.was_killed(),
2578        "expected terminal handle to be killed on cancellation"
2579    );
2580
2581    // Verify we got a cancellation stop event
2582    assert_eq!(
2583        stop_events(remaining_events),
2584        vec![acp::StopReason::Cancelled],
2585    );
2586
2587    // Verify the tool result contains the terminal output, not just "Tool canceled by user"
2588    thread.update(cx, |thread, _cx| {
2589        let message = thread.last_received_or_pending_message().unwrap();
2590        let agent_message = message.as_agent_message().unwrap();
2591
2592        let tool_use = agent_message
2593            .content
2594            .iter()
2595            .find_map(|content| match content {
2596                AgentMessageContent::ToolUse(tool_use) => Some(tool_use),
2597                _ => None,
2598            })
2599            .expect("expected tool use in agent message");
2600
2601        let tool_result = agent_message
2602            .tool_results
2603            .get(&tool_use.id)
2604            .expect("expected tool result");
2605
2606        let result_text = tool_result.text_contents();
2607
2608        // "partial output" comes from FakeTerminalHandle's output field
2609        assert!(
2610            result_text.contains("partial output"),
2611            "expected tool result to contain terminal output, got: {result_text}"
2612        );
2613        // Match the actual format from process_content in terminal_tool.rs
2614        assert!(
2615            result_text.contains("The user stopped this command"),
2616            "expected tool result to indicate user stopped, got: {result_text}"
2617        );
2618    });
2619
2620    // Verify we can send a new message after cancellation
2621    verify_thread_recovery(&thread, &fake_model, cx).await;
2622}
2623
2624#[gpui::test]
2625async fn test_cancellation_aware_tool_responds_to_cancellation(cx: &mut TestAppContext) {
2626    // This test verifies that tools which properly handle cancellation via
2627    // `event_stream.cancelled_by_user()` (like edit_file_tool) respond promptly
2628    // to cancellation and report that they were cancelled.
2629    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
2630    always_allow_tools(cx);
2631    let fake_model = model.as_fake();
2632
2633    let (tool, was_cancelled) = CancellationAwareTool::new();
2634
2635    let mut events = thread
2636        .update(cx, |thread, cx| {
2637            thread.add_tool(tool);
2638            thread.send(
2639                ClientUserMessageId::new(),
2640                ["call the cancellation aware tool"],
2641                cx,
2642            )
2643        })
2644        .unwrap();
2645
2646    cx.run_until_parked();
2647
2648    // Simulate the model calling the cancellation-aware tool
2649    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2650        LanguageModelToolUse {
2651            id: "cancellation_aware_1".into(),
2652            name: "cancellation_aware".into(),
2653            raw_input: r#"{}"#.into(),
2654            input: language_model::LanguageModelToolUseInput::Json(json!({})),
2655            is_input_complete: true,
2656            thought_signature: None,
2657        },
2658    ));
2659    fake_model.end_last_completion_stream();
2660
2661    cx.run_until_parked();
2662
2663    // Wait for the tool call to be reported
2664    let mut tool_started = false;
2665    let deadline = cx.executor().num_cpus() * 100;
2666    for _ in 0..deadline {
2667        cx.run_until_parked();
2668
2669        while let Some(Some(event)) = events.next().now_or_never() {
2670            if let Ok(ThreadEvent::ToolCall(tool_call)) = &event {
2671                if tool_call.title == "Cancellation Aware Tool" {
2672                    tool_started = true;
2673                    break;
2674                }
2675            }
2676        }
2677
2678        if tool_started {
2679            break;
2680        }
2681
2682        cx.background_executor
2683            .timer(Duration::from_millis(10))
2684            .await;
2685    }
2686    assert!(tool_started, "expected cancellation aware tool to start");
2687
2688    // Cancel the thread and wait for it to complete
2689    let cancel_task = thread.update(cx, |thread, cx| thread.cancel(cx));
2690
2691    // The cancel task should complete promptly because the tool handles cancellation
2692    let timeout = cx.background_executor.timer(Duration::from_secs(5));
2693    futures::select! {
2694        _ = cancel_task.fuse() => {}
2695        _ = timeout.fuse() => {
2696            panic!("cancel task timed out - tool did not respond to cancellation");
2697        }
2698    }
2699
2700    // Verify the tool detected cancellation via its flag
2701    assert!(
2702        was_cancelled.load(std::sync::atomic::Ordering::SeqCst),
2703        "tool should have detected cancellation via event_stream.cancelled_by_user()"
2704    );
2705
2706    // Collect remaining events
2707    let remaining_events = collect_events_until_stop(&mut events, cx).await;
2708
2709    // Verify we got a cancellation stop event
2710    assert_eq!(
2711        stop_events(remaining_events),
2712        vec![acp::StopReason::Cancelled],
2713    );
2714
2715    // Verify we can send a new message after cancellation
2716    verify_thread_recovery(&thread, &fake_model, cx).await;
2717}
2718
2719/// Helper to verify thread can recover after cancellation by sending a simple message.
2720async fn verify_thread_recovery(
2721    thread: &Entity<Thread>,
2722    fake_model: &FakeLanguageModel,
2723    cx: &mut TestAppContext,
2724) {
2725    let events = thread
2726        .update(cx, |thread, cx| {
2727            thread.send(
2728                ClientUserMessageId::new(),
2729                ["Testing: reply with 'Hello' then stop."],
2730                cx,
2731            )
2732        })
2733        .unwrap();
2734    cx.run_until_parked();
2735    fake_model.send_last_completion_stream_text_chunk("Hello");
2736    fake_model
2737        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
2738    fake_model.end_last_completion_stream();
2739
2740    let events = events.collect::<Vec<_>>().await;
2741    thread.update(cx, |thread, _cx| {
2742        let message = thread.last_received_or_pending_message().unwrap();
2743        let agent_message = message.as_agent_message().unwrap();
2744        assert_eq!(
2745            agent_message.content,
2746            vec![AgentMessageContent::Text("Hello".to_string())]
2747        );
2748    });
2749    assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
2750}
2751
2752/// Waits for a terminal tool to start by watching for a ToolCallUpdate with terminal content.
2753async fn wait_for_terminal_tool_started(
2754    events: &mut mpsc::UnboundedReceiver<Result<ThreadEvent>>,
2755    cx: &mut TestAppContext,
2756) {
2757    let deadline = cx.executor().num_cpus() * 100; // Scale with available parallelism
2758    for _ in 0..deadline {
2759        cx.run_until_parked();
2760
2761        while let Some(Some(event)) = events.next().now_or_never() {
2762            if let Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
2763                update,
2764            ))) = &event
2765            {
2766                if update.fields.content.as_ref().is_some_and(|content| {
2767                    content
2768                        .iter()
2769                        .any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
2770                }) {
2771                    return;
2772                }
2773            }
2774        }
2775
2776        cx.background_executor
2777            .timer(Duration::from_millis(10))
2778            .await;
2779    }
2780    panic!("terminal tool did not start within the expected time");
2781}
2782
2783/// Collects events until a Stop event is received, driving the executor to completion.
2784async fn collect_events_until_stop(
2785    events: &mut mpsc::UnboundedReceiver<Result<ThreadEvent>>,
2786    cx: &mut TestAppContext,
2787) -> Vec<Result<ThreadEvent>> {
2788    let mut collected = Vec::new();
2789    let deadline = cx.executor().num_cpus() * 200;
2790
2791    for _ in 0..deadline {
2792        cx.executor().advance_clock(Duration::from_millis(10));
2793        cx.run_until_parked();
2794
2795        while let Some(Some(event)) = events.next().now_or_never() {
2796            let is_stop = matches!(&event, Ok(ThreadEvent::Stop(_)));
2797            collected.push(event);
2798            if is_stop {
2799                return collected;
2800            }
2801        }
2802    }
2803    panic!(
2804        "did not receive Stop event within the expected time; collected {} events",
2805        collected.len()
2806    );
2807}
2808
2809#[gpui::test]
2810async fn test_truncate_while_terminal_tool_running(cx: &mut TestAppContext) {
2811    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
2812    always_allow_tools(cx);
2813    disable_sandboxing(cx);
2814    let fake_model = model.as_fake();
2815
2816    let environment = Rc::new(cx.update(|cx| {
2817        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
2818    }));
2819    let handle = environment.terminal_handle.clone().unwrap();
2820
2821    let message_id = ClientUserMessageId::new();
2822    let mut events = thread
2823        .update(cx, |thread, cx| {
2824            thread.add_tool(crate::TerminalTool::new(
2825                thread.project().clone(),
2826                environment,
2827            ));
2828            thread.send(message_id.clone(), ["run a command"], cx)
2829        })
2830        .unwrap();
2831
2832    cx.run_until_parked();
2833
2834    // Simulate the model calling the terminal tool
2835    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2836        LanguageModelToolUse {
2837            id: "terminal_tool_1".into(),
2838            name: TerminalTool::NAME.into(),
2839            raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
2840            input: language_model::LanguageModelToolUseInput::Json(
2841                json!({"command": "sleep 1000", "cd": "."}),
2842            ),
2843            is_input_complete: true,
2844            thought_signature: None,
2845        },
2846    ));
2847    fake_model.end_last_completion_stream();
2848
2849    // Wait for the terminal tool to start running
2850    wait_for_terminal_tool_started(&mut events, cx).await;
2851
2852    // Truncate the thread while the terminal is running
2853    thread
2854        .update(cx, |thread, cx| thread.truncate(message_id, cx))
2855        .unwrap();
2856
2857    // Drive the executor to let cancellation complete
2858    let _ = collect_events_until_stop(&mut events, cx).await;
2859
2860    // Verify the terminal was killed
2861    assert!(
2862        handle.was_killed(),
2863        "expected terminal handle to be killed on truncate"
2864    );
2865
2866    // Verify the thread is empty after truncation
2867    thread.update(cx, |thread, _cx| {
2868        assert_eq!(
2869            thread.to_markdown(),
2870            "",
2871            "expected thread to be empty after truncating the only message"
2872        );
2873    });
2874
2875    // Verify we can send a new message after truncation
2876    verify_thread_recovery(&thread, &fake_model, cx).await;
2877}
2878
2879#[gpui::test]
2880async fn test_cancel_multiple_concurrent_terminal_tools(cx: &mut TestAppContext) {
2881    // Tests that cancellation properly kills all running terminal tools when multiple are active.
2882    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
2883    always_allow_tools(cx);
2884    disable_sandboxing(cx);
2885    let fake_model = model.as_fake();
2886
2887    let environment = Rc::new(MultiTerminalEnvironment::new());
2888
2889    let mut events = thread
2890        .update(cx, |thread, cx| {
2891            thread.add_tool(crate::TerminalTool::new(
2892                thread.project().clone(),
2893                environment.clone(),
2894            ));
2895            thread.send(ClientUserMessageId::new(), ["run multiple commands"], cx)
2896        })
2897        .unwrap();
2898
2899    cx.run_until_parked();
2900
2901    // Simulate the model calling two terminal tools
2902    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2903        LanguageModelToolUse {
2904            id: "terminal_tool_1".into(),
2905            name: TerminalTool::NAME.into(),
2906            raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
2907            input: language_model::LanguageModelToolUseInput::Json(
2908                json!({"command": "sleep 1000", "cd": "."}),
2909            ),
2910            is_input_complete: true,
2911            thought_signature: None,
2912        },
2913    ));
2914    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
2915        LanguageModelToolUse {
2916            id: "terminal_tool_2".into(),
2917            name: TerminalTool::NAME.into(),
2918            raw_input: r#"{"command": "sleep 2000", "cd": "."}"#.into(),
2919            input: language_model::LanguageModelToolUseInput::Json(
2920                json!({"command": "sleep 2000", "cd": "."}),
2921            ),
2922            is_input_complete: true,
2923            thought_signature: None,
2924        },
2925    ));
2926    fake_model.end_last_completion_stream();
2927
2928    // Wait for both terminal tools to start by counting terminal content updates
2929    let mut terminals_started = 0;
2930    let deadline = cx.executor().num_cpus() * 100;
2931    for _ in 0..deadline {
2932        cx.run_until_parked();
2933
2934        while let Some(Some(event)) = events.next().now_or_never() {
2935            if let Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
2936                update,
2937            ))) = &event
2938            {
2939                if update.fields.content.as_ref().is_some_and(|content| {
2940                    content
2941                        .iter()
2942                        .any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
2943                }) {
2944                    terminals_started += 1;
2945                    if terminals_started >= 2 {
2946                        break;
2947                    }
2948                }
2949            }
2950        }
2951        if terminals_started >= 2 {
2952            break;
2953        }
2954
2955        cx.background_executor
2956            .timer(Duration::from_millis(10))
2957            .await;
2958    }
2959    assert!(
2960        terminals_started >= 2,
2961        "expected 2 terminal tools to start, got {terminals_started}"
2962    );
2963
2964    // Cancel the thread while both terminals are running
2965    thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
2966
2967    // Collect remaining events
2968    let remaining_events = collect_events_until_stop(&mut events, cx).await;
2969
2970    // Verify both terminal handles were killed
2971    let handles = environment.handles();
2972    assert_eq!(
2973        handles.len(),
2974        2,
2975        "expected 2 terminal handles to be created"
2976    );
2977    assert!(
2978        handles[0].was_killed(),
2979        "expected first terminal handle to be killed on cancellation"
2980    );
2981    assert!(
2982        handles[1].was_killed(),
2983        "expected second terminal handle to be killed on cancellation"
2984    );
2985
2986    // Verify we got a cancellation stop event
2987    assert_eq!(
2988        stop_events(remaining_events),
2989        vec![acp::StopReason::Cancelled],
2990    );
2991}
2992
2993#[gpui::test]
2994async fn test_terminal_tool_stopped_via_terminal_card_button(cx: &mut TestAppContext) {
2995    // Tests that clicking the stop button on the terminal card (as opposed to the main
2996    // cancel button) properly reports user stopped via the was_stopped_by_user path.
2997    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
2998    always_allow_tools(cx);
2999    disable_sandboxing(cx);
3000    let fake_model = model.as_fake();
3001
3002    let environment = Rc::new(cx.update(|cx| {
3003        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
3004    }));
3005    let handle = environment.terminal_handle.clone().unwrap();
3006
3007    let mut events = thread
3008        .update(cx, |thread, cx| {
3009            thread.add_tool(crate::TerminalTool::new(
3010                thread.project().clone(),
3011                environment,
3012            ));
3013            thread.send(ClientUserMessageId::new(), ["run a command"], cx)
3014        })
3015        .unwrap();
3016
3017    cx.run_until_parked();
3018
3019    // Simulate the model calling the terminal tool
3020    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
3021        LanguageModelToolUse {
3022            id: "terminal_tool_1".into(),
3023            name: TerminalTool::NAME.into(),
3024            raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
3025            input: language_model::LanguageModelToolUseInput::Json(
3026                json!({"command": "sleep 1000", "cd": "."}),
3027            ),
3028            is_input_complete: true,
3029            thought_signature: None,
3030        },
3031    ));
3032    fake_model.end_last_completion_stream();
3033
3034    // Wait for the terminal tool to start running
3035    wait_for_terminal_tool_started(&mut events, cx).await;
3036
3037    // Simulate user clicking stop on the terminal card itself.
3038    // This sets the flag and signals exit (simulating what the real UI would do).
3039    handle.set_stopped_by_user(true);
3040    handle.killed.store(true, Ordering::SeqCst);
3041    handle.signal_exit();
3042
3043    // Wait for the tool to complete
3044    cx.run_until_parked();
3045
3046    // The thread continues after tool completion - simulate the model ending its turn
3047    fake_model
3048        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3049    fake_model.end_last_completion_stream();
3050
3051    // Collect remaining events
3052    let remaining_events = collect_events_until_stop(&mut events, cx).await;
3053
3054    // Verify we got an EndTurn (not Cancelled, since we didn't cancel the thread)
3055    assert_eq!(
3056        stop_events(remaining_events),
3057        vec![acp::StopReason::EndTurn],
3058    );
3059
3060    // Verify the tool result indicates user stopped
3061    thread.update(cx, |thread, _cx| {
3062        let message = thread.last_received_or_pending_message().unwrap();
3063        let agent_message = message.as_agent_message().unwrap();
3064
3065        let tool_use = agent_message
3066            .content
3067            .iter()
3068            .find_map(|content| match content {
3069                AgentMessageContent::ToolUse(tool_use) => Some(tool_use),
3070                _ => None,
3071            })
3072            .expect("expected tool use in agent message");
3073
3074        let tool_result = agent_message
3075            .tool_results
3076            .get(&tool_use.id)
3077            .expect("expected tool result");
3078
3079        let result_text = tool_result.text_contents();
3080
3081        assert!(
3082            result_text.contains("The user stopped this command"),
3083            "expected tool result to indicate user stopped, got: {result_text}"
3084        );
3085    });
3086}
3087
3088#[gpui::test]
3089async fn test_terminal_tool_timeout_expires(cx: &mut TestAppContext) {
3090    // Tests that when a timeout is configured and expires, the tool result indicates timeout.
3091    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3092    always_allow_tools(cx);
3093    disable_sandboxing(cx);
3094    let fake_model = model.as_fake();
3095
3096    let environment = Rc::new(cx.update(|cx| {
3097        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
3098    }));
3099    let handle = environment.terminal_handle.clone().unwrap();
3100
3101    let mut events = thread
3102        .update(cx, |thread, cx| {
3103            thread.add_tool(crate::TerminalTool::new(
3104                thread.project().clone(),
3105                environment,
3106            ));
3107            thread.send(
3108                ClientUserMessageId::new(),
3109                ["run a command with timeout"],
3110                cx,
3111            )
3112        })
3113        .unwrap();
3114
3115    cx.run_until_parked();
3116
3117    // Simulate the model calling the terminal tool with a short timeout
3118    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
3119        LanguageModelToolUse {
3120            id: "terminal_tool_1".into(),
3121            name: TerminalTool::NAME.into(),
3122            raw_input: r#"{"command": "sleep 1000", "cd": ".", "timeout_ms": 100}"#.into(),
3123            input: language_model::LanguageModelToolUseInput::Json(
3124                json!({"command": "sleep 1000", "cd": ".", "timeout_ms": 100}),
3125            ),
3126            is_input_complete: true,
3127            thought_signature: None,
3128        },
3129    ));
3130    fake_model.end_last_completion_stream();
3131
3132    // Wait for the terminal tool to start running
3133    wait_for_terminal_tool_started(&mut events, cx).await;
3134
3135    // Advance clock past the timeout
3136    cx.executor().advance_clock(Duration::from_millis(200));
3137    cx.run_until_parked();
3138
3139    // The thread continues after tool completion - simulate the model ending its turn
3140    fake_model
3141        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3142    fake_model.end_last_completion_stream();
3143
3144    // Collect remaining events
3145    let remaining_events = collect_events_until_stop(&mut events, cx).await;
3146
3147    // Verify the terminal was killed due to timeout
3148    assert!(
3149        handle.was_killed(),
3150        "expected terminal handle to be killed on timeout"
3151    );
3152
3153    // Verify we got an EndTurn (the tool completed, just with timeout)
3154    assert_eq!(
3155        stop_events(remaining_events),
3156        vec![acp::StopReason::EndTurn],
3157    );
3158
3159    // Verify the tool result indicates timeout, not user stopped
3160    thread.update(cx, |thread, _cx| {
3161        let message = thread.last_received_or_pending_message().unwrap();
3162        let agent_message = message.as_agent_message().unwrap();
3163
3164        let tool_use = agent_message
3165            .content
3166            .iter()
3167            .find_map(|content| match content {
3168                AgentMessageContent::ToolUse(tool_use) => Some(tool_use),
3169                _ => None,
3170            })
3171            .expect("expected tool use in agent message");
3172
3173        let tool_result = agent_message
3174            .tool_results
3175            .get(&tool_use.id)
3176            .expect("expected tool result");
3177
3178        let result_text = tool_result.text_contents();
3179
3180        assert!(
3181            result_text.contains("timed out"),
3182            "expected tool result to indicate timeout, got: {result_text}"
3183        );
3184        assert!(
3185            !result_text.contains("The user stopped"),
3186            "tool result should not mention user stopped when it timed out, got: {result_text}"
3187        );
3188    });
3189}
3190
3191#[gpui::test]
3192async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
3193    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3194    let fake_model = model.as_fake();
3195
3196    let events_1 = thread
3197        .update(cx, |thread, cx| {
3198            thread.send(ClientUserMessageId::new(), ["Hello 1"], cx)
3199        })
3200        .unwrap();
3201    cx.run_until_parked();
3202    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
3203    cx.run_until_parked();
3204
3205    let events_2 = thread
3206        .update(cx, |thread, cx| {
3207            thread.send(ClientUserMessageId::new(), ["Hello 2"], cx)
3208        })
3209        .unwrap();
3210    cx.run_until_parked();
3211    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
3212    fake_model
3213        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3214    fake_model.end_last_completion_stream();
3215
3216    let events_1 = events_1.collect::<Vec<_>>().await;
3217    assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]);
3218    let events_2 = events_2.collect::<Vec<_>>().await;
3219    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
3220}
3221
3222#[gpui::test]
3223async fn test_retry_cancelled_promptly_on_new_send(cx: &mut TestAppContext) {
3224    // Regression test: when a completion fails with a retryable error (e.g. upstream 500),
3225    // the retry loop waits on a timer. If the user switches models and sends a new message
3226    // during that delay, the old turn should exit immediately instead of retrying with the
3227    // stale model.
3228    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3229    let model_a = model.as_fake();
3230
3231    // Start a turn with model_a.
3232    let events_1 = thread
3233        .update(cx, |thread, cx| {
3234            thread.send(ClientUserMessageId::new(), ["Hello"], cx)
3235        })
3236        .unwrap();
3237    cx.run_until_parked();
3238    assert_eq!(model_a.completion_count(), 1);
3239
3240    // Model returns a retryable upstream 500. The turn enters the retry delay.
3241    model_a.send_last_completion_stream_error(
3242        LanguageModelCompletionError::UpstreamProviderError {
3243            message: "Internal server error".to_string(),
3244            status: http_client::StatusCode::INTERNAL_SERVER_ERROR,
3245            retry_after: None,
3246        },
3247    );
3248    model_a.end_last_completion_stream();
3249    cx.run_until_parked();
3250
3251    // The old completion was consumed; model_a has no pending requests yet because the
3252    // retry timer hasn't fired.
3253    assert_eq!(model_a.completion_count(), 0);
3254
3255    // Switch to model_b and send a new message. This cancels the old turn.
3256    let model_b = Arc::new(FakeLanguageModel::with_id_and_thinking(
3257        "fake", "model-b", "Model B", false,
3258    ));
3259    thread.update(cx, |thread, cx| {
3260        thread.set_model(model_b.clone(), cx);
3261    });
3262    let events_2 = thread
3263        .update(cx, |thread, cx| {
3264            thread.send(ClientUserMessageId::new(), ["Continue"], cx)
3265        })
3266        .unwrap();
3267    cx.run_until_parked();
3268
3269    // model_b should have received its completion request.
3270    assert_eq!(model_b.as_fake().completion_count(), 1);
3271
3272    // Advance the clock well past the retry delay (BASE_RETRY_DELAY = 5s).
3273    cx.executor().advance_clock(Duration::from_secs(10));
3274    cx.run_until_parked();
3275
3276    // model_a must NOT have received another completion request — the cancelled turn
3277    // should have exited during the retry delay rather than retrying with the old model.
3278    assert_eq!(
3279        model_a.completion_count(),
3280        0,
3281        "old model should not receive a retry request after cancellation"
3282    );
3283
3284    // Complete model_b's turn.
3285    model_b
3286        .as_fake()
3287        .send_last_completion_stream_text_chunk("Done!");
3288    model_b
3289        .as_fake()
3290        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3291    model_b.as_fake().end_last_completion_stream();
3292
3293    let events_1 = events_1.collect::<Vec<_>>().await;
3294    assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]);
3295
3296    let events_2 = events_2.collect::<Vec<_>>().await;
3297    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
3298}
3299
3300#[gpui::test]
3301async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
3302    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3303    let fake_model = model.as_fake();
3304
3305    let events_1 = thread
3306        .update(cx, |thread, cx| {
3307            thread.send(ClientUserMessageId::new(), ["Hello 1"], cx)
3308        })
3309        .unwrap();
3310    cx.run_until_parked();
3311    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
3312    fake_model
3313        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3314    fake_model.end_last_completion_stream();
3315    let events_1 = events_1.collect::<Vec<_>>().await;
3316
3317    let events_2 = thread
3318        .update(cx, |thread, cx| {
3319            thread.send(ClientUserMessageId::new(), ["Hello 2"], cx)
3320        })
3321        .unwrap();
3322    cx.run_until_parked();
3323    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
3324    fake_model
3325        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
3326    fake_model.end_last_completion_stream();
3327    let events_2 = events_2.collect::<Vec<_>>().await;
3328
3329    assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]);
3330    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
3331}
3332
3333#[gpui::test]
3334async fn test_refusal(cx: &mut TestAppContext) {
3335    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3336    let fake_model = model.as_fake();
3337
3338    let events = thread
3339        .update(cx, |thread, cx| {
3340            thread.send(ClientUserMessageId::new(), ["Hello"], cx)
3341        })
3342        .unwrap();
3343    cx.run_until_parked();
3344    thread.read_with(cx, |thread, _| {
3345        assert_eq!(
3346            thread.to_markdown(),
3347            indoc! {"
3348                ## User
3349
3350                Hello
3351            "}
3352        );
3353    });
3354
3355    fake_model.send_last_completion_stream_text_chunk("Hey!");
3356    cx.run_until_parked();
3357    thread.read_with(cx, |thread, _| {
3358        assert_eq!(
3359            thread.to_markdown(),
3360            indoc! {"
3361                ## User
3362
3363                Hello
3364
3365                ## Assistant
3366
3367                Hey!
3368            "}
3369        );
3370    });
3371
3372    // If the model refuses to continue, the thread should remove all the messages after the last user message.
3373    fake_model
3374        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::Refusal));
3375    let events = events.collect::<Vec<_>>().await;
3376    assert_eq!(stop_events(events), vec![acp::StopReason::Refusal]);
3377    thread.read_with(cx, |thread, _| {
3378        assert_eq!(thread.to_markdown(), "");
3379    });
3380}
3381
3382#[gpui::test]
3383async fn test_truncate_first_message(cx: &mut TestAppContext) {
3384    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3385    let fake_model = model.as_fake();
3386
3387    let message_id = ClientUserMessageId::new();
3388    thread
3389        .update(cx, |thread, cx| {
3390            thread.send(message_id.clone(), ["Hello"], cx)
3391        })
3392        .unwrap();
3393    cx.run_until_parked();
3394    thread.read_with(cx, |thread, _| {
3395        assert_eq!(
3396            thread.to_markdown(),
3397            indoc! {"
3398                ## User
3399
3400                Hello
3401            "}
3402        );
3403        assert_eq!(thread.latest_token_usage(), None);
3404    });
3405
3406    fake_model.send_last_completion_stream_text_chunk("Hey!");
3407    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3408        language_model::TokenUsage {
3409            input_tokens: 32_000,
3410            output_tokens: 16_000,
3411            cache_creation_input_tokens: 0,
3412            cache_read_input_tokens: 0,
3413        },
3414    ));
3415    cx.run_until_parked();
3416    thread.read_with(cx, |thread, _| {
3417        assert_eq!(
3418            thread.to_markdown(),
3419            indoc! {"
3420                ## User
3421
3422                Hello
3423
3424                ## Assistant
3425
3426                Hey!
3427            "}
3428        );
3429        assert_eq!(
3430            thread.latest_token_usage(),
3431            Some(acp_thread::TokenUsage {
3432                used_tokens: 32_000 + 16_000,
3433                max_tokens: 1_000_000,
3434                max_output_tokens: None,
3435                input_tokens: 32_000,
3436                output_tokens: 16_000,
3437            })
3438        );
3439    });
3440
3441    thread
3442        .update(cx, |thread, cx| thread.truncate(message_id, cx))
3443        .unwrap();
3444    cx.run_until_parked();
3445    thread.read_with(cx, |thread, _| {
3446        assert_eq!(thread.to_markdown(), "");
3447        assert_eq!(thread.latest_token_usage(), None);
3448    });
3449
3450    // Ensure we can still send a new message after truncation.
3451    thread
3452        .update(cx, |thread, cx| {
3453            thread.send(ClientUserMessageId::new(), ["Hi"], cx)
3454        })
3455        .unwrap();
3456    thread.update(cx, |thread, _cx| {
3457        assert_eq!(
3458            thread.to_markdown(),
3459            indoc! {"
3460                ## User
3461
3462                Hi
3463            "}
3464        );
3465    });
3466    cx.run_until_parked();
3467    fake_model.send_last_completion_stream_text_chunk("Ahoy!");
3468    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3469        language_model::TokenUsage {
3470            input_tokens: 40_000,
3471            output_tokens: 20_000,
3472            cache_creation_input_tokens: 0,
3473            cache_read_input_tokens: 0,
3474        },
3475    ));
3476    cx.run_until_parked();
3477    thread.read_with(cx, |thread, _| {
3478        assert_eq!(
3479            thread.to_markdown(),
3480            indoc! {"
3481                ## User
3482
3483                Hi
3484
3485                ## Assistant
3486
3487                Ahoy!
3488            "}
3489        );
3490
3491        assert_eq!(
3492            thread.latest_token_usage(),
3493            Some(acp_thread::TokenUsage {
3494                used_tokens: 40_000 + 20_000,
3495                max_tokens: 1_000_000,
3496                max_output_tokens: None,
3497                input_tokens: 40_000,
3498                output_tokens: 20_000,
3499            })
3500        );
3501    });
3502}
3503
3504#[gpui::test]
3505async fn test_latest_token_usage_counts_cached_input_tokens(cx: &mut TestAppContext) {
3506    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3507    let fake_model = model.as_fake();
3508
3509    let message_1_id = ClientUserMessageId::new();
3510    thread
3511        .update(cx, |thread, cx| {
3512            thread.send(message_1_id, ["Message 1"], cx)
3513        })
3514        .unwrap();
3515    cx.run_until_parked();
3516
3517    fake_model.send_last_completion_stream_text_chunk("Response 1");
3518    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3519        language_model::TokenUsage {
3520            input_tokens: 100,
3521            output_tokens: 50,
3522            cache_creation_input_tokens: 25,
3523            cache_read_input_tokens: 75,
3524        },
3525    ));
3526    fake_model.end_last_completion_stream();
3527    cx.run_until_parked();
3528
3529    thread.read_with(cx, |thread, _| {
3530        assert_eq!(
3531            thread.latest_token_usage(),
3532            Some(acp_thread::TokenUsage {
3533                used_tokens: 250,
3534                max_tokens: 1_000_000,
3535                max_output_tokens: None,
3536                input_tokens: 200,
3537                output_tokens: 50,
3538            })
3539        );
3540    });
3541
3542    let message_2_id = ClientUserMessageId::new();
3543    thread
3544        .update(cx, |thread, cx| {
3545            thread.send(message_2_id.clone(), ["Message 2"], cx)
3546        })
3547        .unwrap();
3548    cx.run_until_parked();
3549
3550    thread.read_with(cx, |thread, _| {
3551        assert_eq!(thread.tokens_before_message(&message_2_id), Some(200));
3552    });
3553}
3554
3555#[gpui::test]
3556async fn test_prompt_too_large_marks_token_usage_exceeded(cx: &mut TestAppContext) {
3557    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3558    let fake_model = model.as_fake();
3559
3560    thread
3561        .update(cx, |thread, cx| {
3562            thread.send(ClientUserMessageId::new(), ["Message 1"], cx)
3563        })
3564        .unwrap();
3565    cx.run_until_parked();
3566
3567    fake_model.send_last_completion_stream_text_chunk("Response 1");
3568    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3569        language_model::TokenUsage {
3570            input_tokens: 100,
3571            output_tokens: 50,
3572            ..Default::default()
3573        },
3574    ));
3575    fake_model.end_last_completion_stream();
3576    cx.run_until_parked();
3577
3578    thread.read_with(cx, |thread, _| {
3579        assert_eq!(
3580            thread.latest_token_usage().unwrap().ratio(),
3581            acp_thread::TokenUsageRatio::Normal
3582        );
3583    });
3584
3585    thread
3586        .update(cx, |thread, cx| {
3587            thread.send(ClientUserMessageId::new(), ["Message 2"], cx)
3588        })
3589        .unwrap();
3590    cx.run_until_parked();
3591
3592    fake_model.send_last_completion_stream_error(LanguageModelCompletionError::PromptTooLarge {
3593        tokens: None,
3594    });
3595    fake_model.end_last_completion_stream();
3596    cx.run_until_parked();
3597
3598    thread.read_with(cx, |thread, _| {
3599        let usage = thread.latest_token_usage().unwrap();
3600        assert_eq!(usage.used_tokens, 1_000_000);
3601        assert_eq!(usage.max_tokens, 1_000_000);
3602        assert_eq!(usage.ratio(), acp_thread::TokenUsageRatio::Exceeded);
3603    });
3604}
3605
3606#[gpui::test]
3607async fn test_prompt_too_large_uses_reported_token_count(cx: &mut TestAppContext) {
3608    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3609    let fake_model = model.as_fake();
3610
3611    thread
3612        .update(cx, |thread, cx| {
3613            thread.send(ClientUserMessageId::new(), ["Message 1"], cx)
3614        })
3615        .unwrap();
3616    cx.run_until_parked();
3617
3618    fake_model.send_last_completion_stream_error(LanguageModelCompletionError::PromptTooLarge {
3619        tokens: Some(1_500_000),
3620    });
3621    fake_model.end_last_completion_stream();
3622    cx.run_until_parked();
3623
3624    thread.read_with(cx, |thread, _| {
3625        let usage = thread.latest_token_usage().unwrap();
3626        assert_eq!(usage.used_tokens, 1_500_000);
3627        assert_eq!(usage.ratio(), acp_thread::TokenUsageRatio::Exceeded);
3628    });
3629}
3630
3631#[gpui::test]
3632async fn test_cumulative_token_usage(cx: &mut TestAppContext) {
3633    let ThreadTest {
3634        model,
3635        thread,
3636        project_context,
3637        ..
3638    } = setup(cx, TestModel::Fake).await;
3639    let fake_model = model.as_fake();
3640
3641    thread
3642        .update(cx, |thread, cx| {
3643            thread.add_tool(EchoTool);
3644            thread.send(ClientUserMessageId::new(), ["Use the echo tool"], cx)
3645        })
3646        .unwrap();
3647    cx.run_until_parked();
3648
3649    // The first request emits two cumulative snapshots; only the final values
3650    // must be counted, exactly once.
3651    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3652        TokenUsage {
3653            input_tokens: 100,
3654            output_tokens: 10,
3655            ..Default::default()
3656        },
3657    ));
3658    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3659        TokenUsage {
3660            input_tokens: 100,
3661            output_tokens: 50,
3662            ..Default::default()
3663        },
3664    ));
3665    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
3666        LanguageModelToolUse {
3667            id: "tool_1".into(),
3668            name: EchoTool::NAME.into(),
3669            raw_input: json!({"text": "hello"}).to_string(),
3670            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
3671            is_input_complete: true,
3672            thought_signature: None,
3673        },
3674    ));
3675    fake_model.end_last_completion_stream();
3676    cx.run_until_parked();
3677
3678    // The second request (after the tool call) is counted in addition to the first.
3679    fake_model.send_last_completion_stream_text_chunk("Done");
3680    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3681        TokenUsage {
3682            input_tokens: 200,
3683            output_tokens: 30,
3684            ..Default::default()
3685        },
3686    ));
3687    fake_model.end_last_completion_stream();
3688    cx.run_until_parked();
3689
3690    let expected = TokenUsage {
3691        input_tokens: 300,
3692        output_tokens: 80,
3693        ..Default::default()
3694    };
3695    thread.read_with(cx, |thread, _| {
3696        assert_eq!(thread.cumulative_token_usage(), expected);
3697    });
3698
3699    let db_thread = thread.read_with(cx, |thread, cx| thread.to_db(cx)).await;
3700    assert_eq!(db_thread.cumulative_token_usage, expected);
3701
3702    cx.update(|cx| {
3703        LanguageModelRegistry::test(cx);
3704    });
3705    let restored = cx.update(|cx| {
3706        let thread = thread.read(cx);
3707        let project = thread.project.clone();
3708        let context_server_registry = thread.context_server_registry.clone();
3709        let templates = thread.templates.clone();
3710        cx.new(|cx| {
3711            Thread::from_db(
3712                acp::SessionId::new("restored"),
3713                db_thread,
3714                project,
3715                project_context.clone(),
3716                context_server_registry,
3717                templates,
3718                cx,
3719            )
3720        })
3721    });
3722    restored.read_with(cx, |thread, _| {
3723        assert_eq!(thread.cumulative_token_usage(), expected);
3724    });
3725}
3726
3727#[gpui::test]
3728async fn test_cumulative_token_usage_keeps_accounted_usage_monotonic(cx: &mut TestAppContext) {
3729    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3730    let fake_model = model.as_fake();
3731
3732    thread
3733        .update(cx, |thread, cx| {
3734            thread.send(ClientUserMessageId::new(), ["hello"], cx)
3735        })
3736        .unwrap();
3737    cx.run_until_parked();
3738
3739    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3740        TokenUsage {
3741            input_tokens: 100,
3742            output_tokens: 10,
3743            ..Default::default()
3744        },
3745    ));
3746    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3747        TokenUsage::default(),
3748    ));
3749    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3750        TokenUsage {
3751            input_tokens: 100,
3752            output_tokens: 50,
3753            ..Default::default()
3754        },
3755    ));
3756    fake_model.end_last_completion_stream();
3757    cx.run_until_parked();
3758
3759    thread.read_with(cx, |thread, _| {
3760        assert_eq!(
3761            thread.cumulative_token_usage(),
3762            TokenUsage {
3763                input_tokens: 100,
3764                output_tokens: 50,
3765                ..Default::default()
3766            }
3767        );
3768    });
3769}
3770
3771#[gpui::test]
3772async fn test_truncate_second_message(cx: &mut TestAppContext) {
3773    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3774    let fake_model = model.as_fake();
3775
3776    thread
3777        .update(cx, |thread, cx| {
3778            thread.send(ClientUserMessageId::new(), ["Message 1"], cx)
3779        })
3780        .unwrap();
3781    cx.run_until_parked();
3782    fake_model.send_last_completion_stream_text_chunk("Message 1 response");
3783    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3784        language_model::TokenUsage {
3785            input_tokens: 32_000,
3786            output_tokens: 16_000,
3787            cache_creation_input_tokens: 0,
3788            cache_read_input_tokens: 0,
3789        },
3790    ));
3791    fake_model.end_last_completion_stream();
3792    cx.run_until_parked();
3793
3794    let assert_first_message_state = |cx: &mut TestAppContext| {
3795        thread.clone().read_with(cx, |thread, _| {
3796            assert_eq!(
3797                thread.to_markdown(),
3798                indoc! {"
3799                    ## User
3800
3801                    Message 1
3802
3803                    ## Assistant
3804
3805                    Message 1 response
3806                "}
3807            );
3808
3809            assert_eq!(
3810                thread.latest_token_usage(),
3811                Some(acp_thread::TokenUsage {
3812                    used_tokens: 32_000 + 16_000,
3813                    max_tokens: 1_000_000,
3814                    max_output_tokens: None,
3815                    input_tokens: 32_000,
3816                    output_tokens: 16_000,
3817                })
3818            );
3819        });
3820    };
3821
3822    assert_first_message_state(cx);
3823
3824    let second_message_id = ClientUserMessageId::new();
3825    thread
3826        .update(cx, |thread, cx| {
3827            thread.send(second_message_id.clone(), ["Message 2"], cx)
3828        })
3829        .unwrap();
3830    cx.run_until_parked();
3831
3832    fake_model.send_last_completion_stream_text_chunk("Message 2 response");
3833    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
3834        language_model::TokenUsage {
3835            input_tokens: 40_000,
3836            output_tokens: 20_000,
3837            cache_creation_input_tokens: 0,
3838            cache_read_input_tokens: 0,
3839        },
3840    ));
3841    fake_model.end_last_completion_stream();
3842    cx.run_until_parked();
3843
3844    thread.read_with(cx, |thread, _| {
3845        assert_eq!(
3846            thread.to_markdown(),
3847            indoc! {"
3848                ## User
3849
3850                Message 1
3851
3852                ## Assistant
3853
3854                Message 1 response
3855
3856                ## User
3857
3858                Message 2
3859
3860                ## Assistant
3861
3862                Message 2 response
3863            "}
3864        );
3865
3866        assert_eq!(
3867            thread.latest_token_usage(),
3868            Some(acp_thread::TokenUsage {
3869                used_tokens: 40_000 + 20_000,
3870                max_tokens: 1_000_000,
3871                max_output_tokens: None,
3872                input_tokens: 40_000,
3873                output_tokens: 20_000,
3874            })
3875        );
3876    });
3877
3878    thread
3879        .update(cx, |thread, cx| thread.truncate(second_message_id, cx))
3880        .unwrap();
3881    cx.run_until_parked();
3882
3883    assert_first_message_state(cx);
3884}
3885
3886#[gpui::test]
3887async fn test_title_generation(cx: &mut TestAppContext) {
3888    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3889    let fake_model = model.as_fake();
3890
3891    let summary_model = Arc::new(FakeLanguageModel::default());
3892    thread.update(cx, |thread, cx| {
3893        thread.set_summarization_model(Some(summary_model.clone()), cx)
3894    });
3895
3896    let send = thread
3897        .update(cx, |thread, cx| {
3898            thread.send(ClientUserMessageId::new(), ["Hello"], cx)
3899        })
3900        .unwrap();
3901    cx.run_until_parked();
3902
3903    fake_model.send_last_completion_stream_text_chunk("Hey!");
3904    fake_model.end_last_completion_stream();
3905    cx.run_until_parked();
3906    thread.read_with(cx, |thread, _| assert_eq!(thread.title(), None));
3907
3908    // Ensure the summary model has been invoked to generate a title.
3909    summary_model.send_last_completion_stream_text_chunk("Hello ");
3910    summary_model.send_last_completion_stream_text_chunk("world\nG");
3911    summary_model.send_last_completion_stream_text_chunk("oodnight Moon");
3912    summary_model.end_last_completion_stream();
3913    send.collect::<Vec<_>>().await;
3914    cx.run_until_parked();
3915    thread.read_with(cx, |thread, _| {
3916        assert_eq!(thread.title(), Some("Hello world".into()))
3917    });
3918
3919    // Send another message, ensuring no title is generated this time.
3920    let send = thread
3921        .update(cx, |thread, cx| {
3922            thread.send(ClientUserMessageId::new(), ["Hello again"], cx)
3923        })
3924        .unwrap();
3925    cx.run_until_parked();
3926    fake_model.send_last_completion_stream_text_chunk("Hey again!");
3927    fake_model.end_last_completion_stream();
3928    cx.run_until_parked();
3929    assert_eq!(summary_model.pending_completions(), Vec::new());
3930    send.collect::<Vec<_>>().await;
3931    thread.read_with(cx, |thread, _| {
3932        assert_eq!(thread.title(), Some("Hello world".into()))
3933    });
3934}
3935
3936#[gpui::test]
3937async fn test_stream_thread_title_keeps_only_first_line(cx: &mut TestAppContext) {
3938    let model = Arc::new(FakeLanguageModel::default());
3939    let request = LanguageModelRequest::default();
3940
3941    let title_task = cx.spawn({
3942        let model = model.clone();
3943        async move |cx| crate::stream_thread_title(model, request, &cx).await
3944    });
3945
3946    cx.run_until_parked();
3947
3948    model.send_last_completion_stream_text_chunk("Hello world\nGoodnight Moon");
3949    model.end_last_completion_stream();
3950
3951    let title = title_task.await.unwrap();
3952    assert_eq!(title, "Hello world");
3953}
3954
3955#[gpui::test]
3956async fn test_stream_thread_title_stops_when_newline_ends_chunk(cx: &mut TestAppContext) {
3957    let model = Arc::new(FakeLanguageModel::default());
3958    let request = LanguageModelRequest::default();
3959
3960    let title_task = cx.spawn({
3961        let model = model.clone();
3962        async move |cx| crate::stream_thread_title(model, request, &cx).await
3963    });
3964
3965    cx.run_until_parked();
3966
3967    model.send_last_completion_stream_text_chunk("Hello world\n");
3968    model.send_last_completion_stream_text_chunk("Goodnight Moon");
3969    model.end_last_completion_stream();
3970
3971    let title = title_task.await.unwrap();
3972    assert_eq!(title, "Hello world");
3973}
3974
3975// `Thread::to_markdown` (live native) and `DbThread::to_markdown` (persisted
3976// native) must stay byte-for-byte identical for the same messages, since both
3977// back the sidebar's native "Open Thread as Markdown" action. This pins that
3978// they share a single rendering path.
3979#[gpui::test]
3980async fn test_db_thread_markdown_matches_live_thread(cx: &mut TestAppContext) {
3981    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
3982    let fake_model = model.as_fake();
3983
3984    let send = thread
3985        .update(cx, |thread, cx| {
3986            thread.send(ClientUserMessageId::new(), ["Hello"], cx)
3987        })
3988        .unwrap();
3989    cx.run_until_parked();
3990    fake_model.send_last_completion_stream_text_chunk("Hey there!");
3991    fake_model.end_last_completion_stream();
3992    send.collect::<Vec<_>>().await;
3993    cx.run_until_parked();
3994
3995    let db_thread = thread.update(cx, |thread, cx| thread.to_db(cx)).await;
3996    let live_markdown = thread.read_with(cx, |thread, _| thread.to_markdown());
3997
3998    assert!(!live_markdown.is_empty());
3999    assert_eq!(db_thread.to_markdown(), live_markdown);
4000}
4001
4002#[gpui::test]
4003async fn test_title_generation_failure_allows_retry(cx: &mut TestAppContext) {
4004    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
4005    let fake_model = model.as_fake();
4006
4007    let summary_model = Arc::new(FakeLanguageModel::default());
4008    let fake_summary_model = summary_model.as_fake();
4009    thread.update(cx, |thread, cx| {
4010        thread.set_summarization_model(Some(summary_model.clone()), cx)
4011    });
4012
4013    let send = thread
4014        .update(cx, |thread, cx| {
4015            thread.send(ClientUserMessageId::new(), ["Hello"], cx)
4016        })
4017        .unwrap();
4018    cx.run_until_parked();
4019
4020    fake_model.send_last_completion_stream_text_chunk("Hey!");
4021    fake_model.end_last_completion_stream();
4022    cx.run_until_parked();
4023
4024    fake_summary_model.send_last_completion_stream_error(
4025        LanguageModelCompletionError::UpstreamProviderError {
4026            message: "Internal server error".to_string(),
4027            status: gpui::http_client::StatusCode::INTERNAL_SERVER_ERROR,
4028            retry_after: None,
4029        },
4030    );
4031    fake_summary_model.end_last_completion_stream();
4032    send.collect::<Vec<_>>().await;
4033    cx.run_until_parked();
4034
4035    thread.read_with(cx, |thread, _| {
4036        assert_eq!(thread.title(), None);
4037        assert!(thread.has_failed_title_generation());
4038        assert!(
4039            thread
4040                .title_generation_error()
4041                .is_some_and(|error| error.contains("Internal server error"))
4042        );
4043        assert!(!thread.is_generating_title());
4044    });
4045
4046    thread.update(cx, |thread, cx| {
4047        thread.generate_title(cx);
4048    });
4049    cx.run_until_parked();
4050
4051    thread.read_with(cx, |thread, _| {
4052        assert!(!thread.has_failed_title_generation());
4053        assert_eq!(thread.title_generation_error(), None);
4054        assert!(thread.is_generating_title());
4055    });
4056
4057    fake_summary_model.send_last_completion_stream_text_chunk("Retried title");
4058    fake_summary_model.end_last_completion_stream();
4059    cx.run_until_parked();
4060
4061    thread.read_with(cx, |thread, _| {
4062        assert_eq!(thread.title(), Some("Retried title".into()));
4063        assert!(!thread.has_failed_title_generation());
4064        assert_eq!(thread.title_generation_error(), None);
4065        assert!(!thread.is_generating_title());
4066    });
4067}
4068
4069#[gpui::test]
4070async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
4071    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
4072    let fake_model = model.as_fake();
4073
4074    let _events = thread
4075        .update(cx, |thread, cx| {
4076            thread.add_tool(ToolRequiringPermission);
4077            thread.add_tool(EchoTool);
4078            thread.send(ClientUserMessageId::new(), ["Hey!"], cx)
4079        })
4080        .unwrap();
4081    cx.run_until_parked();
4082
4083    let permission_tool_use = LanguageModelToolUse {
4084        id: "tool_id_1".into(),
4085        name: ToolRequiringPermission::NAME.into(),
4086        raw_input: "{}".into(),
4087        input: language_model::LanguageModelToolUseInput::Json(json!({})),
4088        is_input_complete: true,
4089        thought_signature: None,
4090    };
4091    let echo_tool_use = LanguageModelToolUse {
4092        id: "tool_id_2".into(),
4093        name: EchoTool::NAME.into(),
4094        raw_input: json!({"text": "test"}).to_string(),
4095        input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
4096        is_input_complete: true,
4097        thought_signature: None,
4098    };
4099    fake_model.send_last_completion_stream_text_chunk("Hi!");
4100    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
4101        permission_tool_use,
4102    ));
4103    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
4104        echo_tool_use.clone(),
4105    ));
4106    fake_model.end_last_completion_stream();
4107    cx.run_until_parked();
4108
4109    // Ensure pending tools are skipped when building a request.
4110    let request = thread
4111        .read_with(cx, |thread, cx| {
4112            thread.build_completion_request(CompletionIntent::EditFile, cx)
4113        })
4114        .unwrap();
4115    assert_eq!(
4116        request.messages[1..],
4117        vec![
4118            LanguageModelRequestMessage {
4119                role: Role::User,
4120                content: vec!["Hey!".into()],
4121                cache: true,
4122                reasoning_details: None,
4123            },
4124            LanguageModelRequestMessage {
4125                role: Role::Assistant,
4126                content: vec![
4127                    MessageContent::Text("Hi!".into()),
4128                    MessageContent::ToolUse(echo_tool_use.clone())
4129                ],
4130                cache: false,
4131                reasoning_details: None,
4132            },
4133            LanguageModelRequestMessage {
4134                role: Role::User,
4135                content: vec![MessageContent::ToolResult(LanguageModelToolResult {
4136                    tool_use_id: echo_tool_use.id.clone(),
4137                    tool_name: echo_tool_use.name,
4138                    is_error: false,
4139                    content: vec!["test".into()],
4140                    output: Some("test".into())
4141                })],
4142                cache: false,
4143                reasoning_details: None,
4144            },
4145        ],
4146    );
4147}
4148
4149#[gpui::test]
4150async fn test_agent_connection(cx: &mut TestAppContext) {
4151    cx.update(settings::init);
4152    let templates = Templates::new();
4153
4154    // Initialize language model system with test provider
4155    cx.update(|cx| {
4156        gpui_tokio::init(cx);
4157
4158        let http_client = FakeHttpClient::with_404_response();
4159        let clock = Arc::new(clock::FakeSystemClock::new());
4160        let client = Client::new(clock, http_client, cx);
4161        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
4162        language_model::init(cx);
4163        RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
4164        language_models::init(user_store, client.clone(), cx);
4165        LanguageModelRegistry::test(cx);
4166    });
4167    cx.executor().forbid_parking();
4168
4169    // Create a project for new_thread
4170    let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone()));
4171    fake_fs.insert_tree(path!("/test"), json!({})).await;
4172    let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await;
4173    let cwd = PathList::new(&[Path::new("/test")]);
4174    let thread_store = cx.new(|cx| ThreadStore::new(cx));
4175
4176    // Create agent and connection
4177    let agent =
4178        cx.update(|cx| NativeAgent::new(thread_store, templates.clone(), fake_fs.clone(), cx));
4179    let connection = NativeAgentConnection(agent.clone());
4180
4181    // Create a thread using new_thread
4182    let connection_rc = Rc::new(connection.clone());
4183    let acp_thread = cx
4184        .update(|cx| connection_rc.new_session(project, cwd, cx))
4185        .await
4186        .expect("new_thread should succeed");
4187
4188    // Get the session_id from the AcpThread
4189    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
4190
4191    // Test model_selector returns Some
4192    let selector_opt = connection.model_selector(&session_id);
4193    assert!(
4194        selector_opt.is_some(),
4195        "agent should always support ModelSelector"
4196    );
4197    let selector = selector_opt.unwrap();
4198
4199    // Test list_models
4200    let listed_models = cx
4201        .update(|cx| selector.list_models(cx))
4202        .await
4203        .expect("list_models should succeed");
4204    let AgentModelList::Grouped(listed_models) = listed_models else {
4205        panic!("Unexpected model list type");
4206    };
4207    assert!(!listed_models.is_empty(), "should have at least one model");
4208    assert_eq!(
4209        listed_models[&AgentModelGroupName("Fake".into())][0]
4210            .id
4211            .as_ref(),
4212        "fake/fake"
4213    );
4214
4215    // Test selected_model returns the default
4216    let model = cx
4217        .update(|cx| selector.selected_model(cx))
4218        .await
4219        .expect("selected_model should succeed");
4220    let model = cx
4221        .update(|cx| agent.read(cx).models().model_from_id(&model.id))
4222        .unwrap();
4223    let model = model.as_fake();
4224    assert_eq!(model.id().0, "fake", "should return default model");
4225
4226    let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx));
4227    cx.run_until_parked();
4228    model.send_last_completion_stream_text_chunk("def");
4229    cx.run_until_parked();
4230    acp_thread.read_with(cx, |thread, cx| {
4231        assert_eq!(
4232            thread.to_markdown(cx),
4233            indoc! {"
4234                ## User
4235
4236                abc
4237
4238                ## Assistant
4239
4240                def
4241
4242            "}
4243        )
4244    });
4245
4246    // Test cancel
4247    cx.update(|cx| connection.cancel(&session_id, cx));
4248    request.await.expect("prompt should fail gracefully");
4249
4250    // Explicitly close the session and drop the ACP thread.
4251    cx.update(|cx| Rc::new(connection.clone()).close_session(&session_id, cx))
4252        .await
4253        .unwrap();
4254    drop(acp_thread);
4255    let result = cx
4256        .update(|cx| {
4257            acp_thread::AgentSessionClientUserMessageIds::prompt(
4258                &connection,
4259                acp_thread::ClientUserMessageId::new(),
4260                acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]),
4261                cx,
4262            )
4263        })
4264        .await;
4265    assert_eq!(
4266        result.as_ref().unwrap_err().to_string(),
4267        "Session not found",
4268        "unexpected result: {:?}",
4269        result
4270    );
4271}
4272
4273#[gpui::test]
4274async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
4275    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
4276    thread.update(cx, |thread, _cx| thread.add_tool(EchoTool));
4277    let fake_model = model.as_fake();
4278
4279    let mut events = thread
4280        .update(cx, |thread, cx| {
4281            thread.send(ClientUserMessageId::new(), ["Echo something"], cx)
4282        })
4283        .unwrap();
4284    cx.run_until_parked();
4285
4286    // Simulate streaming partial input.
4287    let input = json!({});
4288    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
4289        LanguageModelToolUse {
4290            id: "1".into(),
4291            name: EchoTool::NAME.into(),
4292            raw_input: input.to_string(),
4293            input: language_model::LanguageModelToolUseInput::Json(input),
4294            is_input_complete: false,
4295            thought_signature: None,
4296        },
4297    ));
4298
4299    // Input streaming completed
4300    let input = json!({ "text": "Hello!" });
4301    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
4302        LanguageModelToolUse {
4303            id: "1".into(),
4304            name: "echo".into(),
4305            raw_input: input.to_string(),
4306            input: language_model::LanguageModelToolUseInput::Json(input),
4307            is_input_complete: true,
4308            thought_signature: None,
4309        },
4310    ));
4311    fake_model.end_last_completion_stream();
4312    cx.run_until_parked();
4313
4314    // User message is index 0, so the tool call is scoped to index 1 (see
4315    // `scoped_tool_call_id`).
4316    let tool_call_id = scoped_tool_call_id(1, &"1".into());
4317
4318    let tool_call = expect_tool_call(&mut events).await;
4319    assert_eq!(
4320        tool_call,
4321        acp::ToolCall::new(tool_call_id.clone(), "Echo")
4322            .raw_input(json!({}))
4323            .meta(acp::Meta::from_iter([("tool_name".into(), "echo".into())]))
4324    );
4325    let update = expect_tool_call_update_fields(&mut events).await;
4326    assert_eq!(
4327        update,
4328        acp::ToolCallUpdate::new(
4329            tool_call_id.clone(),
4330            acp::ToolCallUpdateFields::new()
4331                .title("Echo")
4332                .kind(acp::ToolKind::Other)
4333                .raw_input(json!({ "text": "Hello!"}))
4334        )
4335    );
4336    let update = expect_tool_call_update_fields(&mut events).await;
4337    assert_eq!(
4338        update,
4339        acp::ToolCallUpdate::new(
4340            tool_call_id.clone(),
4341            acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress)
4342        )
4343    );
4344    let update = expect_tool_call_update_fields(&mut events).await;
4345    assert_eq!(
4346        update,
4347        acp::ToolCallUpdate::new(
4348            tool_call_id,
4349            acp::ToolCallUpdateFields::new()
4350                .status(acp::ToolCallStatus::Completed)
4351                .raw_output("Hello!")
4352        )
4353    );
4354}
4355
4356#[gpui::test]
4357async fn test_send_no_retry_on_success(cx: &mut TestAppContext) {
4358    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
4359    let fake_model = model.as_fake();
4360
4361    let mut events = thread
4362        .update(cx, |thread, cx| {
4363            thread.send(ClientUserMessageId::new(), ["Hello!"], cx)
4364        })
4365        .unwrap();
4366    cx.run_until_parked();
4367
4368    fake_model.send_last_completion_stream_text_chunk("Hey!");
4369    fake_model.end_last_completion_stream();
4370
4371    let mut retry_events = Vec::new();
4372    while let Some(Ok(event)) = events.next().await {
4373        match event {
4374            ThreadEvent::Retry(retry_status) => {
4375                retry_events.push(retry_status);
4376            }
4377            ThreadEvent::Stop(..) => break,
4378            _ => {}
4379        }
4380    }
4381
4382    assert_eq!(retry_events.len(), 0);
4383    thread.read_with(cx, |thread, _cx| {
4384        assert_eq!(
4385            thread.to_markdown(),
4386            indoc! {"
4387                ## User
4388
4389                Hello!
4390
4391                ## Assistant
4392
4393                Hey!
4394            "}
4395        )
4396    });
4397}
4398
4399#[gpui::test]
4400async fn test_send_retry_on_error(cx: &mut TestAppContext) {
4401    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
4402    let fake_model = model.as_fake();
4403
4404    let mut events = thread
4405        .update(cx, |thread, cx| {
4406            thread.send(ClientUserMessageId::new(), ["Hello!"], cx)
4407        })
4408        .unwrap();
4409    cx.run_until_parked();
4410
4411    fake_model.send_last_completion_stream_text_chunk("Hey,");
4412    fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
4413        provider: LanguageModelProviderName::new("Anthropic"),
4414        retry_after: Some(Duration::from_secs(3)),
4415    });
4416    fake_model.end_last_completion_stream();
4417
4418    cx.executor().advance_clock(Duration::from_secs(3));
4419    cx.run_until_parked();
4420
4421    fake_model.send_last_completion_stream_text_chunk("there!");
4422    fake_model.end_last_completion_stream();
4423    cx.run_until_parked();
4424
4425    let mut retry_events = Vec::new();
4426    while let Some(Ok(event)) = events.next().await {
4427        match event {
4428            ThreadEvent::Retry(retry_status) => {
4429                retry_events.push(retry_status);
4430            }
4431            ThreadEvent::Stop(..) => break,
4432            _ => {}
4433        }
4434    }
4435
4436    assert_eq!(retry_events.len(), 1);
4437    assert!(matches!(
4438        retry_events[0],
4439        acp_thread::RetryStatus { attempt: 1, .. }
4440    ));
4441    thread.read_with(cx, |thread, _cx| {
4442        assert_eq!(
4443            thread.to_markdown(),
4444            indoc! {"
4445                ## User
4446
4447                Hello!
4448
4449                ## Assistant
4450
4451                Hey,
4452
4453                [resume]
4454
4455                ## Assistant
4456
4457                there!
4458            "}
4459        )
4460    });
4461}
4462
4463#[gpui::test]
4464async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
4465    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
4466    let fake_model = model.as_fake();
4467
4468    let events = thread
4469        .update(cx, |thread, cx| {
4470            thread.add_tool(EchoTool);
4471            thread.send(ClientUserMessageId::new(), ["Call the echo tool!"], cx)
4472        })
4473        .unwrap();
4474    cx.run_until_parked();
4475
4476    let tool_use_1 = LanguageModelToolUse {
4477        id: "tool_1".into(),
4478        name: EchoTool::NAME.into(),
4479        raw_input: json!({"text": "test"}).to_string(),
4480        input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
4481        is_input_complete: true,
4482        thought_signature: None,
4483    };
4484    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
4485        tool_use_1.clone(),
4486    ));
4487    fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
4488        provider: LanguageModelProviderName::new("Anthropic"),
4489        retry_after: Some(Duration::from_secs(3)),
4490    });
4491    fake_model.end_last_completion_stream();
4492
4493    cx.executor().advance_clock(Duration::from_secs(3));
4494    let completion = fake_model.pending_completions().pop().unwrap();
4495    assert_eq!(
4496        completion.messages[1..],
4497        vec![
4498            LanguageModelRequestMessage {
4499                role: Role::User,
4500                content: vec!["Call the echo tool!".into()],
4501                cache: false,
4502                reasoning_details: None,
4503            },
4504            LanguageModelRequestMessage {
4505                role: Role::Assistant,
4506                content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
4507                cache: false,
4508                reasoning_details: None,
4509            },
4510            LanguageModelRequestMessage {
4511                role: Role::User,
4512                content: vec![language_model::MessageContent::ToolResult(
4513                    LanguageModelToolResult {
4514                        tool_use_id: tool_use_1.id.clone(),
4515                        tool_name: tool_use_1.name.clone(),
4516                        is_error: false,
4517                        content: vec!["test".into()],
4518                        output: Some("test".into())
4519                    }
4520                )],
4521                cache: true,
4522                reasoning_details: None,
4523            },
4524        ]
4525    );
4526
4527    fake_model.send_last_completion_stream_text_chunk("Done");
4528    fake_model.end_last_completion_stream();
4529    cx.run_until_parked();
4530    events.collect::<Vec<_>>().await;
4531    thread.read_with(cx, |thread, _cx| {
4532        assert_eq!(
4533            thread.last_received_or_pending_message().as_deref(),
4534            Some(&Message::Agent(AgentMessage {
4535                content: vec![AgentMessageContent::Text("Done".into())],
4536                tool_results: IndexMap::default(),
4537                reasoning_details: None,
4538            }))
4539        );
4540    })
4541}
4542
4543#[gpui::test]
4544async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
4545    let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
4546    let fake_model = model.as_fake();
4547
4548    let mut events = thread
4549        .update(cx, |thread, cx| {
4550            thread.send(ClientUserMessageId::new(), ["Hello!"], cx)
4551        })
4552        .unwrap();
4553    cx.run_until_parked();
4554
4555    for _ in 0..crate::thread::MAX_RETRY_ATTEMPTS + 1 {
4556        fake_model.send_last_completion_stream_error(
4557            LanguageModelCompletionError::ServerOverloaded {
4558                provider: LanguageModelProviderName::new("Anthropic"),
4559                retry_after: Some(Duration::from_secs(3)),
4560            },
4561        );
4562        fake_model.end_last_completion_stream();
4563        cx.executor().advance_clock(Duration::from_secs(3));
4564        cx.run_until_parked();
4565    }
4566
4567    let mut errors = Vec::new();
4568    let mut retry_events = Vec::new();
4569    while let Some(event) = events.next().await {
4570        match event {
4571            Ok(ThreadEvent::Retry(retry_status)) => {
4572                retry_events.push(retry_status);
4573            }
4574            Ok(ThreadEvent::Stop(..)) => break,
4575            Err(error) => errors.push(error),
4576            _ => {}
4577        }
4578    }
4579
4580    assert_eq!(
4581        retry_events.len(),
4582        crate::thread::MAX_RETRY_ATTEMPTS as usize
4583    );
4584    for i in 0..crate::thread::MAX_RETRY_ATTEMPTS as usize {
4585        assert_eq!(retry_events[i].attempt, i + 1);
4586    }
4587    assert_eq!(errors.len(), 1);
4588    let error = errors[0]
4589        .downcast_ref::<LanguageModelCompletionError>()
4590        .unwrap();
4591    assert!(matches!(
4592        error,
4593        LanguageModelCompletionError::ServerOverloaded { .. }
4594    ));
4595}
4596
4597#[gpui::test]
4598async fn test_streaming_tool_completes_when_llm_stream_ends_without_final_input(
4599    cx: &mut TestAppContext,
4600) {
4601    init_test(cx);
4602    always_allow_tools(cx);
4603
4604    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
4605    let fake_model = model.as_fake();
4606
4607    thread.update(cx, |thread, _cx| {
4608        thread.add_tool(StreamingEchoTool::new());
4609    });
4610
4611    let _events = thread
4612        .update(cx, |thread, cx| {
4613            thread.send(
4614                ClientUserMessageId::new(),
4615                ["Use the streaming_echo tool"],
4616                cx,
4617            )
4618        })
4619        .unwrap();
4620    cx.run_until_parked();
4621
4622    // Send a partial tool use (is_input_complete = false), simulating the LLM
4623    // streaming input for a tool.
4624    let tool_use = LanguageModelToolUse {
4625        id: "tool_1".into(),
4626        name: "streaming_echo".into(),
4627        raw_input: r#"{"text": "partial"}"#.into(),
4628        input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
4629        is_input_complete: false,
4630        thought_signature: None,
4631    };
4632    fake_model
4633        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
4634    cx.run_until_parked();
4635
4636    // Send a stream error WITHOUT ever sending is_input_complete = true.
4637    // Before the fix, this would deadlock: the tool waits for more partials
4638    // (or cancellation), run_turn_internal waits for the tool, and the sender
4639    // keeping the channel open lives inside RunningTurn.
4640    fake_model.send_last_completion_stream_error(
4641        LanguageModelCompletionError::UpstreamProviderError {
4642            message: "Internal server error".to_string(),
4643            status: http_client::StatusCode::INTERNAL_SERVER_ERROR,
4644            retry_after: None,
4645        },
4646    );
4647    fake_model.end_last_completion_stream();
4648
4649    // Advance past the retry delay so run_turn_internal retries.
4650    cx.executor().advance_clock(Duration::from_secs(5));
4651    cx.run_until_parked();
4652
4653    // The retry request should contain the streaming tool's error result,
4654    // proving the tool terminated and its result was forwarded.
4655    let completion = fake_model
4656        .pending_completions()
4657        .pop()
4658        .expect("No running turn");
4659    assert_eq!(
4660        completion.messages[1..],
4661        vec![
4662            LanguageModelRequestMessage {
4663                role: Role::User,
4664                content: vec!["Use the streaming_echo tool".into()],
4665                cache: false,
4666                reasoning_details: None,
4667            },
4668            LanguageModelRequestMessage {
4669                role: Role::Assistant,
4670                content: vec![language_model::MessageContent::ToolUse(tool_use.clone())],
4671                cache: false,
4672                reasoning_details: None,
4673            },
4674            LanguageModelRequestMessage {
4675                role: Role::User,
4676                content: vec![language_model::MessageContent::ToolResult(
4677                    LanguageModelToolResult {
4678                        tool_use_id: tool_use.id.clone(),
4679                        tool_name: tool_use.name,
4680                        is_error: true,
4681                        content: vec!["tool input was not fully received".into(),],
4682                        output: Some("tool input was not fully received".into()),
4683                    }
4684                )],
4685                cache: true,
4686                reasoning_details: None,
4687            },
4688        ]
4689    );
4690
4691    // Finish the retry round so the turn completes cleanly.
4692    fake_model.send_last_completion_stream_text_chunk("Done");
4693    fake_model.end_last_completion_stream();
4694    cx.run_until_parked();
4695
4696    thread.read_with(cx, |thread, _cx| {
4697        assert!(
4698            thread.is_turn_complete(),
4699            "Thread should not be stuck; the turn should have completed",
4700        );
4701    });
4702}
4703
4704#[gpui::test]
4705async fn test_streaming_tool_json_parse_error_is_forwarded_to_running_tool(
4706    cx: &mut TestAppContext,
4707) {
4708    init_test(cx);
4709    always_allow_tools(cx);
4710
4711    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
4712    let fake_model = model.as_fake();
4713
4714    thread.update(cx, |thread, _cx| {
4715        thread.add_tool(StreamingJsonErrorContextTool);
4716    });
4717
4718    let _events = thread
4719        .update(cx, |thread, cx| {
4720            thread.send(
4721                ClientUserMessageId::new(),
4722                ["Use the streaming_json_error_context tool"],
4723                cx,
4724            )
4725        })
4726        .unwrap();
4727    cx.run_until_parked();
4728
4729    let tool_use = LanguageModelToolUse {
4730        id: "tool_1".into(),
4731        name: StreamingJsonErrorContextTool::NAME.into(),
4732        raw_input: r#"{"text": "partial"#.into(),
4733        input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
4734        is_input_complete: false,
4735        thought_signature: None,
4736    };
4737    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use));
4738    cx.run_until_parked();
4739
4740    fake_model.send_last_completion_stream_event(
4741        LanguageModelCompletionEvent::ToolUseJsonParseError {
4742            id: "tool_1".into(),
4743            tool_name: StreamingJsonErrorContextTool::NAME.into(),
4744            raw_input: r#"{"text": "partial"#.into(),
4745            json_parse_error: "EOF while parsing a string at line 1 column 17".into(),
4746        },
4747    );
4748    fake_model
4749        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::ToolUse));
4750    fake_model.end_last_completion_stream();
4751    cx.run_until_parked();
4752
4753    cx.executor().advance_clock(Duration::from_secs(5));
4754    cx.run_until_parked();
4755
4756    let completion = fake_model
4757        .pending_completions()
4758        .pop()
4759        .expect("No running turn");
4760
4761    let tool_results: Vec<_> = completion
4762        .messages
4763        .iter()
4764        .flat_map(|message| &message.content)
4765        .filter_map(|content| match content {
4766            MessageContent::ToolResult(result)
4767                if result.tool_use_id == language_model::LanguageModelToolUseId::from("tool_1") =>
4768            {
4769                Some(result)
4770            }
4771            _ => None,
4772        })
4773        .collect();
4774
4775    assert_eq!(
4776        tool_results.len(),
4777        1,
4778        "Expected exactly 1 tool result for tool_1, got {}: {:#?}",
4779        tool_results.len(),
4780        tool_results
4781    );
4782
4783    let result = tool_results[0];
4784    assert!(result.is_error);
4785    let content_text = result.text_contents();
4786    assert!(
4787        content_text.contains("Saw partial text 'partial' before invalid JSON"),
4788        "Expected tool-enriched partial context, got: {content_text}"
4789    );
4790    assert!(
4791        content_text
4792            .contains("Error parsing input JSON: EOF while parsing a string at line 1 column 17"),
4793        "Expected forwarded JSON parse error, got: {content_text}"
4794    );
4795    assert!(
4796        !content_text.contains("tool input was not fully received"),
4797        "Should not contain orphaned sender error, got: {content_text}"
4798    );
4799
4800    fake_model.send_last_completion_stream_text_chunk("Done");
4801    fake_model.end_last_completion_stream();
4802    cx.run_until_parked();
4803
4804    thread.read_with(cx, |thread, _cx| {
4805        assert!(
4806            thread.is_turn_complete(),
4807            "Thread should not be stuck; the turn should have completed",
4808        );
4809    });
4810}
4811
4812/// Filters out the stop events for asserting against in tests
4813fn stop_events(result_events: Vec<Result<ThreadEvent>>) -> Vec<acp::StopReason> {
4814    result_events
4815        .into_iter()
4816        .filter_map(|event| match event.unwrap() {
4817            ThreadEvent::Stop(stop_reason) => Some(stop_reason),
4818            _ => None,
4819        })
4820        .collect()
4821}
4822
4823struct ThreadTest {
4824    model: Arc<dyn LanguageModel>,
4825    thread: Entity<Thread>,
4826    project_context: Entity<ProjectContext>,
4827    context_server_store: Entity<ContextServerStore>,
4828    fs: Arc<FakeFs>,
4829}
4830
4831enum TestModel {
4832    Sonnet4,
4833    Fake,
4834}
4835
4836impl TestModel {
4837    fn id(&self) -> LanguageModelId {
4838        match self {
4839            TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()),
4840            TestModel::Fake => unreachable!(),
4841        }
4842    }
4843}
4844
4845async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
4846    cx.executor().allow_parking();
4847
4848    let fs = FakeFs::new(cx.background_executor.clone());
4849    fs.create_dir(paths::settings_file().parent().unwrap())
4850        .await
4851        .unwrap();
4852    fs.insert_file(
4853        paths::settings_file(),
4854        json!({
4855            "agent": {
4856                "default_profile": "test-profile",
4857                "profiles": {
4858                    "test-profile": {
4859                        "name": "Test Profile",
4860                        "tools": {
4861                            EchoTool::NAME: true,
4862                            DelayTool::NAME: true,
4863                            WordListTool::NAME: true,
4864                            ToolRequiringPermission::NAME: true,
4865                            ToolRequiringPermission2::NAME: true,
4866                            InfiniteTool::NAME: true,
4867                            CancellationAwareTool::NAME: true,
4868                            StreamingEchoTool::NAME: true,
4869                            StreamingJsonErrorContextTool::NAME: true,
4870                            StreamingFailingEchoTool::NAME: true,
4871                            TerminalTool::NAME: true,
4872                        }
4873                    }
4874                }
4875            }
4876        })
4877        .to_string()
4878        .into_bytes(),
4879    )
4880    .await;
4881
4882    cx.update(|cx| {
4883        settings::init(cx);
4884
4885        match model {
4886            TestModel::Fake => {}
4887            TestModel::Sonnet4 => {
4888                gpui_tokio::init(cx);
4889                let http_client = ReqwestClient::user_agent("agent tests").unwrap();
4890                cx.set_http_client(Arc::new(http_client));
4891                let client = Client::production(cx);
4892                let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
4893                language_model::init(cx);
4894                RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
4895                language_models::init(user_store, client.clone(), cx);
4896            }
4897        };
4898
4899        watch_settings(fs.clone(), cx);
4900    });
4901
4902    let templates = Templates::new();
4903
4904    fs.insert_tree(path!("/test"), json!({})).await;
4905    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
4906
4907    let model = cx
4908        .update(|cx| {
4909            if let TestModel::Fake = model {
4910                Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
4911            } else {
4912                let model_id = model.id();
4913                let models = LanguageModelRegistry::read_global(cx);
4914                let model = models
4915                    .available_models(cx)
4916                    .find(|model| model.id() == model_id)
4917                    .unwrap();
4918
4919                let provider = models.provider(&model.provider_id()).unwrap();
4920                let authenticated = provider.authenticate(cx);
4921
4922                cx.spawn(async move |_cx| {
4923                    authenticated.await.unwrap();
4924                    model
4925                })
4926            }
4927        })
4928        .await;
4929
4930    let project_context = cx.new(|_cx| ProjectContext::default());
4931    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
4932    let context_server_registry =
4933        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
4934    let thread = cx.new(|cx| {
4935        Thread::new(
4936            project,
4937            project_context.clone(),
4938            context_server_registry,
4939            templates,
4940            Some(model.clone()),
4941            cx,
4942        )
4943    });
4944    ThreadTest {
4945        model,
4946        thread,
4947        project_context,
4948        context_server_store,
4949        fs,
4950    }
4951}
4952
4953#[cfg(test)]
4954#[ctor::ctor(unsafe)]
4955fn init_logger() {
4956    if std::env::var("RUST_LOG").is_ok() {
4957        env_logger::init();
4958    }
4959}
4960
4961fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
4962    let fs = fs.clone();
4963    cx.spawn({
4964        async move |cx| {
4965            let (mut new_settings_content_rx, watcher_task) = settings::watch_config_file(
4966                cx.background_executor(),
4967                fs,
4968                paths::settings_file().clone(),
4969            );
4970            let _watcher_task = watcher_task;
4971
4972            while let Some(new_settings_content) = new_settings_content_rx.next().await {
4973                cx.update(|cx| {
4974                    SettingsStore::update_global(cx, |settings, cx| {
4975                        settings.set_user_settings(&new_settings_content, cx)
4976                    })
4977                })
4978                .ok();
4979            }
4980        }
4981    })
4982    .detach();
4983}
4984
4985fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec<String> {
4986    completion
4987        .tools
4988        .iter()
4989        .map(|tool| tool.name.clone())
4990        .collect()
4991}
4992
4993fn setup_context_server(
4994    name: &'static str,
4995    tools: Vec<context_server::types::Tool>,
4996    context_server_store: &Entity<ContextServerStore>,
4997    cx: &mut TestAppContext,
4998) -> mpsc::UnboundedReceiver<(
4999    context_server::types::CallToolParams,
5000    oneshot::Sender<context_server::types::CallToolResponse>,
5001)> {
5002    cx.update(|cx| {
5003        let mut settings = ProjectSettings::get_global(cx).clone();
5004        settings.context_servers.insert(
5005            name.into(),
5006            project::project_settings::ContextServerSettings::Stdio {
5007                enabled: true,
5008                remote: false,
5009                command: ContextServerCommand {
5010                    path: "somebinary".into(),
5011                    args: Vec::new(),
5012                    env: None,
5013                    timeout: None,
5014                },
5015            },
5016        );
5017        ProjectSettings::override_global(settings, cx);
5018    });
5019
5020    let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded();
5021    let fake_transport = context_server::test::create_fake_transport(name, cx.executor())
5022        .on_request::<context_server::types::requests::Initialize, _>(move |_params| async move {
5023            context_server::types::InitializeResponse {
5024                protocol_version: context_server::types::ProtocolVersion(
5025                    context_server::types::LATEST_PROTOCOL_VERSION.to_string(),
5026                ),
5027                server_info: context_server::types::Implementation {
5028                    name: name.into(),
5029                    title: None,
5030                    version: "1.0.0".to_string(),
5031                    description: None,
5032                },
5033                capabilities: context_server::types::ServerCapabilities {
5034                    tools: Some(context_server::types::ToolsCapabilities {
5035                        list_changed: Some(true),
5036                    }),
5037                    ..Default::default()
5038                },
5039                meta: None,
5040            }
5041        })
5042        .on_request::<context_server::types::requests::ListTools, _>(move |_params| {
5043            let tools = tools.clone();
5044            async move {
5045                context_server::types::ListToolsResponse {
5046                    tools,
5047                    next_cursor: None,
5048                    meta: None,
5049                }
5050            }
5051        })
5052        .on_request::<context_server::types::requests::CallTool, _>(move |params| {
5053            let mcp_tool_calls_tx = mcp_tool_calls_tx.clone();
5054            async move {
5055                let (response_tx, response_rx) = oneshot::channel();
5056                mcp_tool_calls_tx
5057                    .unbounded_send((params, response_tx))
5058                    .unwrap();
5059                response_rx.await.unwrap()
5060            }
5061        });
5062    context_server_store.update(cx, |store, cx| {
5063        store.start_server(
5064            Arc::new(ContextServer::new(
5065                ContextServerId(name.into()),
5066                Arc::new(fake_transport),
5067            )),
5068            cx,
5069        );
5070    });
5071    cx.run_until_parked();
5072    mcp_tool_calls_rx
5073}
5074
5075#[gpui::test]
5076async fn test_tokens_before_message(cx: &mut TestAppContext) {
5077    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
5078    let fake_model = model.as_fake();
5079
5080    // First message
5081    let message_1_id = ClientUserMessageId::new();
5082    thread
5083        .update(cx, |thread, cx| {
5084            thread.send(message_1_id.clone(), ["First message"], cx)
5085        })
5086        .unwrap();
5087    cx.run_until_parked();
5088
5089    // Before any response, tokens_before_message should return None for first message
5090    thread.read_with(cx, |thread, _| {
5091        assert_eq!(
5092            thread.tokens_before_message(&message_1_id),
5093            None,
5094            "First message should have no tokens before it"
5095        );
5096    });
5097
5098    // Complete first message with usage
5099    fake_model.send_last_completion_stream_text_chunk("Response 1");
5100    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
5101        language_model::TokenUsage {
5102            input_tokens: 100,
5103            output_tokens: 50,
5104            cache_creation_input_tokens: 0,
5105            cache_read_input_tokens: 0,
5106        },
5107    ));
5108    fake_model.end_last_completion_stream();
5109    cx.run_until_parked();
5110
5111    // First message still has no tokens before it
5112    thread.read_with(cx, |thread, _| {
5113        assert_eq!(
5114            thread.tokens_before_message(&message_1_id),
5115            None,
5116            "First message should still have no tokens before it after response"
5117        );
5118    });
5119
5120    // Second message
5121    let message_2_id = ClientUserMessageId::new();
5122    thread
5123        .update(cx, |thread, cx| {
5124            thread.send(message_2_id.clone(), ["Second message"], cx)
5125        })
5126        .unwrap();
5127    cx.run_until_parked();
5128
5129    // Second message should have first message's input tokens before it
5130    thread.read_with(cx, |thread, _| {
5131        assert_eq!(
5132            thread.tokens_before_message(&message_2_id),
5133            Some(100),
5134            "Second message should have 100 tokens before it (from first request)"
5135        );
5136    });
5137
5138    // Complete second message
5139    fake_model.send_last_completion_stream_text_chunk("Response 2");
5140    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
5141        language_model::TokenUsage {
5142            input_tokens: 250, // Total for this request (includes previous context)
5143            output_tokens: 75,
5144            cache_creation_input_tokens: 0,
5145            cache_read_input_tokens: 0,
5146        },
5147    ));
5148    fake_model.end_last_completion_stream();
5149    cx.run_until_parked();
5150
5151    // Third message
5152    let message_3_id = ClientUserMessageId::new();
5153    thread
5154        .update(cx, |thread, cx| {
5155            thread.send(message_3_id.clone(), ["Third message"], cx)
5156        })
5157        .unwrap();
5158    cx.run_until_parked();
5159
5160    // Third message should have second message's input tokens (250) before it
5161    thread.read_with(cx, |thread, _| {
5162        assert_eq!(
5163            thread.tokens_before_message(&message_3_id),
5164            Some(250),
5165            "Third message should have 250 tokens before it (from second request)"
5166        );
5167        // Second message should still have 100
5168        assert_eq!(
5169            thread.tokens_before_message(&message_2_id),
5170            Some(100),
5171            "Second message should still have 100 tokens before it"
5172        );
5173        // First message still has none
5174        assert_eq!(
5175            thread.tokens_before_message(&message_1_id),
5176            None,
5177            "First message should still have no tokens before it"
5178        );
5179    });
5180}
5181
5182#[gpui::test]
5183async fn test_tokens_before_message_after_truncate(cx: &mut TestAppContext) {
5184    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
5185    let fake_model = model.as_fake();
5186
5187    // Set up three messages with responses
5188    let message_1_id = ClientUserMessageId::new();
5189    thread
5190        .update(cx, |thread, cx| {
5191            thread.send(message_1_id.clone(), ["Message 1"], cx)
5192        })
5193        .unwrap();
5194    cx.run_until_parked();
5195    fake_model.send_last_completion_stream_text_chunk("Response 1");
5196    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
5197        language_model::TokenUsage {
5198            input_tokens: 100,
5199            output_tokens: 50,
5200            cache_creation_input_tokens: 0,
5201            cache_read_input_tokens: 0,
5202        },
5203    ));
5204    fake_model.end_last_completion_stream();
5205    cx.run_until_parked();
5206
5207    let message_2_id = ClientUserMessageId::new();
5208    thread
5209        .update(cx, |thread, cx| {
5210            thread.send(message_2_id.clone(), ["Message 2"], cx)
5211        })
5212        .unwrap();
5213    cx.run_until_parked();
5214    fake_model.send_last_completion_stream_text_chunk("Response 2");
5215    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
5216        language_model::TokenUsage {
5217            input_tokens: 250,
5218            output_tokens: 75,
5219            cache_creation_input_tokens: 0,
5220            cache_read_input_tokens: 0,
5221        },
5222    ));
5223    fake_model.end_last_completion_stream();
5224    cx.run_until_parked();
5225
5226    // Verify initial state
5227    thread.read_with(cx, |thread, _| {
5228        assert_eq!(thread.tokens_before_message(&message_2_id), Some(100));
5229    });
5230
5231    // Truncate at message 2 (removes message 2 and everything after)
5232    thread
5233        .update(cx, |thread, cx| thread.truncate(message_2_id.clone(), cx))
5234        .unwrap();
5235    cx.run_until_parked();
5236
5237    // After truncation, message_2_id no longer exists, so lookup should return None
5238    thread.read_with(cx, |thread, _| {
5239        assert_eq!(
5240            thread.tokens_before_message(&message_2_id),
5241            None,
5242            "After truncation, message 2 no longer exists"
5243        );
5244        // Message 1 still exists but has no tokens before it
5245        assert_eq!(
5246            thread.tokens_before_message(&message_1_id),
5247            None,
5248            "First message still has no tokens before it"
5249        );
5250    });
5251}
5252
5253#[gpui::test]
5254async fn test_terminal_tool_permission_rules(cx: &mut TestAppContext) {
5255    init_test(cx);
5256
5257    let fs = FakeFs::new(cx.executor());
5258    fs.insert_tree("/root", json!({})).await;
5259    let project = Project::test(fs, ["/root".as_ref()], cx).await;
5260
5261    // Test 1: Deny rule blocks command
5262    {
5263        let environment = Rc::new(cx.update(|cx| {
5264            FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
5265        }));
5266
5267        cx.update(|cx| {
5268            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
5269            settings.tool_permissions.tools.insert(
5270                TerminalTool::NAME.into(),
5271                agent_settings::ToolRules {
5272                    default: Some(settings::ToolPermissionMode::Confirm),
5273                    always_allow: vec![],
5274                    always_deny: vec![
5275                        agent_settings::CompiledRegex::new(r"rm\s+-rf", false).unwrap(),
5276                    ],
5277                    always_confirm: vec![],
5278                    invalid_patterns: vec![],
5279                },
5280            );
5281            agent_settings::AgentSettings::override_global(settings, cx);
5282        });
5283
5284        #[allow(clippy::arc_with_non_send_sync)]
5285        let tool = Arc::new(crate::TerminalTool::new(project.clone(), environment));
5286        let (event_stream, _rx) = crate::ToolCallEventStream::test();
5287
5288        let task = cx.update(|cx| {
5289            tool.run(
5290                ToolInput::resolved(crate::TerminalToolInput {
5291                    command: "rm -rf /".to_string(),
5292                    cd: ".".to_string(),
5293                    timeout_ms: None,
5294                    ..Default::default()
5295                }),
5296                event_stream,
5297                cx,
5298            )
5299        });
5300
5301        let result = task.await;
5302        assert!(
5303            result.is_err(),
5304            "expected command to be blocked by deny rule"
5305        );
5306        let err_msg = result.unwrap_err().to_lowercase();
5307        assert!(
5308            err_msg.contains("blocked"),
5309            "error should mention the command was blocked"
5310        );
5311    }
5312
5313    // Test 2: Allow rule skips confirmation (and overrides default: Deny)
5314    {
5315        let environment = Rc::new(cx.update(|cx| {
5316            FakeThreadEnvironment::default()
5317                .with_terminal(FakeTerminalHandle::new_with_immediate_exit(cx, 0))
5318        }));
5319
5320        cx.update(|cx| {
5321            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
5322            settings.tool_permissions.tools.insert(
5323                TerminalTool::NAME.into(),
5324                agent_settings::ToolRules {
5325                    default: Some(settings::ToolPermissionMode::Deny),
5326                    always_allow: vec![
5327                        agent_settings::CompiledRegex::new(r"^echo\s", false).unwrap(),
5328                    ],
5329                    always_deny: vec![],
5330                    always_confirm: vec![],
5331                    invalid_patterns: vec![],
5332                },
5333            );
5334            agent_settings::AgentSettings::override_global(settings, cx);
5335        });
5336
5337        #[allow(clippy::arc_with_non_send_sync)]
5338        let tool = Arc::new(crate::TerminalTool::new(project.clone(), environment));
5339        let (event_stream, mut rx) = crate::ToolCallEventStream::test();
5340
5341        let task = cx.update(|cx| {
5342            tool.run(
5343                ToolInput::resolved(crate::TerminalToolInput {
5344                    command: "echo hello".to_string(),
5345                    cd: ".".to_string(),
5346                    timeout_ms: None,
5347                    ..Default::default()
5348                }),
5349                event_stream,
5350                cx,
5351            )
5352        });
5353
5354        let update = rx.expect_update_fields().await;
5355        assert!(
5356            update.content.iter().any(|blocks| {
5357                blocks
5358                    .iter()
5359                    .any(|c| matches!(c, acp::ToolCallContent::Terminal(_)))
5360            }),
5361            "expected terminal content (allow rule should skip confirmation and override default deny)"
5362        );
5363
5364        let result = task.await;
5365        assert!(
5366            result.is_ok(),
5367            "expected command to succeed without confirmation"
5368        );
5369    }
5370
5371    // Test 3: global default: allow does NOT override always_confirm patterns
5372    {
5373        let environment = Rc::new(cx.update(|cx| {
5374            FakeThreadEnvironment::default()
5375                .with_terminal(FakeTerminalHandle::new_with_immediate_exit(cx, 0))
5376        }));
5377
5378        cx.update(|cx| {
5379            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
5380            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
5381            settings.tool_permissions.tools.insert(
5382                TerminalTool::NAME.into(),
5383                agent_settings::ToolRules {
5384                    default: Some(settings::ToolPermissionMode::Allow),
5385                    always_allow: vec![],
5386                    always_deny: vec![],
5387                    always_confirm: vec![
5388                        agent_settings::CompiledRegex::new(r"sudo", false).unwrap(),
5389                    ],
5390                    invalid_patterns: vec![],
5391                },
5392            );
5393            agent_settings::AgentSettings::override_global(settings, cx);
5394        });
5395
5396        #[allow(clippy::arc_with_non_send_sync)]
5397        let tool = Arc::new(crate::TerminalTool::new(project.clone(), environment));
5398        let (event_stream, mut rx) = crate::ToolCallEventStream::test();
5399
5400        let _task = cx.update(|cx| {
5401            tool.run(
5402                ToolInput::resolved(crate::TerminalToolInput {
5403                    command: "sudo rm file".to_string(),
5404                    cd: ".".to_string(),
5405                    timeout_ms: None,
5406                    ..Default::default()
5407                }),
5408                event_stream,
5409                cx,
5410            )
5411        });
5412
5413        // With global default: allow, confirm patterns are still respected
5414        // The expect_authorization() call will panic if no authorization is requested,
5415        // which validates that the confirm pattern still triggers confirmation
5416        let _auth = rx.expect_authorization().await;
5417
5418        drop(_task);
5419    }
5420
5421    // Test 4: tool-specific default: deny is respected even with global default: allow
5422    {
5423        let environment = Rc::new(cx.update(|cx| {
5424            FakeThreadEnvironment::default()
5425                .with_terminal(FakeTerminalHandle::new_with_immediate_exit(cx, 0))
5426        }));
5427
5428        cx.update(|cx| {
5429            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
5430            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
5431            settings.tool_permissions.tools.insert(
5432                TerminalTool::NAME.into(),
5433                agent_settings::ToolRules {
5434                    default: Some(settings::ToolPermissionMode::Deny),
5435                    always_allow: vec![],
5436                    always_deny: vec![],
5437                    always_confirm: vec![],
5438                    invalid_patterns: vec![],
5439                },
5440            );
5441            agent_settings::AgentSettings::override_global(settings, cx);
5442        });
5443
5444        #[allow(clippy::arc_with_non_send_sync)]
5445        let tool = Arc::new(crate::TerminalTool::new(project.clone(), environment));
5446        let (event_stream, _rx) = crate::ToolCallEventStream::test();
5447
5448        let task = cx.update(|cx| {
5449            tool.run(
5450                ToolInput::resolved(crate::TerminalToolInput {
5451                    command: "echo hello".to_string(),
5452                    cd: ".".to_string(),
5453                    timeout_ms: None,
5454                    ..Default::default()
5455                }),
5456                event_stream,
5457                cx,
5458            )
5459        });
5460
5461        // tool-specific default: deny is respected even with global default: allow
5462        let result = task.await;
5463        assert!(
5464            result.is_err(),
5465            "expected command to be blocked by tool-specific deny default"
5466        );
5467        let err_msg = result.unwrap_err().to_lowercase();
5468        assert!(
5469            err_msg.contains("disabled"),
5470            "error should mention the tool is disabled, got: {err_msg}"
5471        );
5472    }
5473}
5474
5475#[gpui::test]
5476async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) {
5477    init_test(cx);
5478    cx.update(|cx| {
5479        LanguageModelRegistry::test(cx);
5480    });
5481    cx.update(|cx| {
5482        cx.update_flags(true, vec!["subagents".to_string()]);
5483    });
5484
5485    let fs = FakeFs::new(cx.executor());
5486    fs.insert_tree(
5487        "/",
5488        json!({
5489            "a": {
5490                "b.md": "Lorem"
5491            }
5492        }),
5493    )
5494    .await;
5495    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
5496    let thread_store = cx.new(|cx| ThreadStore::new(cx));
5497    let agent =
5498        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
5499    let connection = Rc::new(NativeAgentConnection(agent.clone()));
5500
5501    let acp_thread = cx
5502        .update(|cx| {
5503            connection
5504                .clone()
5505                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
5506        })
5507        .await
5508        .unwrap();
5509    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
5510    let thread = agent.read_with(cx, |agent, _| {
5511        agent.sessions.get(&session_id).unwrap().thread.clone()
5512    });
5513    let model = Arc::new(FakeLanguageModel::default());
5514
5515    // Ensure empty threads are not saved, even if they get mutated.
5516    thread.update(cx, |thread, cx| {
5517        thread.set_model(model.clone(), cx);
5518    });
5519    cx.run_until_parked();
5520
5521    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx));
5522    cx.run_until_parked();
5523    model.send_last_completion_stream_text_chunk("spawning subagent");
5524    let subagent_tool_input = SpawnAgentToolInput {
5525        label: "label".to_string(),
5526        message: "subagent task prompt".to_string(),
5527        session_id: None,
5528    };
5529    let subagent_tool_use = LanguageModelToolUse {
5530        id: "subagent_1".into(),
5531        name: SpawnAgentTool::NAME.into(),
5532        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
5533        input: language_model::LanguageModelToolUseInput::Json(
5534            serde_json::to_value(&subagent_tool_input).unwrap(),
5535        ),
5536        is_input_complete: true,
5537        thought_signature: None,
5538    };
5539    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
5540        subagent_tool_use,
5541    ));
5542    model.end_last_completion_stream();
5543
5544    cx.run_until_parked();
5545
5546    let subagent_session_id = thread.read_with(cx, |thread, cx| {
5547        thread
5548            .running_subagent_ids(cx)
5549            .get(0)
5550            .expect("subagent thread should be running")
5551            .clone()
5552    });
5553
5554    let subagent_thread = agent.read_with(cx, |agent, _cx| {
5555        agent
5556            .sessions
5557            .get(&subagent_session_id)
5558            .expect("subagent session should exist")
5559            .acp_thread
5560            .clone()
5561    });
5562
5563    model.send_last_completion_stream_text_chunk("subagent task response");
5564    model.end_last_completion_stream();
5565
5566    cx.run_until_parked();
5567
5568    assert_eq!(
5569        subagent_thread.read_with(cx, |thread, cx| thread.to_markdown(cx)),
5570        indoc! {"
5571            ## User
5572
5573            subagent task prompt
5574
5575            ## Assistant
5576
5577            subagent task response
5578
5579        "}
5580    );
5581
5582    model.send_last_completion_stream_text_chunk("Response");
5583    model.end_last_completion_stream();
5584
5585    send.await.unwrap();
5586
5587    assert_eq!(
5588        acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx)),
5589        indoc! {r#"
5590            ## User
5591
5592            Prompt
5593
5594            ## Assistant
5595
5596            spawning subagent
5597
5598            **Tool Call: label**
5599            Status: Completed
5600
5601            subagent task response
5602
5603            ## Assistant
5604
5605            Response
5606
5607        "#},
5608    );
5609}
5610
5611#[gpui::test]
5612async fn test_subagent_tool_output_does_not_include_thinking(cx: &mut TestAppContext) {
5613    init_test(cx);
5614    cx.update(|cx| {
5615        LanguageModelRegistry::test(cx);
5616    });
5617    cx.update(|cx| {
5618        cx.update_flags(true, vec!["subagents".to_string()]);
5619    });
5620
5621    let fs = FakeFs::new(cx.executor());
5622    fs.insert_tree(
5623        "/",
5624        json!({
5625            "a": {
5626                "b.md": "Lorem"
5627            }
5628        }),
5629    )
5630    .await;
5631    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
5632    let thread_store = cx.new(|cx| ThreadStore::new(cx));
5633    let agent =
5634        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
5635    let connection = Rc::new(NativeAgentConnection(agent.clone()));
5636
5637    let acp_thread = cx
5638        .update(|cx| {
5639            connection
5640                .clone()
5641                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
5642        })
5643        .await
5644        .unwrap();
5645    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
5646    let thread = agent.read_with(cx, |agent, _| {
5647        agent.sessions.get(&session_id).unwrap().thread.clone()
5648    });
5649    let model = Arc::new(FakeLanguageModel::default());
5650
5651    // Ensure empty threads are not saved, even if they get mutated.
5652    thread.update(cx, |thread, cx| {
5653        thread.set_model(model.clone(), cx);
5654    });
5655    cx.run_until_parked();
5656
5657    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx));
5658    cx.run_until_parked();
5659    model.send_last_completion_stream_text_chunk("spawning subagent");
5660    let subagent_tool_input = SpawnAgentToolInput {
5661        label: "label".to_string(),
5662        message: "subagent task prompt".to_string(),
5663        session_id: None,
5664    };
5665    let subagent_tool_use = LanguageModelToolUse {
5666        id: "subagent_1".into(),
5667        name: SpawnAgentTool::NAME.into(),
5668        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
5669        input: language_model::LanguageModelToolUseInput::Json(
5670            serde_json::to_value(&subagent_tool_input).unwrap(),
5671        ),
5672        is_input_complete: true,
5673        thought_signature: None,
5674    };
5675    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
5676        subagent_tool_use,
5677    ));
5678    model.end_last_completion_stream();
5679
5680    cx.run_until_parked();
5681
5682    let subagent_session_id = thread.read_with(cx, |thread, cx| {
5683        thread
5684            .running_subagent_ids(cx)
5685            .get(0)
5686            .expect("subagent thread should be running")
5687            .clone()
5688    });
5689
5690    let subagent_thread = agent.read_with(cx, |agent, _cx| {
5691        agent
5692            .sessions
5693            .get(&subagent_session_id)
5694            .expect("subagent session should exist")
5695            .acp_thread
5696            .clone()
5697    });
5698
5699    model.send_last_completion_stream_text_chunk("subagent task response 1");
5700    model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking {
5701        text: "thinking more about the subagent task".into(),
5702        signature: None,
5703    });
5704    model.send_last_completion_stream_text_chunk("subagent task response 2");
5705    model.end_last_completion_stream();
5706
5707    cx.run_until_parked();
5708
5709    assert_eq!(
5710        subagent_thread.read_with(cx, |thread, cx| thread.to_markdown(cx)),
5711        indoc! {"
5712            ## User
5713
5714            subagent task prompt
5715
5716            ## Assistant
5717
5718            subagent task response 1
5719
5720            <thinking>
5721            thinking more about the subagent task
5722            </thinking>
5723
5724            subagent task response 2
5725
5726        "}
5727    );
5728
5729    model.send_last_completion_stream_text_chunk("Response");
5730    model.end_last_completion_stream();
5731
5732    send.await.unwrap();
5733
5734    assert_eq!(
5735        acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx)),
5736        indoc! {r#"
5737            ## User
5738
5739            Prompt
5740
5741            ## Assistant
5742
5743            spawning subagent
5744
5745            **Tool Call: label**
5746            Status: Completed
5747
5748            subagent task response 1
5749
5750            subagent task response 2
5751
5752            ## Assistant
5753
5754            Response
5755
5756        "#},
5757    );
5758}
5759
5760#[gpui::test]
5761async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAppContext) {
5762    init_test(cx);
5763    cx.update(|cx| {
5764        LanguageModelRegistry::test(cx);
5765    });
5766    cx.update(|cx| {
5767        cx.update_flags(true, vec!["subagents".to_string()]);
5768    });
5769
5770    let fs = FakeFs::new(cx.executor());
5771    fs.insert_tree(
5772        "/",
5773        json!({
5774            "a": {
5775                "b.md": "Lorem"
5776            }
5777        }),
5778    )
5779    .await;
5780    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
5781    let thread_store = cx.new(|cx| ThreadStore::new(cx));
5782    let agent =
5783        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
5784    let connection = Rc::new(NativeAgentConnection(agent.clone()));
5785
5786    let acp_thread = cx
5787        .update(|cx| {
5788            connection
5789                .clone()
5790                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
5791        })
5792        .await
5793        .unwrap();
5794    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
5795    let thread = agent.read_with(cx, |agent, _| {
5796        agent.sessions.get(&session_id).unwrap().thread.clone()
5797    });
5798    let model = Arc::new(FakeLanguageModel::default());
5799
5800    // Ensure empty threads are not saved, even if they get mutated.
5801    thread.update(cx, |thread, cx| {
5802        thread.set_model(model.clone(), cx);
5803    });
5804    cx.run_until_parked();
5805
5806    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx));
5807    cx.run_until_parked();
5808    model.send_last_completion_stream_text_chunk("spawning subagent");
5809    let subagent_tool_input = SpawnAgentToolInput {
5810        label: "label".to_string(),
5811        message: "subagent task prompt".to_string(),
5812        session_id: None,
5813    };
5814    let subagent_tool_use = LanguageModelToolUse {
5815        id: "subagent_1".into(),
5816        name: SpawnAgentTool::NAME.into(),
5817        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
5818        input: language_model::LanguageModelToolUseInput::Json(
5819            serde_json::to_value(&subagent_tool_input).unwrap(),
5820        ),
5821        is_input_complete: true,
5822        thought_signature: None,
5823    };
5824    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
5825        subagent_tool_use,
5826    ));
5827    model.end_last_completion_stream();
5828
5829    cx.run_until_parked();
5830
5831    let subagent_session_id = thread.read_with(cx, |thread, cx| {
5832        thread
5833            .running_subagent_ids(cx)
5834            .get(0)
5835            .expect("subagent thread should be running")
5836            .clone()
5837    });
5838    let subagent_acp_thread = agent.read_with(cx, |agent, _cx| {
5839        agent
5840            .sessions
5841            .get(&subagent_session_id)
5842            .expect("subagent session should exist")
5843            .acp_thread
5844            .clone()
5845    });
5846
5847    // model.send_last_completion_stream_text_chunk("subagent task response");
5848    // model.end_last_completion_stream();
5849
5850    // cx.run_until_parked();
5851
5852    acp_thread.update(cx, |thread, cx| thread.cancel(cx)).await;
5853
5854    cx.run_until_parked();
5855
5856    send.await.unwrap();
5857
5858    acp_thread.read_with(cx, |thread, cx| {
5859        assert_eq!(thread.status(), ThreadStatus::Idle);
5860        assert_eq!(
5861            thread.to_markdown(cx),
5862            indoc! {"
5863                ## User
5864
5865                Prompt
5866
5867                ## Assistant
5868
5869                spawning subagent
5870
5871                **Tool Call: label**
5872                Status: Canceled
5873
5874            "}
5875        );
5876    });
5877    subagent_acp_thread.read_with(cx, |thread, cx| {
5878        assert_eq!(thread.status(), ThreadStatus::Idle);
5879        assert_eq!(
5880            thread.to_markdown(cx),
5881            indoc! {"
5882                ## User
5883
5884                subagent task prompt
5885
5886            "}
5887        );
5888    });
5889}
5890
5891#[gpui::test]
5892async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
5893    init_test(cx);
5894    cx.update(|cx| {
5895        LanguageModelRegistry::test(cx);
5896    });
5897    cx.update(|cx| {
5898        cx.update_flags(true, vec!["subagents".to_string()]);
5899    });
5900
5901    let fs = FakeFs::new(cx.executor());
5902    fs.insert_tree(
5903        "/",
5904        json!({
5905            "a": {
5906                "b.md": "Lorem"
5907            }
5908        }),
5909    )
5910    .await;
5911    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
5912    let thread_store = cx.new(|cx| ThreadStore::new(cx));
5913    let agent =
5914        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
5915    let connection = Rc::new(NativeAgentConnection(agent.clone()));
5916
5917    let acp_thread = cx
5918        .update(|cx| {
5919            connection
5920                .clone()
5921                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
5922        })
5923        .await
5924        .unwrap();
5925    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
5926    let thread = agent.read_with(cx, |agent, _| {
5927        agent.sessions.get(&session_id).unwrap().thread.clone()
5928    });
5929    let model = Arc::new(FakeLanguageModel::default());
5930
5931    thread.update(cx, |thread, cx| {
5932        thread.set_model(model.clone(), cx);
5933    });
5934    cx.run_until_parked();
5935
5936    // === First turn: create subagent ===
5937    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("First prompt", cx));
5938    cx.run_until_parked();
5939    model.send_last_completion_stream_text_chunk("spawning subagent");
5940    let subagent_tool_input = SpawnAgentToolInput {
5941        label: "initial task".to_string(),
5942        message: "do the first task".to_string(),
5943        session_id: None,
5944    };
5945    let subagent_tool_use = LanguageModelToolUse {
5946        id: "subagent_1".into(),
5947        name: SpawnAgentTool::NAME.into(),
5948        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
5949        input: language_model::LanguageModelToolUseInput::Json(
5950            serde_json::to_value(&subagent_tool_input).unwrap(),
5951        ),
5952        is_input_complete: true,
5953        thought_signature: None,
5954    };
5955    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
5956        subagent_tool_use,
5957    ));
5958    model.end_last_completion_stream();
5959
5960    cx.run_until_parked();
5961
5962    let subagent_session_id = thread.read_with(cx, |thread, cx| {
5963        thread
5964            .running_subagent_ids(cx)
5965            .get(0)
5966            .expect("subagent thread should be running")
5967            .clone()
5968    });
5969
5970    let subagent_acp_thread = agent.read_with(cx, |agent, _cx| {
5971        agent
5972            .sessions
5973            .get(&subagent_session_id)
5974            .expect("subagent session should exist")
5975            .acp_thread
5976            .clone()
5977    });
5978
5979    // Subagent responds
5980    model.send_last_completion_stream_text_chunk("first task response");
5981    model.end_last_completion_stream();
5982
5983    cx.run_until_parked();
5984
5985    // Parent model responds to complete first turn
5986    model.send_last_completion_stream_text_chunk("First response");
5987    model.end_last_completion_stream();
5988
5989    send.await.unwrap();
5990
5991    // Verify subagent is no longer running
5992    thread.read_with(cx, |thread, cx| {
5993        assert!(
5994            thread.running_subagent_ids(cx).is_empty(),
5995            "subagent should not be running after completion"
5996        );
5997    });
5998
5999    // === Second turn: resume subagent with session_id ===
6000    let send2 = acp_thread.update(cx, |thread, cx| thread.send_raw("Follow up", cx));
6001    cx.run_until_parked();
6002    model.send_last_completion_stream_text_chunk("resuming subagent");
6003    let resume_tool_input = SpawnAgentToolInput {
6004        label: "follow-up task".to_string(),
6005        message: "do the follow-up task".to_string(),
6006        session_id: Some(subagent_session_id.clone()),
6007    };
6008    let resume_tool_use = LanguageModelToolUse {
6009        id: "subagent_2".into(),
6010        name: SpawnAgentTool::NAME.into(),
6011        raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
6012        input: language_model::LanguageModelToolUseInput::Json(
6013            serde_json::to_value(&resume_tool_input).unwrap(),
6014        ),
6015        is_input_complete: true,
6016        thought_signature: None,
6017    };
6018    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(resume_tool_use));
6019    model.end_last_completion_stream();
6020
6021    cx.run_until_parked();
6022
6023    // Subagent should be running again with the same session
6024    thread.read_with(cx, |thread, cx| {
6025        let running = thread.running_subagent_ids(cx);
6026        assert_eq!(running.len(), 1, "subagent should be running");
6027        assert_eq!(running[0], subagent_session_id, "should be same session");
6028    });
6029
6030    // Subagent responds to follow-up
6031    model.send_last_completion_stream_text_chunk("follow-up task response");
6032    model.end_last_completion_stream();
6033
6034    cx.run_until_parked();
6035
6036    // Parent model responds to complete second turn
6037    model.send_last_completion_stream_text_chunk("Second response");
6038    model.end_last_completion_stream();
6039
6040    send2.await.unwrap();
6041
6042    // Verify subagent is no longer running
6043    thread.read_with(cx, |thread, cx| {
6044        assert!(
6045            thread.running_subagent_ids(cx).is_empty(),
6046            "subagent should not be running after resume completion"
6047        );
6048    });
6049
6050    // Verify the subagent's acp thread has both conversation turns
6051    assert_eq!(
6052        subagent_acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx)),
6053        indoc! {"
6054            ## User
6055
6056            do the first task
6057
6058            ## Assistant
6059
6060            first task response
6061
6062            ## User
6063
6064            do the follow-up task
6065
6066            ## Assistant
6067
6068            follow-up task response
6069
6070        "}
6071    );
6072}
6073
6074#[gpui::test]
6075async fn test_subagent_thread_inherits_parent_thread_properties(cx: &mut TestAppContext) {
6076    init_test(cx);
6077
6078    cx.update(|cx| {
6079        cx.update_flags(true, vec!["subagents".to_string()]);
6080    });
6081
6082    let fs = FakeFs::new(cx.executor());
6083    fs.insert_tree(path!("/test"), json!({})).await;
6084    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6085    let project_context = cx.new(|_cx| ProjectContext::default());
6086    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6087    let context_server_registry =
6088        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6089    let model = Arc::new(FakeLanguageModel::default());
6090
6091    let parent_thread = cx.new(|cx| {
6092        Thread::new(
6093            project.clone(),
6094            project_context,
6095            context_server_registry,
6096            Templates::new(),
6097            Some(model.clone()),
6098            cx,
6099        )
6100    });
6101
6102    let subagent_thread = cx.new(|cx| Thread::new_subagent(&parent_thread, cx));
6103    subagent_thread.read_with(cx, |subagent_thread, cx| {
6104        assert!(subagent_thread.is_subagent());
6105        assert_eq!(subagent_thread.depth(), 1);
6106        assert_eq!(
6107            subagent_thread.model().map(|model| model.id()),
6108            Some(model.id())
6109        );
6110        assert_eq!(
6111            subagent_thread.parent_thread_id(),
6112            Some(parent_thread.read(cx).id().clone())
6113        );
6114
6115        let request = subagent_thread
6116            .build_completion_request(CompletionIntent::UserPrompt, cx)
6117            .unwrap();
6118        assert_eq!(request.intent, Some(CompletionIntent::Subagent));
6119    });
6120}
6121
6122#[gpui::test]
6123async fn test_subagent_thread_uses_configured_subagent_model(cx: &mut TestAppContext) {
6124    init_test(cx);
6125
6126    let fs = FakeFs::new(cx.executor());
6127    fs.insert_tree(path!("/test"), json!({})).await;
6128    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6129    let project_context = cx.new(|_cx| ProjectContext::default());
6130    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6131    let context_server_registry =
6132        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6133    let parent_model = Arc::new(FakeLanguageModel::default());
6134    let subagent_model = Arc::new(FakeLanguageModel::with_id_and_thinking(
6135        "fake-corp",
6136        "subagent-model",
6137        "Subagent Model",
6138        true,
6139    ));
6140
6141    cx.update(|cx| {
6142        LanguageModelRegistry::test(cx);
6143
6144        let provider = Arc::new(
6145            FakeLanguageModelProvider::new(
6146                LanguageModelProviderId::from("fake-corp".to_string()),
6147                LanguageModelProviderName::from("Fake Corp".to_string()),
6148            )
6149            .with_models(vec![subagent_model.clone()]),
6150        );
6151        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
6152            registry.register_provider(provider, cx);
6153        });
6154
6155        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
6156        settings.subagent_model = Some(LanguageModelSelection {
6157            provider: LanguageModelProviderSetting("fake-corp".to_string()),
6158            model: "subagent-model".to_string(),
6159            enable_thinking: true,
6160            effort: Some("high".to_string()),
6161            speed: None,
6162        });
6163        agent_settings::AgentSettings::override_global(settings, cx);
6164    });
6165
6166    let parent_thread = cx.new(|cx| {
6167        Thread::new(
6168            project.clone(),
6169            project_context,
6170            context_server_registry,
6171            Templates::new(),
6172            Some(parent_model.clone()),
6173            cx,
6174        )
6175    });
6176
6177    let subagent_thread = cx.new(|cx| Thread::new_subagent(&parent_thread, cx));
6178    subagent_thread.read_with(cx, |subagent_thread, _cx| {
6179        assert_eq!(
6180            subagent_thread.model().map(|model| model.id()),
6181            Some(subagent_model.id())
6182        );
6183        assert!(subagent_thread.thinking_enabled());
6184        assert_eq!(subagent_thread.thinking_effort(), Some(&"high".to_string()));
6185    });
6186
6187    parent_thread.update(cx, |parent_thread, _cx| {
6188        parent_thread.register_running_subagent(subagent_thread.downgrade());
6189    });
6190    parent_thread.update(cx, |parent_thread, cx| {
6191        parent_thread.set_model(parent_model.clone(), cx);
6192        parent_thread.set_thinking_enabled(false, cx);
6193        parent_thread.set_thinking_effort(None, cx);
6194    });
6195
6196    subagent_thread.read_with(cx, |subagent_thread, _cx| {
6197        assert_eq!(
6198            subagent_thread.model().map(|model| model.id()),
6199            Some(subagent_model.id())
6200        );
6201        assert!(subagent_thread.thinking_enabled());
6202        assert_eq!(subagent_thread.thinking_effort(), Some(&"high".to_string()));
6203    });
6204}
6205
6206#[gpui::test]
6207async fn test_max_subagent_depth_prevents_tool_registration(cx: &mut TestAppContext) {
6208    init_test(cx);
6209
6210    cx.update(|cx| {
6211        cx.update_flags(true, vec!["subagents".to_string()]);
6212    });
6213
6214    let fs = FakeFs::new(cx.executor());
6215    fs.insert_tree(path!("/test"), json!({})).await;
6216    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6217    let project_context = cx.new(|_cx| ProjectContext::default());
6218    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6219    let context_server_registry =
6220        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6221    let model = Arc::new(FakeLanguageModel::default());
6222    let environment = Rc::new(cx.update(|cx| {
6223        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
6224    }));
6225
6226    let deep_parent_thread = cx.new(|cx| {
6227        let mut thread = Thread::new(
6228            project.clone(),
6229            project_context,
6230            context_server_registry,
6231            Templates::new(),
6232            Some(model.clone()),
6233            cx,
6234        );
6235        thread.set_subagent_context(SubagentContext {
6236            parent_thread_id: acp::SessionId::new("parent-id"),
6237            depth: MAX_SUBAGENT_DEPTH - 1,
6238        });
6239        thread
6240    });
6241    let deep_subagent_thread = cx.new(|cx| {
6242        let mut thread = Thread::new_subagent(&deep_parent_thread, cx);
6243        thread.add_default_tools(environment, cx);
6244        thread
6245    });
6246
6247    deep_subagent_thread.read_with(cx, |thread, _| {
6248        assert_eq!(thread.depth(), MAX_SUBAGENT_DEPTH);
6249        assert!(
6250            !thread.has_registered_tool(SpawnAgentTool::NAME),
6251            "subagent tool should not be present at max depth"
6252        );
6253    });
6254}
6255
6256#[gpui::test]
6257async fn test_lsp_tools_gated_by_feature_flag(cx: &mut TestAppContext) {
6258    init_test(cx);
6259
6260    let fs = FakeFs::new(cx.executor());
6261    fs.insert_tree(path!("/test"), json!({})).await;
6262    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6263    let project_context = cx.new(|_cx| ProjectContext::default());
6264    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6265    let context_server_registry =
6266        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6267    let model = Arc::new(FakeLanguageModel::default());
6268    let environment = Rc::new(cx.update(|cx| {
6269        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
6270    }));
6271
6272    let thread = cx.new(|cx| {
6273        let mut thread = Thread::new(
6274            project,
6275            project_context,
6276            context_server_registry,
6277            Templates::new(),
6278            Some(model.clone() as Arc<dyn LanguageModel>),
6279            cx,
6280        );
6281        thread.add_default_tools(environment, cx);
6282        thread
6283    });
6284
6285    let lsp_tool_names = [
6286        FindReferencesTool::NAME,
6287        GetCodeActionsTool::NAME,
6288        ApplyCodeActionTool::NAME,
6289        GoToDefinitionTool::NAME,
6290    ];
6291
6292    // All LSP tools and the rename tool should be registered on the thread
6293    // regardless of the flag, since the feature flags only control exposure
6294    // to the model rather than registration.
6295    thread.read_with(cx, |thread, _| {
6296        for name in &lsp_tool_names {
6297            assert!(
6298                thread.has_registered_tool(name),
6299                "expected LSP tool {name} to be registered"
6300            );
6301        }
6302        assert!(
6303            thread.has_registered_tool(RenameTool::NAME),
6304            "expected rename tool to be registered"
6305        );
6306    });
6307
6308    // Without the `lsp-tool` flag, sending a message should produce a
6309    // completion request whose tool list excludes the LSP tools.
6310    // The rename tool is on its own `rename-tool` flag with
6311    // `enabled_for_staff`, so it is already visible in debug builds.
6312    thread
6313        .update(cx, |thread, cx| {
6314            thread.send(ClientUserMessageId::new(), ["hello"], cx)
6315        })
6316        .unwrap();
6317    cx.run_until_parked();
6318
6319    let completion = model.pending_completions().pop().unwrap();
6320    let tool_names = tool_names_for_completion(&completion);
6321    for name in &lsp_tool_names {
6322        assert!(
6323            !tool_names.iter().any(|t| t == name),
6324            "expected LSP tool {name} to be hidden without the lsp-tool flag, \
6325             but completion tools were: {tool_names:?}"
6326        );
6327    }
6328    assert!(
6329        tool_names.iter().any(|t| t == RenameTool::NAME),
6330        "expected rename tool to be visible (enabled_for_staff in debug builds), \
6331         but completion tools were: {tool_names:?}"
6332    );
6333    // Sanity check: a non-LSP default tool should still be exposed.
6334    assert!(
6335        tool_names.iter().any(|t| t == ReadFileTool::NAME),
6336        "expected non-LSP tools to still be exposed, got: {tool_names:?}"
6337    );
6338    model.end_last_completion_stream();
6339    cx.run_until_parked();
6340
6341    // Enable the `lsp-tool` flag and send another message; the LSP tools
6342    // should now appear in the completion request.
6343    cx.update(|cx| {
6344        cx.update_flags(false, vec!["lsp-tool".to_string()]);
6345    });
6346
6347    thread
6348        .update(cx, |thread, cx| {
6349            thread.send(ClientUserMessageId::new(), ["hello again"], cx)
6350        })
6351        .unwrap();
6352    cx.run_until_parked();
6353
6354    let completion = model.pending_completions().pop().unwrap();
6355    let tool_names = tool_names_for_completion(&completion);
6356    for name in &lsp_tool_names {
6357        assert!(
6358            tool_names.iter().any(|t| t == name),
6359            "expected LSP tool {name} to be exposed when lsp-tool flag is on, \
6360             but completion tools were: {tool_names:?}"
6361        );
6362    }
6363    assert!(
6364        tool_names.iter().any(|t| t == RenameTool::NAME),
6365        "expected rename tool to still be exposed, \
6366         but completion tools were: {tool_names:?}"
6367    );
6368}
6369
6370#[gpui::test]
6371async fn test_sibling_thread_tools_gated_by_feature_flag(cx: &mut TestAppContext) {
6372    init_test(cx);
6373
6374    // `CreateThreadToolFeatureFlag::enabled_for_staff()` returns true, which
6375    // means tests in debug builds resolve it to ON unless we explicitly
6376    // override it via `FeatureFlagsSettings`. Register the settings type and
6377    // install an (empty) `FeatureFlagStore` global so the `cx.has_flag` path
6378    // actually consults overrides instead of falling back to the
6379    // staff-debug-build default.
6380    cx.update(|cx| {
6381        SettingsStore::update_global(cx, |store, _| {
6382            store.register_setting::<feature_flags::FeatureFlagsSettings>();
6383        });
6384        cx.update_flags(false, vec![]);
6385    });
6386
6387    fn set_flag_override(value: &str, cx: &mut TestAppContext) {
6388        cx.update(|cx| {
6389            SettingsStore::update_global(cx, |store, cx| {
6390                store.update_user_settings(cx, |content| {
6391                    content
6392                        .feature_flags
6393                        .get_or_insert_default()
6394                        .insert("create-thread-tool".to_string(), value.to_string());
6395                });
6396            });
6397        });
6398    }
6399
6400    let fs = FakeFs::new(cx.executor());
6401    fs.insert_tree(path!("/test"), json!({})).await;
6402    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6403    let project_context = cx.new(|_cx| ProjectContext::default());
6404    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6405    let context_server_registry =
6406        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6407    let model = Arc::new(FakeLanguageModel::default());
6408    let environment = Rc::new(cx.update(|cx| {
6409        FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
6410    }));
6411
6412    let thread = cx.new(|cx| {
6413        let mut thread = Thread::new(
6414            project,
6415            project_context,
6416            context_server_registry,
6417            Templates::new(),
6418            Some(model.clone() as Arc<dyn LanguageModel>),
6419            cx,
6420        );
6421        thread.add_default_tools(environment, cx);
6422        thread
6423    });
6424
6425    let sibling_tool_names = [CreateThreadTool::NAME, ListAgentsAndModelsTool::NAME];
6426
6427    // Like the LSP/rename tools, sibling-thread tools are registered
6428    // unconditionally and gated only at exposure time. The registration must
6429    // be visible regardless of the flag's current value.
6430    thread.read_with(cx, |thread, _| {
6431        for name in &sibling_tool_names {
6432            assert!(
6433                thread.has_registered_tool(name),
6434                "expected sibling-thread tool {name} to be registered"
6435            );
6436        }
6437    });
6438
6439    // Flag explicitly off: a completion request must omit the tools.
6440    set_flag_override("off", cx);
6441    thread
6442        .update(cx, |thread, cx| {
6443            thread.send(ClientUserMessageId::new(), ["hello"], cx)
6444        })
6445        .unwrap();
6446    cx.run_until_parked();
6447
6448    let completion = model.pending_completions().pop().unwrap();
6449    let tool_names = tool_names_for_completion(&completion);
6450    for name in &sibling_tool_names {
6451        assert!(
6452            !tool_names.iter().any(|t| t == name),
6453            "expected {name} to be hidden when create-thread-tool flag is off, \
6454             but completion tools were: {tool_names:?}"
6455        );
6456    }
6457    // Sanity check: an unrelated default tool should still be exposed.
6458    assert!(
6459        tool_names.iter().any(|t| t == ReadFileTool::NAME),
6460        "expected non-sibling-thread tools to still be exposed, got: {tool_names:?}"
6461    );
6462    model.end_last_completion_stream();
6463    cx.run_until_parked();
6464
6465    // Flag explicitly on: the next completion request must include both tools.
6466    set_flag_override("on", cx);
6467    thread
6468        .update(cx, |thread, cx| {
6469            thread.send(ClientUserMessageId::new(), ["hello again"], cx)
6470        })
6471        .unwrap();
6472    cx.run_until_parked();
6473
6474    let completion = model.pending_completions().pop().unwrap();
6475    let tool_names = tool_names_for_completion(&completion);
6476    for name in &sibling_tool_names {
6477        assert!(
6478            tool_names.iter().any(|t| t == name),
6479            "expected {name} to be exposed when create-thread-tool flag is on, \
6480             but completion tools were: {tool_names:?}"
6481        );
6482    }
6483}
6484
6485#[gpui::test]
6486async fn test_parent_cancel_stops_subagent(cx: &mut TestAppContext) {
6487    init_test(cx);
6488
6489    cx.update(|cx| {
6490        cx.update_flags(true, vec!["subagents".to_string()]);
6491    });
6492
6493    let fs = FakeFs::new(cx.executor());
6494    fs.insert_tree(path!("/test"), json!({})).await;
6495    let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6496    let project_context = cx.new(|_cx| ProjectContext::default());
6497    let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
6498    let context_server_registry =
6499        cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
6500    let model = Arc::new(FakeLanguageModel::default());
6501
6502    let parent = cx.new(|cx| {
6503        Thread::new(
6504            project.clone(),
6505            project_context.clone(),
6506            context_server_registry.clone(),
6507            Templates::new(),
6508            Some(model.clone()),
6509            cx,
6510        )
6511    });
6512
6513    let subagent = cx.new(|cx| Thread::new_subagent(&parent, cx));
6514
6515    parent.update(cx, |thread, _cx| {
6516        thread.register_running_subagent(subagent.downgrade());
6517    });
6518
6519    subagent
6520        .update(cx, |thread, cx| {
6521            thread.send(ClientUserMessageId::new(), ["Do work".to_string()], cx)
6522        })
6523        .unwrap();
6524    cx.run_until_parked();
6525
6526    subagent.read_with(cx, |thread, _| {
6527        assert!(!thread.is_turn_complete(), "subagent should be running");
6528    });
6529
6530    parent.update(cx, |thread, cx| {
6531        thread.cancel(cx).detach();
6532    });
6533
6534    subagent.read_with(cx, |thread, _| {
6535        assert!(
6536            thread.is_turn_complete(),
6537            "subagent should be cancelled when parent cancels"
6538        );
6539    });
6540}
6541
6542#[gpui::test]
6543async fn test_subagent_context_window_warning(cx: &mut TestAppContext) {
6544    init_test(cx);
6545    cx.update(|cx| {
6546        LanguageModelRegistry::test(cx);
6547    });
6548    cx.update(|cx| {
6549        cx.update_flags(true, vec!["subagents".to_string()]);
6550    });
6551
6552    let fs = FakeFs::new(cx.executor());
6553    fs.insert_tree(
6554        "/",
6555        json!({
6556            "a": {
6557                "b.md": "Lorem"
6558            }
6559        }),
6560    )
6561    .await;
6562    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
6563    let thread_store = cx.new(|cx| ThreadStore::new(cx));
6564    let agent =
6565        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
6566    let connection = Rc::new(NativeAgentConnection(agent.clone()));
6567
6568    let acp_thread = cx
6569        .update(|cx| {
6570            connection
6571                .clone()
6572                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
6573        })
6574        .await
6575        .unwrap();
6576    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
6577    let thread = agent.read_with(cx, |agent, _| {
6578        agent.sessions.get(&session_id).unwrap().thread.clone()
6579    });
6580    let model = Arc::new(FakeLanguageModel::default());
6581
6582    thread.update(cx, |thread, cx| {
6583        thread.set_model(model.clone(), cx);
6584    });
6585    cx.run_until_parked();
6586
6587    // Start the parent turn
6588    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx));
6589    cx.run_until_parked();
6590    model.send_last_completion_stream_text_chunk("spawning subagent");
6591    let subagent_tool_input = SpawnAgentToolInput {
6592        label: "label".to_string(),
6593        message: "subagent task prompt".to_string(),
6594        session_id: None,
6595    };
6596    let subagent_tool_use = LanguageModelToolUse {
6597        id: "subagent_1".into(),
6598        name: SpawnAgentTool::NAME.into(),
6599        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
6600        input: language_model::LanguageModelToolUseInput::Json(
6601            serde_json::to_value(&subagent_tool_input).unwrap(),
6602        ),
6603        is_input_complete: true,
6604        thought_signature: None,
6605    };
6606    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
6607        subagent_tool_use,
6608    ));
6609    model.end_last_completion_stream();
6610
6611    cx.run_until_parked();
6612
6613    // Verify subagent is running
6614    let subagent_session_id = thread.read_with(cx, |thread, cx| {
6615        thread
6616            .running_subagent_ids(cx)
6617            .get(0)
6618            .expect("subagent thread should be running")
6619            .clone()
6620    });
6621
6622    // Send a usage update that crosses the warning threshold (80% of 1,000,000)
6623    model.send_last_completion_stream_text_chunk("partial work");
6624    model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
6625        TokenUsage {
6626            input_tokens: 850_000,
6627            output_tokens: 0,
6628            cache_creation_input_tokens: 0,
6629            cache_read_input_tokens: 0,
6630        },
6631    ));
6632
6633    cx.run_until_parked();
6634
6635    // The subagent should no longer be running
6636    thread.read_with(cx, |thread, cx| {
6637        assert!(
6638            thread.running_subagent_ids(cx).is_empty(),
6639            "subagent should be stopped after context window warning"
6640        );
6641    });
6642
6643    // The parent model should get a new completion request to respond to the tool error
6644    model.send_last_completion_stream_text_chunk("Response after warning");
6645    model.end_last_completion_stream();
6646
6647    send.await.unwrap();
6648
6649    // Verify the parent thread shows the warning error in the tool call
6650    let markdown = acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
6651    assert!(
6652        markdown.contains("nearing the end of its context window"),
6653        "tool output should contain context window warning message, got:\n{markdown}"
6654    );
6655    assert!(
6656        markdown.contains("Status: Failed"),
6657        "tool call should have Failed status, got:\n{markdown}"
6658    );
6659
6660    // Verify the subagent session still exists (can be resumed)
6661    agent.read_with(cx, |agent, _cx| {
6662        assert!(
6663            agent.sessions.contains_key(&subagent_session_id),
6664            "subagent session should still exist for potential resume"
6665        );
6666    });
6667}
6668
6669#[gpui::test]
6670async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mut TestAppContext) {
6671    init_test(cx);
6672    cx.update(|cx| {
6673        LanguageModelRegistry::test(cx);
6674    });
6675    cx.update(|cx| {
6676        cx.update_flags(true, vec!["subagents".to_string()]);
6677    });
6678
6679    let fs = FakeFs::new(cx.executor());
6680    fs.insert_tree(
6681        "/",
6682        json!({
6683            "a": {
6684                "b.md": "Lorem"
6685            }
6686        }),
6687    )
6688    .await;
6689    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
6690    let thread_store = cx.new(|cx| ThreadStore::new(cx));
6691    let agent =
6692        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
6693    let connection = Rc::new(NativeAgentConnection(agent.clone()));
6694
6695    let acp_thread = cx
6696        .update(|cx| {
6697            connection
6698                .clone()
6699                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
6700        })
6701        .await
6702        .unwrap();
6703    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
6704    let thread = agent.read_with(cx, |agent, _| {
6705        agent.sessions.get(&session_id).unwrap().thread.clone()
6706    });
6707    let model = Arc::new(FakeLanguageModel::default());
6708
6709    thread.update(cx, |thread, cx| {
6710        thread.set_model(model.clone(), cx);
6711    });
6712    cx.run_until_parked();
6713
6714    // === First turn: create subagent, trigger context window warning ===
6715    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("First prompt", cx));
6716    cx.run_until_parked();
6717    model.send_last_completion_stream_text_chunk("spawning subagent");
6718    let subagent_tool_input = SpawnAgentToolInput {
6719        label: "initial task".to_string(),
6720        message: "do the first task".to_string(),
6721        session_id: None,
6722    };
6723    let subagent_tool_use = LanguageModelToolUse {
6724        id: "subagent_1".into(),
6725        name: SpawnAgentTool::NAME.into(),
6726        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
6727        input: language_model::LanguageModelToolUseInput::Json(
6728            serde_json::to_value(&subagent_tool_input).unwrap(),
6729        ),
6730        is_input_complete: true,
6731        thought_signature: None,
6732    };
6733    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
6734        subagent_tool_use,
6735    ));
6736    model.end_last_completion_stream();
6737
6738    cx.run_until_parked();
6739
6740    let subagent_session_id = thread.read_with(cx, |thread, cx| {
6741        thread
6742            .running_subagent_ids(cx)
6743            .get(0)
6744            .expect("subagent thread should be running")
6745            .clone()
6746    });
6747
6748    // Subagent sends a usage update that crosses the warning threshold.
6749    // This triggers Normal→Warning, stopping the subagent.
6750    model.send_last_completion_stream_text_chunk("partial work");
6751    model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
6752        TokenUsage {
6753            input_tokens: 850_000,
6754            output_tokens: 0,
6755            cache_creation_input_tokens: 0,
6756            cache_read_input_tokens: 0,
6757        },
6758    ));
6759
6760    cx.run_until_parked();
6761
6762    // Verify the first turn was stopped with a context window warning
6763    thread.read_with(cx, |thread, cx| {
6764        assert!(
6765            thread.running_subagent_ids(cx).is_empty(),
6766            "subagent should be stopped after context window warning"
6767        );
6768    });
6769
6770    // Parent model responds to complete first turn
6771    model.send_last_completion_stream_text_chunk("First response");
6772    model.end_last_completion_stream();
6773
6774    send.await.unwrap();
6775
6776    let markdown = acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
6777    assert!(
6778        markdown.contains("nearing the end of its context window"),
6779        "first turn should have context window warning, got:\n{markdown}"
6780    );
6781
6782    // === Second turn: resume the same subagent (now at Warning level) ===
6783    let send2 = acp_thread.update(cx, |thread, cx| thread.send_raw("Follow up", cx));
6784    cx.run_until_parked();
6785    model.send_last_completion_stream_text_chunk("resuming subagent");
6786    let resume_tool_input = SpawnAgentToolInput {
6787        label: "follow-up task".to_string(),
6788        message: "do the follow-up task".to_string(),
6789        session_id: Some(subagent_session_id.clone()),
6790    };
6791    let resume_tool_use = LanguageModelToolUse {
6792        id: "subagent_2".into(),
6793        name: SpawnAgentTool::NAME.into(),
6794        raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
6795        input: language_model::LanguageModelToolUseInput::Json(
6796            serde_json::to_value(&resume_tool_input).unwrap(),
6797        ),
6798        is_input_complete: true,
6799        thought_signature: None,
6800    };
6801    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(resume_tool_use));
6802    model.end_last_completion_stream();
6803
6804    cx.run_until_parked();
6805
6806    // Subagent responds with tokens still at warning level (no worse).
6807    // Since ratio_before_prompt was already Warning, this should NOT
6808    // trigger the context window warning again.
6809    model.send_last_completion_stream_text_chunk("follow-up task response");
6810    model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
6811        TokenUsage {
6812            input_tokens: 870_000,
6813            output_tokens: 0,
6814            cache_creation_input_tokens: 0,
6815            cache_read_input_tokens: 0,
6816        },
6817    ));
6818    model.end_last_completion_stream();
6819
6820    cx.run_until_parked();
6821
6822    // Parent model responds to complete second turn
6823    model.send_last_completion_stream_text_chunk("Second response");
6824    model.end_last_completion_stream();
6825
6826    send2.await.unwrap();
6827
6828    // The resumed subagent should have completed normally since the ratio
6829    // didn't transition (it was Warning before and stayed at Warning)
6830    let markdown = acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
6831    assert!(
6832        markdown.contains("follow-up task response"),
6833        "resumed subagent should complete normally when already at warning, got:\n{markdown}"
6834    );
6835    // The second tool call should NOT have a context window warning
6836    let second_tool_pos = markdown
6837        .find("follow-up task")
6838        .expect("should find follow-up tool call");
6839    let after_second_tool = &markdown[second_tool_pos..];
6840    assert!(
6841        !after_second_tool.contains("nearing the end of its context window"),
6842        "should NOT contain context window warning for resumed subagent at same level, got:\n{after_second_tool}"
6843    );
6844}
6845
6846#[gpui::test]
6847async fn test_subagent_error_propagation(cx: &mut TestAppContext) {
6848    init_test(cx);
6849    cx.update(|cx| {
6850        LanguageModelRegistry::test(cx);
6851    });
6852    cx.update(|cx| {
6853        cx.update_flags(true, vec!["subagents".to_string()]);
6854    });
6855
6856    let fs = FakeFs::new(cx.executor());
6857    fs.insert_tree(
6858        "/",
6859        json!({
6860            "a": {
6861                "b.md": "Lorem"
6862            }
6863        }),
6864    )
6865    .await;
6866    let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
6867    let thread_store = cx.new(|cx| ThreadStore::new(cx));
6868    let agent =
6869        cx.update(|cx| NativeAgent::new(thread_store.clone(), Templates::new(), fs.clone(), cx));
6870    let connection = Rc::new(NativeAgentConnection(agent.clone()));
6871
6872    let acp_thread = cx
6873        .update(|cx| {
6874            connection
6875                .clone()
6876                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
6877        })
6878        .await
6879        .unwrap();
6880    let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
6881    let thread = agent.read_with(cx, |agent, _| {
6882        agent.sessions.get(&session_id).unwrap().thread.clone()
6883    });
6884    let model = Arc::new(FakeLanguageModel::default());
6885
6886    thread.update(cx, |thread, cx| {
6887        thread.set_model(model.clone(), cx);
6888    });
6889    cx.run_until_parked();
6890
6891    // Start the parent turn
6892    let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx));
6893    cx.run_until_parked();
6894    model.send_last_completion_stream_text_chunk("spawning subagent");
6895    let subagent_tool_input = SpawnAgentToolInput {
6896        label: "label".to_string(),
6897        message: "subagent task prompt".to_string(),
6898        session_id: None,
6899    };
6900    let subagent_tool_use = LanguageModelToolUse {
6901        id: "subagent_1".into(),
6902        name: SpawnAgentTool::NAME.into(),
6903        raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
6904        input: language_model::LanguageModelToolUseInput::Json(
6905            serde_json::to_value(&subagent_tool_input).unwrap(),
6906        ),
6907        is_input_complete: true,
6908        thought_signature: None,
6909    };
6910    model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
6911        subagent_tool_use,
6912    ));
6913    model.end_last_completion_stream();
6914
6915    cx.run_until_parked();
6916
6917    // Verify subagent is running
6918    thread.read_with(cx, |thread, cx| {
6919        assert!(
6920            !thread.running_subagent_ids(cx).is_empty(),
6921            "subagent should be running"
6922        );
6923    });
6924
6925    // The subagent's model returns a non-retryable error
6926    model.send_last_completion_stream_error(LanguageModelCompletionError::PromptTooLarge {
6927        tokens: None,
6928    });
6929
6930    cx.run_until_parked();
6931
6932    // The subagent should no longer be running
6933    thread.read_with(cx, |thread, cx| {
6934        assert!(
6935            thread.running_subagent_ids(cx).is_empty(),
6936            "subagent should not be running after error"
6937        );
6938    });
6939
6940    // The parent model should get a new completion request to respond to the tool error
6941    model.send_last_completion_stream_text_chunk("Response after error");
6942    model.end_last_completion_stream();
6943
6944    send.await.unwrap();
6945
6946    // Verify the parent thread shows the error in the tool call
6947    let markdown = acp_thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
6948    assert!(
6949        markdown.contains("Status: Failed"),
6950        "tool call should have Failed status after model error, got:\n{markdown}"
6951    );
6952}
6953
6954#[gpui::test]
6955async fn test_edit_file_tool_deny_rule_blocks_edit(cx: &mut TestAppContext) {
6956    init_test(cx);
6957
6958    let fs = FakeFs::new(cx.executor());
6959    fs.insert_tree("/root", json!({"sensitive_config.txt": "secret data"}))
6960        .await;
6961    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6962
6963    cx.update(|cx| {
6964        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
6965        settings.tool_permissions.tools.insert(
6966            EditFileTool::NAME.into(),
6967            agent_settings::ToolRules {
6968                default: Some(settings::ToolPermissionMode::Allow),
6969                always_allow: vec![],
6970                always_deny: vec![agent_settings::CompiledRegex::new(r"sensitive", false).unwrap()],
6971                always_confirm: vec![],
6972                invalid_patterns: vec![],
6973            },
6974        );
6975        agent_settings::AgentSettings::override_global(settings, cx);
6976    });
6977
6978    let context_server_registry =
6979        cx.new(|cx| crate::ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
6980    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
6981    let templates = crate::Templates::new();
6982    let thread = cx.new(|cx| {
6983        crate::Thread::new(
6984            project.clone(),
6985            cx.new(|_cx| prompt_store::ProjectContext::default()),
6986            context_server_registry,
6987            templates.clone(),
6988            None,
6989            cx,
6990        )
6991    });
6992    let action_log = cx.update(|cx| thread.read(cx).action_log.clone());
6993
6994    #[allow(clippy::arc_with_non_send_sync)]
6995    let tool = Arc::new(crate::EditFileTool::new(
6996        project.clone(),
6997        thread.downgrade(),
6998        action_log,
6999        language_registry,
7000    ));
7001    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7002
7003    let task = cx.update(|cx| {
7004        tool.run(
7005            ToolInput::resolved(crate::EditFileToolInput {
7006                path: "root/sensitive_config.txt".into(),
7007                edits: vec![],
7008            }),
7009            event_stream,
7010            cx,
7011        )
7012    });
7013
7014    let result = task.await;
7015    assert!(result.is_err(), "expected edit to be blocked");
7016    assert!(
7017        result.unwrap_err().to_string().contains("blocked"),
7018        "error should mention the edit was blocked"
7019    );
7020}
7021
7022#[gpui::test]
7023async fn test_delete_path_tool_deny_rule_blocks_deletion(cx: &mut TestAppContext) {
7024    init_test(cx);
7025
7026    let fs = FakeFs::new(cx.executor());
7027    fs.insert_tree("/root", json!({"important_data.txt": "critical info"}))
7028        .await;
7029    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7030
7031    cx.update(|cx| {
7032        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7033        settings.tool_permissions.tools.insert(
7034            DeletePathTool::NAME.into(),
7035            agent_settings::ToolRules {
7036                default: Some(settings::ToolPermissionMode::Allow),
7037                always_allow: vec![],
7038                always_deny: vec![agent_settings::CompiledRegex::new(r"important", false).unwrap()],
7039                always_confirm: vec![],
7040                invalid_patterns: vec![],
7041            },
7042        );
7043        agent_settings::AgentSettings::override_global(settings, cx);
7044    });
7045
7046    let action_log = cx.new(|_cx| action_log::ActionLog::new(project.clone()));
7047
7048    #[allow(clippy::arc_with_non_send_sync)]
7049    let tool = Arc::new(crate::DeletePathTool::new(project, action_log));
7050    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7051
7052    let task = cx.update(|cx| {
7053        tool.run(
7054            ToolInput::resolved(crate::DeletePathToolInput {
7055                path: "root/important_data.txt".to_string(),
7056            }),
7057            event_stream,
7058            cx,
7059        )
7060    });
7061
7062    let result = task.await;
7063    assert!(result.is_err(), "expected deletion to be blocked");
7064    assert!(
7065        result.unwrap_err().contains("blocked"),
7066        "error should mention the deletion was blocked"
7067    );
7068}
7069
7070#[gpui::test]
7071async fn test_move_path_tool_denies_if_destination_denied(cx: &mut TestAppContext) {
7072    init_test(cx);
7073
7074    let fs = FakeFs::new(cx.executor());
7075    fs.insert_tree(
7076        "/root",
7077        json!({
7078            "safe.txt": "content",
7079            "protected": {}
7080        }),
7081    )
7082    .await;
7083    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7084
7085    cx.update(|cx| {
7086        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7087        settings.tool_permissions.tools.insert(
7088            MovePathTool::NAME.into(),
7089            agent_settings::ToolRules {
7090                default: Some(settings::ToolPermissionMode::Allow),
7091                always_allow: vec![],
7092                always_deny: vec![agent_settings::CompiledRegex::new(r"protected", false).unwrap()],
7093                always_confirm: vec![],
7094                invalid_patterns: vec![],
7095            },
7096        );
7097        agent_settings::AgentSettings::override_global(settings, cx);
7098    });
7099
7100    #[allow(clippy::arc_with_non_send_sync)]
7101    let tool = Arc::new(crate::MovePathTool::new(project));
7102    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7103
7104    let task = cx.update(|cx| {
7105        tool.run(
7106            ToolInput::resolved(crate::MovePathToolInput {
7107                source_path: "root/safe.txt".to_string(),
7108                destination_path: "root/protected/safe.txt".to_string(),
7109            }),
7110            event_stream,
7111            cx,
7112        )
7113    });
7114
7115    let result = task.await;
7116    assert!(
7117        result.is_err(),
7118        "expected move to be blocked due to destination path"
7119    );
7120    assert!(
7121        result.unwrap_err().contains("blocked"),
7122        "error should mention the move was blocked"
7123    );
7124}
7125
7126#[gpui::test]
7127async fn test_move_path_tool_denies_if_source_denied(cx: &mut TestAppContext) {
7128    init_test(cx);
7129
7130    let fs = FakeFs::new(cx.executor());
7131    fs.insert_tree(
7132        "/root",
7133        json!({
7134            "secret.txt": "secret content",
7135            "public": {}
7136        }),
7137    )
7138    .await;
7139    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7140
7141    cx.update(|cx| {
7142        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7143        settings.tool_permissions.tools.insert(
7144            MovePathTool::NAME.into(),
7145            agent_settings::ToolRules {
7146                default: Some(settings::ToolPermissionMode::Allow),
7147                always_allow: vec![],
7148                always_deny: vec![agent_settings::CompiledRegex::new(r"secret", false).unwrap()],
7149                always_confirm: vec![],
7150                invalid_patterns: vec![],
7151            },
7152        );
7153        agent_settings::AgentSettings::override_global(settings, cx);
7154    });
7155
7156    #[allow(clippy::arc_with_non_send_sync)]
7157    let tool = Arc::new(crate::MovePathTool::new(project));
7158    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7159
7160    let task = cx.update(|cx| {
7161        tool.run(
7162            ToolInput::resolved(crate::MovePathToolInput {
7163                source_path: "root/secret.txt".to_string(),
7164                destination_path: "root/public/not_secret.txt".to_string(),
7165            }),
7166            event_stream,
7167            cx,
7168        )
7169    });
7170
7171    let result = task.await;
7172    assert!(
7173        result.is_err(),
7174        "expected move to be blocked due to source path"
7175    );
7176    assert!(
7177        result.unwrap_err().contains("blocked"),
7178        "error should mention the move was blocked"
7179    );
7180}
7181
7182#[gpui::test]
7183async fn test_copy_path_tool_deny_rule_blocks_copy(cx: &mut TestAppContext) {
7184    init_test(cx);
7185
7186    let fs = FakeFs::new(cx.executor());
7187    fs.insert_tree(
7188        "/root",
7189        json!({
7190            "confidential.txt": "confidential data",
7191            "dest": {}
7192        }),
7193    )
7194    .await;
7195    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7196
7197    cx.update(|cx| {
7198        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7199        settings.tool_permissions.tools.insert(
7200            CopyPathTool::NAME.into(),
7201            agent_settings::ToolRules {
7202                default: Some(settings::ToolPermissionMode::Allow),
7203                always_allow: vec![],
7204                always_deny: vec![
7205                    agent_settings::CompiledRegex::new(r"confidential", false).unwrap(),
7206                ],
7207                always_confirm: vec![],
7208                invalid_patterns: vec![],
7209            },
7210        );
7211        agent_settings::AgentSettings::override_global(settings, cx);
7212    });
7213
7214    #[allow(clippy::arc_with_non_send_sync)]
7215    let tool = Arc::new(crate::CopyPathTool::new(project));
7216    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7217
7218    let task = cx.update(|cx| {
7219        tool.run(
7220            ToolInput::resolved(crate::CopyPathToolInput {
7221                source_path: "root/confidential.txt".to_string(),
7222                destination_path: "root/dest/copy.txt".to_string(),
7223            }),
7224            event_stream,
7225            cx,
7226        )
7227    });
7228
7229    let result = task.await;
7230    assert!(result.is_err(), "expected copy to be blocked");
7231    assert!(
7232        result.unwrap_err().contains("blocked"),
7233        "error should mention the copy was blocked"
7234    );
7235}
7236
7237#[gpui::test]
7238async fn test_web_search_tool_deny_rule_blocks_search(cx: &mut TestAppContext) {
7239    init_test(cx);
7240
7241    cx.update(|cx| {
7242        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7243        settings.tool_permissions.tools.insert(
7244            WebSearchTool::NAME.into(),
7245            agent_settings::ToolRules {
7246                default: Some(settings::ToolPermissionMode::Allow),
7247                always_allow: vec![],
7248                always_deny: vec![
7249                    agent_settings::CompiledRegex::new(r"internal\.company", false).unwrap(),
7250                ],
7251                always_confirm: vec![],
7252                invalid_patterns: vec![],
7253            },
7254        );
7255        agent_settings::AgentSettings::override_global(settings, cx);
7256    });
7257
7258    #[allow(clippy::arc_with_non_send_sync)]
7259    let tool = Arc::new(crate::WebSearchTool);
7260    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7261
7262    let input: crate::WebSearchToolInput =
7263        serde_json::from_value(json!({"query": "internal.company.com secrets"})).unwrap();
7264
7265    let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7266
7267    let result = task.await;
7268    assert!(result.is_err(), "expected search to be blocked");
7269    match result.unwrap_err() {
7270        crate::WebSearchToolOutput::Error { error } => {
7271            assert!(
7272                error.contains("blocked"),
7273                "error should mention the search was blocked"
7274            );
7275        }
7276        other => panic!("expected Error variant, got: {other:?}"),
7277    }
7278}
7279
7280#[gpui::test]
7281async fn test_edit_file_tool_allow_rule_skips_confirmation(cx: &mut TestAppContext) {
7282    init_test(cx);
7283
7284    let fs = FakeFs::new(cx.executor());
7285    fs.insert_tree("/root", json!({"README.md": "# Hello"}))
7286        .await;
7287    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7288
7289    cx.update(|cx| {
7290        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7291        settings.tool_permissions.tools.insert(
7292            EditFileTool::NAME.into(),
7293            agent_settings::ToolRules {
7294                default: Some(settings::ToolPermissionMode::Confirm),
7295                always_allow: vec![agent_settings::CompiledRegex::new(r"\.md$", false).unwrap()],
7296                always_deny: vec![],
7297                always_confirm: vec![],
7298                invalid_patterns: vec![],
7299            },
7300        );
7301        agent_settings::AgentSettings::override_global(settings, cx);
7302    });
7303
7304    let context_server_registry =
7305        cx.new(|cx| crate::ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
7306    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
7307    let templates = crate::Templates::new();
7308    let thread = cx.new(|cx| {
7309        crate::Thread::new(
7310            project.clone(),
7311            cx.new(|_cx| prompt_store::ProjectContext::default()),
7312            context_server_registry,
7313            templates.clone(),
7314            None,
7315            cx,
7316        )
7317    });
7318    let action_log = thread.read_with(cx, |thread, _cx| thread.action_log().clone());
7319
7320    #[allow(clippy::arc_with_non_send_sync)]
7321    let tool = Arc::new(crate::EditFileTool::new(
7322        project,
7323        thread.downgrade(),
7324        action_log,
7325        language_registry,
7326    ));
7327    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7328
7329    let _task = cx.update(|cx| {
7330        tool.run(
7331            ToolInput::resolved(crate::EditFileToolInput {
7332                path: "root/README.md".into(),
7333                edits: vec![],
7334            }),
7335            event_stream,
7336            cx,
7337        )
7338    });
7339
7340    cx.run_until_parked();
7341
7342    let event = rx.try_recv();
7343    assert!(
7344        !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
7345        "expected no authorization request for allowed .md file"
7346    );
7347}
7348
7349#[gpui::test]
7350async fn test_edit_file_tool_allow_still_prompts_for_local_settings(cx: &mut TestAppContext) {
7351    init_test(cx);
7352
7353    let fs = FakeFs::new(cx.executor());
7354    fs.insert_tree(
7355        "/root",
7356        json!({
7357            ".zed": {
7358                "settings.json": "{}"
7359            },
7360            "README.md": "# Hello"
7361        }),
7362    )
7363    .await;
7364    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7365
7366    cx.update(|cx| {
7367        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7368        settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
7369        agent_settings::AgentSettings::override_global(settings, cx);
7370    });
7371
7372    let context_server_registry =
7373        cx.new(|cx| crate::ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
7374    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
7375    let templates = crate::Templates::new();
7376    let thread = cx.new(|cx| {
7377        crate::Thread::new(
7378            project.clone(),
7379            cx.new(|_cx| prompt_store::ProjectContext::default()),
7380            context_server_registry,
7381            templates.clone(),
7382            None,
7383            cx,
7384        )
7385    });
7386    let action_log = thread.read_with(cx, |thread, _cx| thread.action_log().clone());
7387
7388    #[allow(clippy::arc_with_non_send_sync)]
7389    let tool = Arc::new(crate::EditFileTool::new(
7390        project,
7391        thread.downgrade(),
7392        action_log,
7393        language_registry,
7394    ));
7395
7396    // Editing a file inside .zed/ should still prompt even with global default: allow,
7397    // because local settings paths are sensitive and require confirmation regardless.
7398    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7399    let _task = cx.update(|cx| {
7400        tool.run(
7401            ToolInput::resolved(crate::EditFileToolInput {
7402                path: "root/.zed/settings.json".into(),
7403                edits: vec![],
7404            }),
7405            event_stream,
7406            cx,
7407        )
7408    });
7409
7410    let _update = rx.expect_update_fields().await;
7411    let _auth = rx.expect_authorization().await;
7412}
7413
7414#[gpui::test]
7415async fn test_fetch_tool_deny_rule_blocks_url(cx: &mut TestAppContext) {
7416    init_test(cx);
7417
7418    cx.update(|cx| {
7419        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7420        settings.tool_permissions.tools.insert(
7421            FetchTool::NAME.into(),
7422            agent_settings::ToolRules {
7423                default: Some(settings::ToolPermissionMode::Allow),
7424                always_allow: vec![],
7425                always_deny: vec![
7426                    agent_settings::CompiledRegex::new(r"internal\.company\.com", false).unwrap(),
7427                ],
7428                always_confirm: vec![],
7429                invalid_patterns: vec![],
7430            },
7431        );
7432        agent_settings::AgentSettings::override_global(settings, cx);
7433    });
7434
7435    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7436
7437    #[allow(clippy::arc_with_non_send_sync)]
7438    let tool = Arc::new(crate::FetchTool::new(http_client));
7439    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7440
7441    let input: crate::FetchToolInput =
7442        serde_json::from_value(json!({"url": "https://internal.company.com/api"})).unwrap();
7443
7444    let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7445
7446    let result = task.await;
7447    assert!(result.is_err(), "expected fetch to be blocked");
7448    assert!(
7449        result.unwrap_err().contains("blocked"),
7450        "error should mention the fetch was blocked"
7451    );
7452}
7453
7454#[gpui::test]
7455async fn test_fetch_tool_allow_rule_skips_confirmation(cx: &mut TestAppContext) {
7456    init_test(cx);
7457
7458    cx.update(|cx| {
7459        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7460        settings.tool_permissions.tools.insert(
7461            FetchTool::NAME.into(),
7462            agent_settings::ToolRules {
7463                default: Some(settings::ToolPermissionMode::Confirm),
7464                always_allow: vec![agent_settings::CompiledRegex::new(r"docs\.rs", false).unwrap()],
7465                always_deny: vec![],
7466                always_confirm: vec![],
7467                invalid_patterns: vec![],
7468            },
7469        );
7470        // The fetch tool also gates on the shared per-host network grant, so
7471        // grant docs.rs to keep this URL fully silent.
7472        settings
7473            .sandbox_permissions
7474            .network_hosts
7475            .push("docs.rs".into());
7476        agent_settings::AgentSettings::override_global(settings, cx);
7477    });
7478
7479    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7480
7481    #[allow(clippy::arc_with_non_send_sync)]
7482    let tool = Arc::new(crate::FetchTool::new(http_client));
7483    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7484
7485    let input: crate::FetchToolInput =
7486        serde_json::from_value(json!({"url": "https://docs.rs/some-crate"})).unwrap();
7487
7488    let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7489
7490    cx.run_until_parked();
7491
7492    let event = rx.try_recv();
7493    assert!(
7494        !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
7495        "expected no authorization request for allowed and granted docs.rs URL"
7496    );
7497}
7498
7499/// A fetch to a host that hasn't been granted network access prompts for the
7500/// shared per-host sandbox grant, even when the tool itself is allowed.
7501#[gpui::test]
7502#[ignore]
7503async fn test_fetch_tool_prompts_for_ungranted_host(cx: &mut TestAppContext) {
7504    init_test(cx);
7505
7506    cx.update(|cx| {
7507        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7508        settings.tool_permissions.tools.insert(
7509            FetchTool::NAME.into(),
7510            agent_settings::ToolRules {
7511                default: Some(settings::ToolPermissionMode::Allow),
7512                always_allow: vec![],
7513                always_deny: vec![],
7514                always_confirm: vec![],
7515                invalid_patterns: vec![],
7516            },
7517        );
7518        agent_settings::AgentSettings::override_global(settings, cx);
7519    });
7520
7521    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7522
7523    #[allow(clippy::arc_with_non_send_sync)]
7524    let tool = Arc::new(crate::FetchTool::new(http_client));
7525    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7526
7527    let input: crate::FetchToolInput =
7528        serde_json::from_value(json!({"url": "https://example.com/page"})).unwrap();
7529
7530    let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7531
7532    cx.run_until_parked();
7533
7534    let authorization = rx.expect_authorization().await;
7535    let details =
7536        acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
7537            .expect("an ungranted host should request a sandbox network grant");
7538    assert_eq!(details.network_hosts, vec!["example.com".to_string()]);
7539    assert!(!details.network_all_hosts);
7540}
7541
7542/// A host already present in the shared sandbox grants lets a fetch proceed
7543/// without any prompt — the same grant the terminal tool records and consults.
7544#[gpui::test]
7545async fn test_fetch_tool_granted_host_skips_prompt(cx: &mut TestAppContext) {
7546    init_test(cx);
7547
7548    cx.update(|cx| {
7549        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7550        // Allow the tool itself so only the shared per-host grant is under test.
7551        settings.tool_permissions.tools.insert(
7552            FetchTool::NAME.into(),
7553            agent_settings::ToolRules {
7554                default: Some(settings::ToolPermissionMode::Allow),
7555                always_allow: vec![],
7556                always_deny: vec![],
7557                always_confirm: vec![],
7558                invalid_patterns: vec![],
7559            },
7560        );
7561        settings
7562            .sandbox_permissions
7563            .network_hosts
7564            .push("example.com".into());
7565        agent_settings::AgentSettings::override_global(settings, cx);
7566    });
7567
7568    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7569
7570    #[allow(clippy::arc_with_non_send_sync)]
7571    let tool = Arc::new(crate::FetchTool::new(http_client));
7572    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7573
7574    let input: crate::FetchToolInput =
7575        serde_json::from_value(json!({"url": "https://example.com/page"})).unwrap();
7576
7577    let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7578
7579    cx.run_until_parked();
7580
7581    let event = rx.try_recv();
7582    assert!(
7583        !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
7584        "expected no authorization request for an already-granted host"
7585    );
7586}
7587
7588/// Loopback / IP-literal hosts can't be granted individually, so without
7589/// unsandboxed access a fetch to them is refused with guidance to grant it.
7590#[ignore]
7591#[gpui::test]
7592async fn test_fetch_tool_refuses_loopback_without_unsandboxed(cx: &mut TestAppContext) {
7593    init_test(cx);
7594
7595    cx.update(|cx| {
7596        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7597        // Allow the tool itself so the request reaches the per-host gate.
7598        settings.tool_permissions.tools.insert(
7599            FetchTool::NAME.into(),
7600            agent_settings::ToolRules {
7601                default: Some(settings::ToolPermissionMode::Allow),
7602                always_allow: vec![],
7603                always_deny: vec![],
7604                always_confirm: vec![],
7605                invalid_patterns: vec![],
7606            },
7607        );
7608        agent_settings::AgentSettings::override_global(settings, cx);
7609    });
7610
7611    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7612
7613    #[allow(clippy::arc_with_non_send_sync)]
7614    let tool = Arc::new(crate::FetchTool::new(http_client));
7615    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7616
7617    let input: crate::FetchToolInput =
7618        serde_json::from_value(json!({"url": "http://localhost:3000/api"})).unwrap();
7619
7620    let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7621    let result = task.await;
7622    assert!(result.is_err(), "expected a loopback fetch to be refused");
7623    assert!(
7624        result.unwrap_err().contains("unsandboxed"),
7625        "error should point at unsandboxed access as the way to reach loopback hosts"
7626    );
7627}
7628
7629/// Granting unsandboxed access lifts every fetch restriction, matching the
7630/// terminal: even loopback hosts become reachable and no per-host prompt is
7631/// requested.
7632#[gpui::test]
7633async fn test_fetch_tool_unsandboxed_lifts_restrictions(cx: &mut TestAppContext) {
7634    init_test(cx);
7635
7636    cx.update(|cx| {
7637        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7638        settings.sandbox_permissions.allow_unsandboxed = true;
7639        // Allow the tool itself so only the per-host gate is under test.
7640        settings.tool_permissions.tools.insert(
7641            FetchTool::NAME.into(),
7642            agent_settings::ToolRules {
7643                default: Some(settings::ToolPermissionMode::Allow),
7644                always_allow: vec![],
7645                always_deny: vec![],
7646                always_confirm: vec![],
7647                invalid_patterns: vec![],
7648            },
7649        );
7650        agent_settings::AgentSettings::override_global(settings, cx);
7651    });
7652
7653    let http_client = gpui::http_client::FakeHttpClient::with_200_response();
7654
7655    #[allow(clippy::arc_with_non_send_sync)]
7656    let tool = Arc::new(crate::FetchTool::new(http_client));
7657    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7658
7659    // A loopback host that could never be granted individually is reachable,
7660    // and no per-host authorization is requested.
7661    let input: crate::FetchToolInput =
7662        serde_json::from_value(json!({"url": "http://localhost:3000/api"})).unwrap();
7663
7664    let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7665
7666    cx.run_until_parked();
7667
7668    let event = rx.try_recv();
7669    assert!(
7670        !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
7671        "expected no authorization request when unsandboxed access is granted"
7672    );
7673}
7674
7675/// A granted host that redirects to a loopback target must not have that
7676/// redirect followed: loopback hosts can't be granted individually, so the hop
7677/// is refused just like a direct loopback fetch. This is the redirect variant of
7678/// the SSRF protection — the approved domain can't be used to bounce the request
7679/// onto the local machine.
7680#[ignore]
7681#[gpui::test]
7682async fn test_fetch_tool_refuses_redirect_to_loopback(cx: &mut TestAppContext) {
7683    init_test(cx);
7684
7685    cx.update(|cx| {
7686        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7687        settings.tool_permissions.tools.insert(
7688            FetchTool::NAME.into(),
7689            agent_settings::ToolRules {
7690                default: Some(settings::ToolPermissionMode::Allow),
7691                always_allow: vec![],
7692                always_deny: vec![],
7693                always_confirm: vec![],
7694                invalid_patterns: vec![],
7695            },
7696        );
7697        settings
7698            .sandbox_permissions
7699            .network_hosts
7700            .push("example.com".into());
7701        agent_settings::AgentSettings::override_global(settings, cx);
7702    });
7703
7704    let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
7705        let uri = req.uri().to_string();
7706        assert!(
7707            uri.contains("example.com"),
7708            "the loopback redirect target must never be requested, but saw {uri}"
7709        );
7710        Ok(gpui::http_client::Response::builder()
7711            .status(302)
7712            .header("location", "http://localhost:3000/internal")
7713            .body("".into())
7714            .unwrap())
7715    });
7716
7717    #[allow(clippy::arc_with_non_send_sync)]
7718    let tool = Arc::new(crate::FetchTool::new(http_client));
7719    let (event_stream, _rx) = crate::ToolCallEventStream::test();
7720
7721    let input: crate::FetchToolInput =
7722        serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
7723
7724    let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7725    let result = task.await;
7726    assert!(
7727        result.is_err(),
7728        "expected a redirect to a loopback host to be refused"
7729    );
7730    assert!(
7731        result.unwrap_err().contains("unsandboxed"),
7732        "error should point at unsandboxed access as the way to reach loopback hosts"
7733    );
7734}
7735
7736/// A granted host that redirects to a *different*, ungranted host triggers a
7737/// fresh per-host authorization prompt for the redirect target — the redirect is
7738/// not silently followed to a host the user never approved.
7739#[ignore]
7740#[gpui::test]
7741async fn test_fetch_tool_reauthorizes_redirect_to_new_host(cx: &mut TestAppContext) {
7742    init_test(cx);
7743
7744    cx.update(|cx| {
7745        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7746        settings.tool_permissions.tools.insert(
7747            FetchTool::NAME.into(),
7748            agent_settings::ToolRules {
7749                default: Some(settings::ToolPermissionMode::Allow),
7750                always_allow: vec![],
7751                always_deny: vec![],
7752                always_confirm: vec![],
7753                invalid_patterns: vec![],
7754            },
7755        );
7756        settings
7757            .sandbox_permissions
7758            .network_hosts
7759            .push("example.com".into());
7760        agent_settings::AgentSettings::override_global(settings, cx);
7761    });
7762
7763    let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
7764        let uri = req.uri().to_string();
7765        assert!(
7766            uri.contains("example.com"),
7767            "the ungranted redirect target must not be requested before authorization, \
7768             but saw {uri}"
7769        );
7770        Ok(gpui::http_client::Response::builder()
7771            .status(302)
7772            .header("location", "https://redirect-target.example/landing")
7773            .body("".into())
7774            .unwrap())
7775    });
7776
7777    #[allow(clippy::arc_with_non_send_sync)]
7778    let tool = Arc::new(crate::FetchTool::new(http_client));
7779    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7780
7781    let input: crate::FetchToolInput =
7782        serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
7783
7784    let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7785
7786    cx.run_until_parked();
7787
7788    let authorization = rx.expect_authorization().await;
7789    let details =
7790        acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
7791            .expect("a redirect to an ungranted host should request a sandbox network grant");
7792    assert_eq!(
7793        details.network_hosts,
7794        vec!["redirect-target.example".to_string()]
7795    );
7796    assert!(!details.network_all_hosts);
7797}
7798
7799/// Redirects between paths on an already-granted host are followed without any
7800/// additional prompt, so ordinary redirects (http→https upgrades, trailing-slash
7801/// canonicalization, etc.) keep working after the per-hop authorization change.
7802#[gpui::test]
7803async fn test_fetch_tool_follows_same_host_redirect(cx: &mut TestAppContext) {
7804    init_test(cx);
7805
7806    cx.update(|cx| {
7807        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7808        settings.tool_permissions.tools.insert(
7809            FetchTool::NAME.into(),
7810            agent_settings::ToolRules {
7811                default: Some(settings::ToolPermissionMode::Allow),
7812                always_allow: vec![],
7813                always_deny: vec![],
7814                always_confirm: vec![],
7815                invalid_patterns: vec![],
7816            },
7817        );
7818        settings
7819            .sandbox_permissions
7820            .network_hosts
7821            .push("example.com".into());
7822        agent_settings::AgentSettings::override_global(settings, cx);
7823    });
7824
7825    let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
7826        let uri = req.uri().to_string();
7827        if uri.ends_with("/start") {
7828            Ok(gpui::http_client::Response::builder()
7829                .status(302)
7830                .header("location", "https://example.com/final")
7831                .body("".into())
7832                .unwrap())
7833        } else if uri.ends_with("/final") {
7834            Ok(gpui::http_client::Response::builder()
7835                .status(200)
7836                .header("content-type", "text/plain")
7837                .body("final content".into())
7838                .unwrap())
7839        } else {
7840            panic!("unexpected request to {uri}");
7841        }
7842    });
7843
7844    #[allow(clippy::arc_with_non_send_sync)]
7845    let tool = Arc::new(crate::FetchTool::new(http_client));
7846    let (event_stream, mut rx) = crate::ToolCallEventStream::test();
7847
7848    let input: crate::FetchToolInput =
7849        serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
7850
7851    let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
7852    let result = task.await;
7853    assert_eq!(
7854        result.expect("same-host redirect should succeed"),
7855        "final content"
7856    );
7857
7858    let event = rx.try_recv();
7859    assert!(
7860        !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
7861        "expected no authorization prompt for a redirect to an already-granted host"
7862    );
7863}
7864
7865/// Approving one pending tool call with "Always for <tool>" auto-resolves
7866/// sibling pending authorizations for the same tool in the same turn.
7867#[gpui::test]
7868async fn test_always_allow_resolves_pending_authorizations(cx: &mut TestAppContext) {
7869    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
7870    let fake_model = model.as_fake();
7871
7872    let mut events = thread
7873        .update(cx, |thread, cx| {
7874            thread.add_tool(ToolRequiringPermission);
7875            thread.send(ClientUserMessageId::new(), ["abc"], cx)
7876        })
7877        .unwrap();
7878    cx.run_until_parked();
7879
7880    // Two parallel tool calls, both require permission.
7881    for id in ["tool_id_1", "tool_id_2"] {
7882        fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
7883            LanguageModelToolUse {
7884                id: id.into(),
7885                name: ToolRequiringPermission::NAME.into(),
7886                raw_input: "{}".into(),
7887                input: language_model::LanguageModelToolUseInput::Json(json!({})),
7888                is_input_complete: true,
7889                thought_signature: None,
7890            },
7891        ));
7892    }
7893    fake_model.end_last_completion_stream();
7894
7895    let tool_call_auth_1 = next_tool_call_authorization(&mut events).await;
7896    let tool_call_auth_2 = next_tool_call_authorization(&mut events).await;
7897
7898    // Approve the first with "always allow" — this persists a setting that
7899    // makes the tool unconditionally allowed. The second pending
7900    // authorization should resolve without user interaction.
7901    tool_call_auth_1
7902        .response
7903        .send(acp_thread::SelectedPermissionOutcome::new(
7904            acp::PermissionOptionId::new("always_allow:tool_requiring_permission"),
7905            acp::PermissionOptionKind::AllowAlways,
7906        ))
7907        .unwrap();
7908    cx.run_until_parked();
7909
7910    // The second tool's receiver was dropped by the auto-resolve path, so
7911    // sending a late response should fail.
7912    let late_send = tool_call_auth_2
7913        .response
7914        .send(acp_thread::SelectedPermissionOutcome::new(
7915            acp::PermissionOptionId::new("allow"),
7916            acp::PermissionOptionKind::AllowOnce,
7917        ));
7918    assert!(
7919        late_send.is_err(),
7920        "expected tool 2's response receiver to be dropped after auto-resolve"
7921    );
7922
7923    let completion = fake_model.pending_completions().pop().unwrap();
7924    let message = completion.messages.last().unwrap();
7925    let results: Vec<_> = message
7926        .content
7927        .iter()
7928        .filter_map(|c| match c {
7929            language_model::MessageContent::ToolResult(r) => Some(r),
7930            _ => None,
7931        })
7932        .collect();
7933    assert_eq!(
7934        results.len(),
7935        2,
7936        "both tool calls should have produced results"
7937    );
7938    assert!(
7939        results.iter().all(|r| !r.is_error),
7940        "both results should be successful after auto-resolve, got: {:?}",
7941        results
7942    );
7943}
7944
7945/// Externally editing settings (e.g. the user opening settings.json and
7946/// adding an `always_allow` rule) resolves pending authorization prompts
7947/// for tool calls that match the new rule.
7948#[gpui::test]
7949async fn test_external_settings_edit_resolves_pending_authorization(cx: &mut TestAppContext) {
7950    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
7951    let fake_model = model.as_fake();
7952
7953    let mut events = thread
7954        .update(cx, |thread, cx| {
7955            thread.add_tool(ToolRequiringPermission);
7956            thread.send(ClientUserMessageId::new(), ["abc"], cx)
7957        })
7958        .unwrap();
7959    cx.run_until_parked();
7960
7961    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
7962        LanguageModelToolUse {
7963            id: "tool_id_1".into(),
7964            name: ToolRequiringPermission::NAME.into(),
7965            raw_input: "{}".into(),
7966            input: language_model::LanguageModelToolUseInput::Json(json!({})),
7967            is_input_complete: true,
7968            thought_signature: None,
7969        },
7970    ));
7971    fake_model.end_last_completion_stream();
7972
7973    let tool_call_auth = next_tool_call_authorization(&mut events).await;
7974
7975    // Simulate the user editing settings.json to globally allow the tool.
7976    cx.update(|cx| {
7977        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
7978        settings.tool_permissions.tools.insert(
7979            ToolRequiringPermission::NAME.into(),
7980            agent_settings::ToolRules {
7981                default: Some(settings::ToolPermissionMode::Allow),
7982                always_allow: vec![],
7983                always_deny: vec![],
7984                always_confirm: vec![],
7985                invalid_patterns: vec![],
7986            },
7987        );
7988        agent_settings::AgentSettings::override_global(settings, cx);
7989    });
7990    cx.run_until_parked();
7991
7992    // The pending prompt auto-resolves without the user clicking anything.
7993    let late_send = tool_call_auth
7994        .response
7995        .send(acp_thread::SelectedPermissionOutcome::new(
7996            acp::PermissionOptionId::new("allow"),
7997            acp::PermissionOptionKind::AllowOnce,
7998        ));
7999    assert!(
8000        late_send.is_err(),
8001        "response receiver should have been dropped after settings-driven auto-resolve"
8002    );
8003
8004    let completion = fake_model.pending_completions().pop().unwrap();
8005    let message = completion.messages.last().unwrap();
8006    let result = message
8007        .content
8008        .iter()
8009        .find_map(|c| match c {
8010            language_model::MessageContent::ToolResult(r) => Some(r),
8011            _ => None,
8012        })
8013        .expect("expected a tool result");
8014    assert!(!result.is_error, "tool should have been auto-allowed");
8015}
8016
8017/// Externally adding a deny rule to settings dismisses a pending
8018/// authorization prompt and returns the tool call as denied.
8019#[gpui::test]
8020async fn test_external_deny_rule_resolves_pending_authorization(cx: &mut TestAppContext) {
8021    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8022    let fake_model = model.as_fake();
8023
8024    let mut events = thread
8025        .update(cx, |thread, cx| {
8026            thread.add_tool(ToolRequiringPermission);
8027            thread.send(ClientUserMessageId::new(), ["abc"], cx)
8028        })
8029        .unwrap();
8030    cx.run_until_parked();
8031
8032    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8033        LanguageModelToolUse {
8034            id: "tool_id_1".into(),
8035            name: ToolRequiringPermission::NAME.into(),
8036            raw_input: "{}".into(),
8037            input: language_model::LanguageModelToolUseInput::Json(json!({})),
8038            is_input_complete: true,
8039            thought_signature: None,
8040        },
8041    ));
8042    fake_model.end_last_completion_stream();
8043
8044    let tool_call_auth = next_tool_call_authorization(&mut events).await;
8045
8046    // Simulate the user adding a deny default for the tool.
8047    cx.update(|cx| {
8048        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
8049        settings.tool_permissions.tools.insert(
8050            ToolRequiringPermission::NAME.into(),
8051            agent_settings::ToolRules {
8052                default: Some(settings::ToolPermissionMode::Deny),
8053                always_allow: vec![],
8054                always_deny: vec![],
8055                always_confirm: vec![],
8056                invalid_patterns: vec![],
8057            },
8058        );
8059        agent_settings::AgentSettings::override_global(settings, cx);
8060    });
8061    cx.run_until_parked();
8062
8063    let late_send = tool_call_auth
8064        .response
8065        .send(acp_thread::SelectedPermissionOutcome::new(
8066            acp::PermissionOptionId::new("allow"),
8067            acp::PermissionOptionKind::AllowOnce,
8068        ));
8069    assert!(
8070        late_send.is_err(),
8071        "response receiver should have been dropped after deny auto-resolve"
8072    );
8073
8074    let completion = fake_model.pending_completions().pop().unwrap();
8075    let message = completion.messages.last().unwrap();
8076    let result = message
8077        .content
8078        .iter()
8079        .find_map(|c| match c {
8080            language_model::MessageContent::ToolResult(r) => Some(r),
8081            _ => None,
8082        })
8083        .expect("expected a tool result");
8084    assert!(
8085        result.is_error,
8086        "tool should have been auto-denied by the new rule"
8087    );
8088}
8089
8090/// Unrelated settings changes must not spuriously resolve pending
8091/// authorizations: if the re-check still returns `Confirm`, the prompt
8092/// stays visible and waits for the user.
8093#[gpui::test]
8094async fn test_unrelated_settings_change_does_not_resolve_pending_authorization(
8095    cx: &mut TestAppContext,
8096) {
8097    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8098    let fake_model = model.as_fake();
8099
8100    let mut events = thread
8101        .update(cx, |thread, cx| {
8102            thread.add_tool(ToolRequiringPermission);
8103            thread.send(ClientUserMessageId::new(), ["abc"], cx)
8104        })
8105        .unwrap();
8106    cx.run_until_parked();
8107
8108    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8109        LanguageModelToolUse {
8110            id: "tool_id_1".into(),
8111            name: ToolRequiringPermission::NAME.into(),
8112            raw_input: "{}".into(),
8113            input: language_model::LanguageModelToolUseInput::Json(json!({})),
8114            is_input_complete: true,
8115            thought_signature: None,
8116        },
8117    ));
8118    fake_model.end_last_completion_stream();
8119
8120    let tool_call_auth = next_tool_call_authorization(&mut events).await;
8121
8122    // Touch SettingsStore with a change that doesn't affect tool
8123    // permissions; the pending authorization should remain pending.
8124    cx.update(|cx| {
8125        let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
8126        settings.single_file_review = !settings.single_file_review;
8127        agent_settings::AgentSettings::override_global(settings, cx);
8128    });
8129    cx.run_until_parked();
8130
8131    // The user still has to act — resolve with an Allow Once.
8132    tool_call_auth
8133        .response
8134        .send(acp_thread::SelectedPermissionOutcome::new(
8135            acp::PermissionOptionId::new("allow"),
8136            acp::PermissionOptionKind::AllowOnce,
8137        ))
8138        .expect("response receiver should still be alive");
8139    cx.run_until_parked();
8140
8141    let completion = fake_model.pending_completions().pop().unwrap();
8142    let message = completion.messages.last().unwrap();
8143    let result = message
8144        .content
8145        .iter()
8146        .find_map(|c| match c {
8147            language_model::MessageContent::ToolResult(r) => Some(r),
8148            _ => None,
8149        })
8150        .expect("expected a tool result");
8151    assert!(!result.is_error);
8152}
8153
8154/// Approving one pending tool call with "Always for <tool A>" must not
8155/// dismiss a sibling pending authorization for a *different* tool: the
8156/// persisted rule is scoped to tool A, so tool B's prompt stays visible
8157/// and waits for the user.
8158#[gpui::test]
8159async fn test_always_allow_does_not_resolve_unrelated_tool_authorization(cx: &mut TestAppContext) {
8160    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8161    let fake_model = model.as_fake();
8162
8163    let mut events = thread
8164        .update(cx, |thread, cx| {
8165            thread.add_tool(ToolRequiringPermission);
8166            thread.add_tool(ToolRequiringPermission2);
8167            thread.send(ClientUserMessageId::new(), ["abc"], cx)
8168        })
8169        .unwrap();
8170    cx.run_until_parked();
8171
8172    // Two parallel tool calls, each for a distinct tool with its own
8173    // permission scope.
8174    for (id, name) in [
8175        ("tool_id_1", ToolRequiringPermission::NAME),
8176        ("tool_id_2", ToolRequiringPermission2::NAME),
8177    ] {
8178        fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8179            LanguageModelToolUse {
8180                id: id.into(),
8181                name: name.into(),
8182                raw_input: "{}".into(),
8183                input: language_model::LanguageModelToolUseInput::Json(json!({})),
8184                is_input_complete: true,
8185                thought_signature: None,
8186            },
8187        ));
8188    }
8189    fake_model.end_last_completion_stream();
8190
8191    let auth_a = next_tool_call_authorization(&mut events).await;
8192    let auth_b = next_tool_call_authorization(&mut events).await;
8193
8194    // Match prompts back to their originating tools via the authorization
8195    // context so the test doesn't depend on scheduling order.
8196    let (auth_for_tool_1, auth_for_tool_2) = {
8197        let a_name = auth_a
8198            .context
8199            .as_ref()
8200            .expect("settings-driven authorization must carry a context")
8201            .tool_name
8202            .clone();
8203        if a_name == ToolRequiringPermission::NAME {
8204            (auth_a, auth_b)
8205        } else {
8206            (auth_b, auth_a)
8207        }
8208    };
8209
8210    // Approve tool 1 with "always allow". Only tool 1's rule is persisted.
8211    auth_for_tool_1
8212        .response
8213        .send(acp_thread::SelectedPermissionOutcome::new(
8214            acp::PermissionOptionId::new("always_allow:tool_requiring_permission"),
8215            acp::PermissionOptionKind::AllowAlways,
8216        ))
8217        .unwrap();
8218    cx.run_until_parked();
8219
8220    // Tool 2's receiver must still be alive: its permission is unrelated
8221    // to the rule that was just added, so its prompt stays pending.
8222    auth_for_tool_2
8223        .response
8224        .send(acp_thread::SelectedPermissionOutcome::new(
8225            acp::PermissionOptionId::new("allow"),
8226            acp::PermissionOptionKind::AllowOnce,
8227        ))
8228        .expect("tool 2's response receiver should still be alive");
8229    cx.run_until_parked();
8230
8231    let completion = fake_model.pending_completions().pop().unwrap();
8232    let message = completion.messages.last().unwrap();
8233    let results: Vec<_> = message
8234        .content
8235        .iter()
8236        .filter_map(|c| match c {
8237            language_model::MessageContent::ToolResult(r) => Some(r),
8238            _ => None,
8239        })
8240        .collect();
8241    assert_eq!(
8242        results.len(),
8243        2,
8244        "both tool calls should have produced results"
8245    );
8246    assert!(
8247        results.iter().all(|r| !r.is_error),
8248        "both results should be successful, got: {:?}",
8249        results
8250    );
8251}
8252
8253#[gpui::test]
8254async fn test_queued_message_ends_turn_at_boundary(cx: &mut TestAppContext) {
8255    init_test(cx);
8256    always_allow_tools(cx);
8257
8258    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8259    let fake_model = model.as_fake();
8260
8261    // Add a tool so we can simulate tool calls
8262    thread.update(cx, |thread, _cx| {
8263        thread.add_tool(EchoTool);
8264    });
8265
8266    // Start a turn by sending a message
8267    let mut events = thread
8268        .update(cx, |thread, cx| {
8269            thread.send(ClientUserMessageId::new(), ["Use the echo tool"], cx)
8270        })
8271        .unwrap();
8272    cx.run_until_parked();
8273
8274    // Simulate the model making a tool call
8275    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8276        LanguageModelToolUse {
8277            id: "tool_1".into(),
8278            name: "echo".into(),
8279            raw_input: r#"{"text": "hello"}"#.into(),
8280            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
8281            is_input_complete: true,
8282            thought_signature: None,
8283        },
8284    ));
8285    fake_model
8286        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::ToolUse));
8287
8288    // Request that the turn end at the next boundary (a "steering" queued message)
8289    thread.update(cx, |thread, _cx| {
8290        thread.set_end_turn_at_next_boundary(true);
8291    });
8292
8293    // Now end the stream - tool will run, and the boundary check should see the queue
8294    fake_model.end_last_completion_stream();
8295
8296    // Collect all events until the turn stops
8297    let all_events = collect_events_until_stop(&mut events, cx).await;
8298
8299    // Verify we received the tool call event
8300    let tool_call_ids: Vec<_> = all_events
8301        .iter()
8302        .filter_map(|e| match e {
8303            Ok(ThreadEvent::ToolCall(tc)) => Some(tc.tool_call_id.to_string()),
8304            _ => None,
8305        })
8306        .collect();
8307    // User message is index 0, so the tool call is scoped to index 1 (see
8308    // `scoped_tool_call_id`).
8309    assert_eq!(
8310        tool_call_ids,
8311        vec![scoped_tool_call_id(1, &"tool_1".into()).to_string()],
8312        "Should have received a tool call event for our echo tool"
8313    );
8314
8315    // The turn should have stopped with EndTurn
8316    let stop_reasons = stop_events(all_events);
8317    assert_eq!(
8318        stop_reasons,
8319        vec![acp::StopReason::EndTurn],
8320        "Turn should have ended after tool completion due to queued message"
8321    );
8322
8323    // Verify the boundary flag is still set
8324    thread.update(cx, |thread, _cx| {
8325        assert!(
8326            thread.end_turn_at_next_boundary(),
8327            "Should still have the end-turn-at-boundary flag set"
8328        );
8329    });
8330
8331    // Thread should be idle now
8332    thread.update(cx, |thread, _cx| {
8333        assert!(
8334            thread.is_turn_complete(),
8335            "Thread should not be running after turn ends"
8336        );
8337    });
8338}
8339
8340#[gpui::test]
8341async fn test_queued_message_does_not_end_turn_without_boundary_flag(cx: &mut TestAppContext) {
8342    init_test(cx);
8343    always_allow_tools(cx);
8344
8345    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8346    let fake_model = model.as_fake();
8347
8348    thread.update(cx, |thread, _cx| {
8349        thread.add_tool(EchoTool);
8350    });
8351
8352    let mut events = thread
8353        .update(cx, |thread, cx| {
8354            thread.send(ClientUserMessageId::new(), ["Use the echo tool"], cx)
8355        })
8356        .unwrap();
8357    cx.run_until_parked();
8358
8359    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8360        LanguageModelToolUse {
8361            id: "tool_1".into(),
8362            name: "echo".into(),
8363            raw_input: r#"{"text": "hello"}"#.into(),
8364            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
8365            is_input_complete: true,
8366            thought_signature: None,
8367        },
8368    ));
8369    fake_model
8370        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::ToolUse));
8371
8372    // Default behavior: even though a message is conceptually queued, we do NOT
8373    // set the boundary flag, so the agent must keep going past the tool boundary
8374    // (running to completion) rather than ending the turn early.
8375    fake_model.end_last_completion_stream();
8376    cx.run_until_parked();
8377
8378    // The agent should have issued a fresh completion request with the tool
8379    // results instead of stopping — proof it continued past the boundary.
8380    let continuation = fake_model.pending_completions();
8381    assert_eq!(
8382        continuation.len(),
8383        1,
8384        "Without the boundary flag, the turn should continue with another completion request"
8385    );
8386
8387    // Let the continuation finish the turn naturally.
8388    fake_model.send_last_completion_stream_text_chunk("All done");
8389    fake_model
8390        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
8391    fake_model.end_last_completion_stream();
8392
8393    let all_events = collect_events_until_stop(&mut events, cx).await;
8394    let stop_reasons = stop_events(all_events);
8395    assert_eq!(
8396        stop_reasons,
8397        vec![acp::StopReason::EndTurn],
8398        "Turn should end only after the agent finishes, not at the tool boundary"
8399    );
8400}
8401
8402#[gpui::test]
8403async fn test_streaming_tool_error_breaks_stream_loop_immediately(cx: &mut TestAppContext) {
8404    init_test(cx);
8405    always_allow_tools(cx);
8406
8407    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8408    let fake_model = model.as_fake();
8409
8410    thread.update(cx, |thread, _cx| {
8411        thread.add_tool(StreamingFailingEchoTool {
8412            receive_chunks_until_failure: 1,
8413        });
8414    });
8415
8416    let _events = thread
8417        .update(cx, |thread, cx| {
8418            thread.send(
8419                ClientUserMessageId::new(),
8420                ["Use the streaming_failing_echo tool"],
8421                cx,
8422            )
8423        })
8424        .unwrap();
8425    cx.run_until_parked();
8426
8427    let tool_use = LanguageModelToolUse {
8428        id: "call_1".into(),
8429        name: StreamingFailingEchoTool::NAME.into(),
8430        raw_input: "hello".into(),
8431        input: language_model::LanguageModelToolUseInput::Json(json!({})),
8432        is_input_complete: false,
8433        thought_signature: None,
8434    };
8435
8436    fake_model
8437        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
8438
8439    cx.run_until_parked();
8440
8441    let completions = fake_model.pending_completions();
8442    let last_completion = completions.last().unwrap();
8443
8444    assert_eq!(
8445        last_completion.messages[1..],
8446        vec![
8447            LanguageModelRequestMessage {
8448                role: Role::User,
8449                content: vec!["Use the streaming_failing_echo tool".into()],
8450                cache: false,
8451                reasoning_details: None,
8452            },
8453            LanguageModelRequestMessage {
8454                role: Role::Assistant,
8455                content: vec![language_model::MessageContent::ToolUse(tool_use.clone())],
8456                cache: false,
8457                reasoning_details: None,
8458            },
8459            LanguageModelRequestMessage {
8460                role: Role::User,
8461                content: vec![language_model::MessageContent::ToolResult(
8462                    LanguageModelToolResult {
8463                        tool_use_id: tool_use.id.clone(),
8464                        tool_name: tool_use.name,
8465                        is_error: true,
8466                        content: vec!["failed".into()],
8467                        output: Some("failed".into()),
8468                    }
8469                )],
8470                cache: true,
8471                reasoning_details: None,
8472            },
8473        ]
8474    );
8475}
8476
8477#[gpui::test]
8478async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut TestAppContext) {
8479    init_test(cx);
8480    always_allow_tools(cx);
8481
8482    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
8483    let fake_model = model.as_fake();
8484
8485    let (complete_streaming_echo_tool_call_tx, complete_streaming_echo_tool_call_rx) =
8486        oneshot::channel();
8487
8488    thread.update(cx, |thread, _cx| {
8489        thread.add_tool(
8490            StreamingEchoTool::new().with_wait_until_complete(complete_streaming_echo_tool_call_rx),
8491        );
8492        thread.add_tool(StreamingFailingEchoTool {
8493            receive_chunks_until_failure: 1,
8494        });
8495    });
8496
8497    let _events = thread
8498        .update(cx, |thread, cx| {
8499            thread.send(
8500                ClientUserMessageId::new(),
8501                ["Use the streaming_echo tool and the streaming_failing_echo tool"],
8502                cx,
8503            )
8504        })
8505        .unwrap();
8506    cx.run_until_parked();
8507
8508    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8509        LanguageModelToolUse {
8510            id: "call_1".into(),
8511            name: StreamingEchoTool::NAME.into(),
8512            raw_input: "hello".into(),
8513            input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
8514            is_input_complete: false,
8515            thought_signature: None,
8516        },
8517    ));
8518    let first_tool_use = LanguageModelToolUse {
8519        id: "call_1".into(),
8520        name: StreamingEchoTool::NAME.into(),
8521        raw_input: "hello world".into(),
8522        input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello world" })),
8523        is_input_complete: true,
8524        thought_signature: None,
8525    };
8526    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8527        first_tool_use.clone(),
8528    ));
8529    let second_tool_use = LanguageModelToolUse {
8530        name: StreamingFailingEchoTool::NAME.into(),
8531        raw_input: "hello".into(),
8532        input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
8533        is_input_complete: false,
8534        thought_signature: None,
8535        id: "call_2".into(),
8536    };
8537    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8538        second_tool_use.clone(),
8539    ));
8540
8541    cx.run_until_parked();
8542
8543    complete_streaming_echo_tool_call_tx.send(()).unwrap();
8544
8545    cx.run_until_parked();
8546
8547    let completions = fake_model.pending_completions();
8548    let last_completion = completions.last().unwrap();
8549
8550    assert_eq!(
8551        last_completion.messages[1..],
8552        vec![
8553            LanguageModelRequestMessage {
8554                role: Role::User,
8555                content: vec![
8556                    "Use the streaming_echo tool and the streaming_failing_echo tool".into()
8557                ],
8558                cache: false,
8559                reasoning_details: None,
8560            },
8561            LanguageModelRequestMessage {
8562                role: Role::Assistant,
8563                content: vec![
8564                    language_model::MessageContent::ToolUse(first_tool_use.clone()),
8565                    language_model::MessageContent::ToolUse(second_tool_use.clone())
8566                ],
8567                cache: false,
8568                reasoning_details: None,
8569            },
8570            LanguageModelRequestMessage {
8571                role: Role::User,
8572                content: vec![
8573                    language_model::MessageContent::ToolResult(LanguageModelToolResult {
8574                        tool_use_id: second_tool_use.id.clone(),
8575                        tool_name: second_tool_use.name,
8576                        is_error: true,
8577                        content: vec!["failed".into()],
8578                        output: Some("failed".into()),
8579                    }),
8580                    language_model::MessageContent::ToolResult(LanguageModelToolResult {
8581                        tool_use_id: first_tool_use.id.clone(),
8582                        tool_name: first_tool_use.name,
8583                        is_error: false,
8584                        content: vec!["hello world".into()],
8585                        output: Some("hello world".into()),
8586                    }),
8587                ],
8588                cache: true,
8589                reasoning_details: None,
8590            },
8591        ]
8592    );
8593}
8594
8595#[gpui::test]
8596async fn test_mid_turn_model_and_settings_refresh(cx: &mut TestAppContext) {
8597    let ThreadTest {
8598        model, thread, fs, ..
8599    } = setup(cx, TestModel::Fake).await;
8600    let fake_model_a = model.as_fake();
8601
8602    thread.update(cx, |thread, _cx| {
8603        thread.add_tool(EchoTool);
8604        thread.add_tool(DelayTool);
8605    });
8606
8607    // Set up two profiles: profile-a has both tools, profile-b has only DelayTool.
8608    fs.insert_file(
8609        paths::settings_file(),
8610        json!({
8611            "agent": {
8612                "profiles": {
8613                    "profile-a": {
8614                        "name": "Profile A",
8615                        "tools": {
8616                            EchoTool::NAME: true,
8617                            DelayTool::NAME: true,
8618                        }
8619                    },
8620                    "profile-b": {
8621                        "name": "Profile B",
8622                        "tools": {
8623                            DelayTool::NAME: true,
8624                        }
8625                    }
8626                }
8627            }
8628        })
8629        .to_string()
8630        .into_bytes(),
8631    )
8632    .await;
8633    cx.run_until_parked();
8634
8635    thread.update(cx, |thread, cx| {
8636        thread.set_profile(AgentProfileId("profile-a".into()), cx);
8637        thread.set_thinking_enabled(false, cx);
8638    });
8639
8640    // Send a message — first iteration starts with model A, profile-a, thinking off.
8641    thread
8642        .update(cx, |thread, cx| {
8643            thread.send(ClientUserMessageId::new(), ["test mid-turn refresh"], cx)
8644        })
8645        .unwrap();
8646    cx.run_until_parked();
8647
8648    // Verify first request has both tools and thinking disabled.
8649    let completions = fake_model_a.pending_completions();
8650    assert_eq!(completions.len(), 1);
8651    let first_tools = tool_names_for_completion(&completions[0]);
8652    assert_eq!(first_tools, vec![DelayTool::NAME, EchoTool::NAME]);
8653    assert!(!completions[0].thinking_allowed);
8654
8655    // Model A responds with an echo tool call.
8656    fake_model_a.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
8657        LanguageModelToolUse {
8658            id: "tool_1".into(),
8659            name: "echo".into(),
8660            raw_input: r#"{"text":"hello"}"#.into(),
8661            input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
8662            is_input_complete: true,
8663            thought_signature: None,
8664        },
8665    ));
8666    fake_model_a.end_last_completion_stream();
8667
8668    // Before the next iteration runs, switch to profile-b (only DelayTool),
8669    // swap in a new model, and enable thinking.
8670    let fake_model_b = Arc::new(FakeLanguageModel::with_id_and_thinking(
8671        "test-provider",
8672        "model-b",
8673        "Model B",
8674        true,
8675    ));
8676    thread.update(cx, |thread, cx| {
8677        thread.set_profile(AgentProfileId("profile-b".into()), cx);
8678        thread.set_model(fake_model_b.clone() as Arc<dyn LanguageModel>, cx);
8679        thread.set_thinking_enabled(true, cx);
8680    });
8681
8682    // Run until parked — processes the echo tool call, loops back, picks up
8683    // the new model/profile/thinking, and makes a second request to model B.
8684    cx.run_until_parked();
8685
8686    // The second request should have gone to model B.
8687    let model_b_completions = fake_model_b.pending_completions();
8688    assert_eq!(
8689        model_b_completions.len(),
8690        1,
8691        "second request should go to model B"
8692    );
8693
8694    // Profile-b only has DelayTool, so echo should be gone.
8695    let second_tools = tool_names_for_completion(&model_b_completions[0]);
8696    assert_eq!(second_tools, vec![DelayTool::NAME]);
8697
8698    // Thinking should now be enabled.
8699    assert!(model_b_completions[0].thinking_allowed);
8700}
8701
Served at tenant.openagents/omega Member data and write actions are omitted.