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