1use std::collections::{BTreeMap, VecDeque};
11use std::time::Instant;
12
13use reqwest::Client;
14use rust_mcp_schema::CreateMessageRequest;
15use tokio_util::sync::CancellationToken;
16
17use crate::api::{ChatMessage, ChatToolCall};
18use crate::auth::AuthManager;
19use crate::character::card::CharacterCard;
20use crate::character::service::CharacterService;
21use crate::core::config::data::Config;
22#[cfg(test)]
23use crate::core::config::data::{DEFAULT_REFINE_INSTRUCTIONS, DEFAULT_REFINE_PREFIX};
24use crate::core::providers::{
25 resolve_env_session, resolve_session, ProviderResolutionError, ProviderSession,
26 ResolveSessionError,
27};
28use crate::ui::appearance::{detect_preferred_appearance, Appearance};
29use crate::ui::builtin_themes::{find_builtin_theme, theme_spec_from_custom};
30use crate::ui::theme::Theme;
31use crate::utils::color::quantize_theme_for_current_terminal;
32use crate::utils::logging::LoggingState;
33use crate::utils::url::construct_api_url;
34
35pub struct SessionContext {
36 pub client: Client,
37 pub model: String,
38 pub api_key: String,
39 pub base_url: String,
40 pub provider_name: String,
41 pub provider_display_name: String,
42 pub logging: LoggingState,
43 pub stream_cancel_token: Option<CancellationToken>,
44 pub current_stream_id: u64,
45 pub last_retry_time: Instant,
46 pub retrying_message_index: Option<usize>,
47 pub is_refining: bool,
48 pub original_refining_content: Option<String>,
49 pub last_refine_prompt: Option<String>,
50 pub refine_instructions: String,
51 pub refine_prefix: String,
52 pub startup_env_only: bool,
53 pub mcp_disabled: bool,
54 pub active_character: Option<CharacterCard>,
55 pub character_greeting_shown: bool,
56 pub has_received_assistant_message: bool,
57 pub tool_pipeline: ToolPipelineState,
58 pub mcp_init: McpInitState,
59 pub active_assistant_message_index: Option<usize>,
60 pub mcp_tools_enabled: bool,
61 pub mcp_tools_unsupported: bool,
62}
63
64#[derive(Default, Clone)]
65pub struct ToolPipelineState {
66 pub pending_tool_calls: BTreeMap<u32, PendingToolCall>,
67 pub pending_tool_queue: VecDeque<ToolCallRequest>,
68 pub active_tool_request: Option<ToolCallRequest>,
69 pub pending_sampling_queue: VecDeque<McpSamplingRequest>,
70 pub active_sampling_request: Option<McpSamplingRequest>,
71 pub tool_call_records: Vec<ChatToolCall>,
72 pub tool_results: Vec<ChatMessage>,
73 pub tool_result_history: Vec<ToolResultRecord>,
74 pub tool_payload_history: Vec<ToolPayloadHistoryEntry>,
75 pub continuation_messages: Option<StreamContinuation>,
76}
77
78#[derive(Clone)]
79pub struct StreamContinuation {
80 pub api_messages: Vec<ChatMessage>,
81 pub api_messages_base: Vec<ChatMessage>,
82}
83
84#[derive(Default)]
85pub struct McpInitState {
86 pub in_progress: bool,
87 pub complete: bool,
88 pub deferred_message: Option<String>,
89}
90
91#[derive(Debug, Clone)]
92pub struct PendingToolCall {
93 pub id: Option<String>,
94 pub name: Option<String>,
95 pub arguments: String,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum ToolResultStatus {
100 Success,
101 Error,
102 Denied,
103 Blocked,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ToolFailureKind {
108 ToolError,
109 ToolCallFailure,
110}
111
112impl ToolFailureKind {
113 pub fn label(self) -> &'static str {
114 match self {
115 ToolFailureKind::ToolError => "tool error",
116 ToolFailureKind::ToolCallFailure => "tool call failure",
117 }
118 }
119
120 pub fn display(self) -> &'static str {
121 match self {
122 ToolFailureKind::ToolError => "Tool error",
123 ToolFailureKind::ToolCallFailure => "Tool call failure",
124 }
125 }
126}
127
128impl ToolResultStatus {
129 pub fn label(self) -> &'static str {
130 match self {
131 ToolResultStatus::Success => "success",
132 ToolResultStatus::Error => "failed",
133 ToolResultStatus::Denied => "denied",
134 ToolResultStatus::Blocked => "blocked",
135 }
136 }
137
138 pub fn display(self) -> &'static str {
139 match self {
140 ToolResultStatus::Success => "Success",
141 ToolResultStatus::Error => "Failed",
142 ToolResultStatus::Denied => "Denied",
143 ToolResultStatus::Blocked => "Blocked",
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
149pub struct ToolResultRecord {
150 pub tool_name: String,
151 pub server_name: Option<String>,
152 pub server_id: Option<String>,
153 pub status: ToolResultStatus,
154 pub failure_kind: Option<ToolFailureKind>,
155 pub content: String,
156 pub summary: String,
157 pub tool_call_id: Option<String>,
158 pub raw_arguments: Option<String>,
159 pub assistant_message_index: Option<usize>,
160}
161
162#[derive(Clone)]
163pub struct ToolPayloadHistoryEntry {
164 pub server_id: Option<String>,
165 pub tool_call_id: Option<String>,
166 pub assistant_message: ChatMessage,
167 pub tool_message: ChatMessage,
168 pub assistant_message_index: Option<usize>,
169}
170
171#[derive(Debug, Clone)]
172pub struct ToolCallRequest {
173 pub server_id: String,
174 pub tool_name: String,
175 pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
176 pub raw_arguments: String,
177 pub tool_call_id: Option<String>,
178}
179
180#[derive(Clone)]
181pub struct McpSamplingRequest {
182 pub server_id: String,
183 pub request: CreateMessageRequest,
184 pub messages: Vec<ChatMessage>,
185}
186
187#[derive(Debug, Clone)]
188pub struct McpPromptRequest {
189 pub server_id: String,
190 pub prompt_name: String,
191 pub arguments: std::collections::HashMap<String, String>,
192}
193
194pub struct SessionBootstrap {
195 pub session: SessionContext,
196 pub theme: Theme,
197 pub startup_requires_provider: bool,
198 pub startup_errors: Vec<String>,
199}
200
201pub struct UninitializedSessionBootstrap {
202 pub session: SessionContext,
203 pub theme: Theme,
204 pub config: Config,
205 pub startup_requires_provider: bool,
206}
207
208pub(crate) struct PrepareWithAuthInput<'a> {
209 pub model: String,
210 pub log_file: Option<String>,
211 pub provider: Option<String>,
212 pub env_only: bool,
213 pub config: &'a Config,
214 pub pre_resolved_session: Option<ProviderSession>,
215 pub character: Option<String>,
216 pub character_service: &'a mut CharacterService,
217}
218
219impl SessionContext {
220 pub fn set_character(&mut self, card: CharacterCard) {
222 let is_same_character = self
224 .active_character
225 .as_ref()
226 .map(|current| current.data.name == card.data.name)
227 .unwrap_or(false);
228
229 self.active_character = Some(card);
230
231 if !is_same_character {
233 self.character_greeting_shown = false;
234 }
235 }
236
237 pub fn clear_character(&mut self) {
239 self.active_character = None;
240 self.character_greeting_shown = false;
241 }
242
243 pub fn get_character(&self) -> Option<&CharacterCard> {
245 self.active_character.as_ref()
246 }
247
248 pub fn should_show_greeting(&self) -> bool {
250 if let Some(character) = &self.active_character {
251 !self.character_greeting_shown && !character.data.first_mes.trim().is_empty()
252 } else {
253 false
254 }
255 }
256
257 pub fn mark_greeting_shown(&mut self) {
259 self.character_greeting_shown = true;
260 }
261
262 #[cfg(test)]
263 pub fn for_test(provider_name: &str, model: &str) -> Self {
264 Self {
265 client: Client::new(),
266 model: model.to_string(),
267 api_key: "test-api-key".to_string(),
268 base_url: "https://example.invalid".to_string(),
269 provider_name: provider_name.to_string(),
270 provider_display_name: provider_name.to_string(),
271 logging: LoggingState::new(None).expect("test logging"),
272 stream_cancel_token: None,
273 current_stream_id: 0,
274 last_retry_time: Instant::now(),
275 retrying_message_index: None,
276 is_refining: false,
277 original_refining_content: None,
278 last_refine_prompt: None,
279 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
280 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
281 startup_env_only: false,
282 mcp_disabled: false,
283 active_character: None,
284 character_greeting_shown: false,
285 has_received_assistant_message: false,
286 tool_pipeline: ToolPipelineState::default(),
287 mcp_init: McpInitState::default(),
288 active_assistant_message_index: None,
289 mcp_tools_enabled: false,
290 mcp_tools_unsupported: false,
291 }
292 }
293}
294
295impl ToolPipelineState {
296 pub fn reset(&mut self) {
297 self.pending_tool_calls.clear();
298 self.pending_tool_queue.clear();
299 self.active_tool_request = None;
300 self.pending_sampling_queue.clear();
301 self.active_sampling_request = None;
302 self.tool_call_records.clear();
303 self.tool_results.clear();
304 self.continuation_messages = None;
305 }
306
307 pub fn advance_tool_queue(&mut self) -> Option<&ToolCallRequest> {
308 let request = self.pending_tool_queue.pop_front()?;
309 self.active_tool_request = Some(request);
310 self.active_tool_request.as_ref()
311 }
312
313 pub fn advance_sampling_queue(&mut self) -> Option<&McpSamplingRequest> {
314 let request = self.pending_sampling_queue.pop_front()?;
315 self.active_sampling_request = Some(request);
316 self.active_sampling_request.as_ref()
317 }
318
319 pub fn record_result(
320 &mut self,
321 record: ToolResultRecord,
322 payload: Option<ToolPayloadHistoryEntry>,
323 ) {
324 self.tool_result_history.push(record);
325 if let Some(payload) = payload {
326 self.tool_payload_history.push(payload);
327 }
328 }
329
330 pub fn prune_for_assistant_index(&mut self, index: usize) {
331 self.prune_records(|candidate| candidate == index);
332 }
333
334 pub fn prune_from_index(&mut self, start: usize) {
335 self.prune_records(|candidate| candidate >= start);
336 }
337
338 pub fn clear_server_records(&mut self, server_id: &str) {
339 self.tool_result_history.retain(|record| {
340 record
341 .server_id
342 .as_deref()
343 .map(|id| !id.eq_ignore_ascii_case(server_id))
344 .unwrap_or(true)
345 });
346 self.tool_payload_history.retain(|entry| {
347 entry
348 .server_id
349 .as_deref()
350 .map(|id| !id.eq_ignore_ascii_case(server_id))
351 .unwrap_or(true)
352 });
353 }
354
355 pub fn set_continuation(&mut self, messages: Vec<ChatMessage>, base: Vec<ChatMessage>) {
356 self.continuation_messages = Some(StreamContinuation {
357 api_messages: messages,
358 api_messages_base: base,
359 });
360 }
361
362 pub fn take_continuation(&mut self) -> Option<StreamContinuation> {
363 self.continuation_messages.take()
364 }
365
366 fn prune_records<F>(&mut self, predicate: F)
367 where
368 F: Fn(usize) -> bool,
369 {
370 self.tool_result_history.retain(|record| {
371 record
372 .assistant_message_index
373 .map(|idx| !predicate(idx))
374 .unwrap_or(true)
375 });
376 self.tool_payload_history.retain(|entry| {
377 entry
378 .assistant_message_index
379 .map(|idx| !predicate(idx))
380 .unwrap_or(true)
381 });
382 }
383}
384
385impl McpInitState {
386 pub fn begin(&mut self) {
387 self.in_progress = true;
388 self.complete = false;
389 }
390
391 pub fn complete(&mut self) -> Option<String> {
392 self.in_progress = false;
393 self.complete = true;
394 self.deferred_message.take()
395 }
396
397 pub fn should_defer(&self) -> bool {
398 self.in_progress && !self.complete
399 }
400
401 pub fn reset(&mut self) {
402 self.in_progress = false;
403 self.complete = false;
404 self.deferred_message = None;
405 }
406}
407
408#[derive(Debug)]
410pub(crate) struct CharacterLoadOutcome {
411 pub character: Option<CharacterCard>,
412 pub errors: Vec<String>,
413}
414
415pub fn exit_with_provider_resolution_error(err: &ProviderResolutionError) -> ! {
416 eprintln!("{}", err);
417 let fixes = err.quick_fixes();
418 if !fixes.is_empty() {
419 eprintln!();
420 eprintln!("💡 Quick fixes:");
421 for fix in fixes {
422 eprintln!(" • {fix}");
423 }
424 }
425 std::process::exit(err.exit_code());
426}
427
428pub fn exit_if_env_only_missing_env(env_only: bool) {
429 if env_only && std::env::var("OPENAI_API_KEY").is_err() {
430 eprintln!("❌ --env used but OPENAI_API_KEY is not set");
431 std::process::exit(2);
432 }
433}
434
435pub(crate) fn load_character_for_session(
438 cli_character: Option<&str>,
439 provider: &str,
440 model: &str,
441 config: &Config,
442 character_service: &mut CharacterService,
443) -> Result<CharacterLoadOutcome, Box<dyn std::error::Error>> {
444 if let Some(character_name) = cli_character {
446 let card = character_service
447 .resolve(character_name)
448 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
449 return Ok(CharacterLoadOutcome {
450 character: Some(card),
451 errors: Vec::new(),
452 });
453 }
454
455 let mut errors = Vec::new();
457 match character_service.load_default_for_session(provider, model, config) {
458 Ok(Some((_name, card))) => {
459 return Ok(CharacterLoadOutcome {
460 character: Some(card),
461 errors,
462 })
463 }
464 Ok(None) => {}
465 Err(err) => {
466 if let Some(default_character) = config.get_default_character(provider, model) {
467 errors.push(format!(
468 "Failed to load default character '{}' for {}:{}: {}",
469 default_character, provider, model, err
470 ));
471 } else {
472 errors.push(format!(
473 "Failed to load default character for {}:{}: {}",
474 provider, model, err
475 ));
476 }
477 }
478 }
479
480 Ok(CharacterLoadOutcome {
482 character: None,
483 errors,
484 })
485}
486
487pub(crate) fn initialize_logging(
488 log_file: Option<String>,
489) -> Result<LoggingState, Box<dyn std::error::Error>> {
490 let mut logging = LoggingState::new(log_file.clone())?;
491 if let Some(log_path) = log_file {
492 if let Err(e) = logging.set_log_file(log_path.clone()) {
493 eprintln!(
494 "Warning: Failed to enable startup logging ({}): {}",
495 log_path, e
496 );
497 }
498 }
499 Ok(logging)
500}
501
502fn theme_from_appearance(appearance: Appearance) -> Theme {
503 match appearance {
504 Appearance::Light => Theme::light(),
505 Appearance::Dark => Theme::dark_default(),
506 }
507}
508
509pub(crate) fn resolve_theme(config: &Config) -> Theme {
510 let resolved_theme = match &config.theme {
511 Some(name) => {
512 if let Some(ct) = config.get_custom_theme(name) {
513 Theme::from_spec(&theme_spec_from_custom(ct))
514 } else if let Some(spec) = find_builtin_theme(name) {
515 Theme::from_spec(&spec)
516 } else {
517 Theme::from_name(name)
518 }
519 }
520 None => detect_preferred_appearance()
521 .map(theme_from_appearance)
522 .unwrap_or_else(Theme::dark_default),
523 };
524
525 quantize_theme_for_current_terminal(resolved_theme)
526}
527
528pub(crate) async fn prepare_with_auth(
529 input: PrepareWithAuthInput<'_>,
530) -> Result<SessionBootstrap, Box<dyn std::error::Error>> {
531 let PrepareWithAuthInput {
532 model,
533 log_file,
534 provider,
535 env_only,
536 config,
537 pre_resolved_session,
538 character,
539 character_service,
540 } = input;
541
542 let session = if let Some(session) = pre_resolved_session {
543 session
544 } else if env_only {
545 resolve_env_session().map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?
546 } else {
547 let auth_manager = AuthManager::new()?;
548 match resolve_session(&auth_manager, config, provider.as_deref()) {
549 Ok(session) => session,
550 Err(ResolveSessionError::Provider(err)) => return Err(Box::new(err)),
551 Err(ResolveSessionError::Source(err)) => return Err(err),
552 }
553 };
554
555 let (api_key, base_url, provider_name, provider_display_name) = session.into_tuple();
556
557 let final_model = if model != "default" {
558 model
559 } else if let Some(default_model) = config.get_default_model(&provider_name) {
560 default_model.clone()
561 } else {
562 String::new()
563 };
564
565 let _api_endpoint = construct_api_url(&base_url, "chat/completions");
566
567 let logging = initialize_logging(log_file)?;
568 let resolved_theme = resolve_theme(config);
569
570 let CharacterLoadOutcome {
572 character: active_character,
573 errors: startup_errors,
574 } = load_character_for_session(
575 character.as_deref(),
576 &provider_name,
577 &final_model,
578 config,
579 character_service,
580 )?;
581
582 let session = SessionContext {
583 client: Client::new(),
584 model: final_model,
585 api_key,
586 base_url,
587 provider_name: provider_name.to_string(),
588 provider_display_name,
589 logging,
590 stream_cancel_token: None,
591 current_stream_id: 0,
592 last_retry_time: Instant::now(),
593 retrying_message_index: None,
594 is_refining: false,
595 original_refining_content: None,
596 last_refine_prompt: None,
597 refine_instructions: config.refine_instructions().into_owned(),
598 refine_prefix: config.refine_prefix().into_owned(),
599 startup_env_only: false,
600 mcp_disabled: false,
601 active_character,
602 character_greeting_shown: false,
603 has_received_assistant_message: false,
604 tool_pipeline: ToolPipelineState::default(),
605 mcp_init: McpInitState::default(),
606 active_assistant_message_index: None,
607 mcp_tools_enabled: false,
608 mcp_tools_unsupported: false,
609 };
610
611 Ok(SessionBootstrap {
612 session,
613 theme: resolved_theme,
614 startup_requires_provider: false,
615 startup_errors,
616 })
617}
618
619pub(crate) async fn prepare_uninitialized(
620 log_file: Option<String>,
621 _character_service: &mut CharacterService,
622) -> Result<UninitializedSessionBootstrap, Box<dyn std::error::Error>> {
623 let config = Config::load()?;
624
625 let logging = initialize_logging(log_file)?;
626 let resolved_theme = resolve_theme(&config);
627
628 let session = SessionContext {
629 client: Client::new(),
630 model: String::new(),
631 api_key: String::new(),
632 base_url: String::new(),
633 provider_name: String::new(),
634 provider_display_name: "(no provider selected)".to_string(),
635 logging,
636 stream_cancel_token: None,
637 current_stream_id: 0,
638 last_retry_time: Instant::now(),
639 retrying_message_index: None,
640 is_refining: false,
641 original_refining_content: None,
642 last_refine_prompt: None,
643 refine_instructions: config.refine_instructions().into_owned(),
644 refine_prefix: config.refine_prefix().into_owned(),
645 startup_env_only: false,
646 mcp_disabled: false,
647 active_character: None,
648 character_greeting_shown: false,
649 has_received_assistant_message: false,
650 tool_pipeline: ToolPipelineState::default(),
651 mcp_init: McpInitState::default(),
652 active_assistant_message_index: None,
653 mcp_tools_enabled: false,
654 mcp_tools_unsupported: false,
655 };
656
657 Ok(UninitializedSessionBootstrap {
658 session,
659 theme: resolved_theme,
660 config,
661 startup_requires_provider: true,
662 })
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::core::config::data::Config;
669 use crate::core::providers::ProviderSession;
670 use crate::utils::test_utils::TestEnvVarGuard;
671 use tempfile::tempdir;
672
673 #[test]
674 fn theme_from_appearance_matches_light_theme() {
675 let theme = theme_from_appearance(Appearance::Light);
676 assert_eq!(theme.background_color, Theme::light().background_color);
677 }
678
679 #[test]
680 fn theme_from_appearance_matches_dark_theme() {
681 let theme = theme_from_appearance(Appearance::Dark);
682 assert_eq!(
683 theme.background_color,
684 Theme::dark_default().background_color
685 );
686 }
687
688 #[test]
689 fn resolve_theme_prefers_configured_theme() {
690 let config = Config {
691 theme: Some("light".to_string()),
692 ..Default::default()
693 };
694
695 let resolved_theme = resolve_theme(&config);
696 let expected_theme = quantize_theme_for_current_terminal(Theme::light());
697 assert_eq!(
698 resolved_theme.background_color,
699 expected_theme.background_color
700 );
701 }
702
703 #[test]
704 fn prepare_with_auth_uses_pre_resolved_session() {
705 let provider_session = ProviderSession {
706 api_key: "test-key".to_string(),
707 base_url: "https://example.invalid".to_string(),
708 provider_id: "test-provider".to_string(),
709 provider_display_name: "Test Provider".to_string(),
710 };
711
712 let config = Config::default();
713 let runtime = tokio::runtime::Runtime::new().expect("runtime");
714 let mut service = crate::character::CharacterService::new();
715
716 let bootstrap = runtime
717 .block_on(super::prepare_with_auth(super::PrepareWithAuthInput {
718 model: "default".to_string(),
719 log_file: None,
720 provider: None,
721 env_only: false,
722 config: &config,
723 pre_resolved_session: Some(provider_session.clone()),
724 character: None,
725 character_service: &mut service,
726 }))
727 .expect("prepare_with_auth");
728
729 assert_eq!(bootstrap.session.api_key, provider_session.api_key);
730 assert_eq!(bootstrap.session.base_url, provider_session.base_url);
731 assert_eq!(
732 bootstrap.session.provider_name,
733 provider_session.provider_id
734 );
735 assert_eq!(
736 bootstrap.session.provider_display_name,
737 provider_session.provider_display_name
738 );
739 assert!(!bootstrap.startup_requires_provider);
740 assert!(!bootstrap.session.startup_env_only);
741 assert!(bootstrap.session.active_character.is_none());
742 assert!(!bootstrap.session.character_greeting_shown);
743 }
744
745 #[test]
746 fn prepare_with_auth_uses_env_session_when_env_only() {
747 let mut env_guard = TestEnvVarGuard::new();
748 env_guard.set_var("OPENAI_API_KEY", "sk-env");
749 env_guard.set_var("OPENAI_BASE_URL", "https://example.com/v1");
750
751 let config = Config::default();
752 let runtime = tokio::runtime::Runtime::new().expect("runtime");
753 let mut service = crate::character::CharacterService::new();
754
755 let bootstrap = runtime
756 .block_on(super::prepare_with_auth(super::PrepareWithAuthInput {
757 model: "default".to_string(),
758 log_file: None,
759 provider: None,
760 env_only: true,
761 config: &config,
762 pre_resolved_session: None,
763 character: None,
764 character_service: &mut service,
765 }))
766 .expect("prepare_with_auth");
767
768 assert_eq!(bootstrap.session.api_key, "sk-env");
769 assert_eq!(bootstrap.session.base_url, "https://example.com/v1");
770 assert_eq!(bootstrap.session.provider_name, "openai-compatible");
771 assert_eq!(bootstrap.session.provider_display_name, "OpenAI-compatible");
772 }
773
774 #[test]
775 fn initialize_logging_with_file_writes_initial_entry() {
776 let temp_dir = tempdir().expect("tempdir");
777 let log_path = temp_dir.path().join("startup.log");
778 let log_file = log_path.to_string_lossy().to_string();
779
780 let logging = initialize_logging(Some(log_file.clone())).expect("logging initialized");
781 logging
782 .log_message("Hello from startup")
783 .expect("log message");
784
785 let contents = std::fs::read_to_string(&log_path).expect("read log file");
786 assert!(contents.contains("Hello from startup"));
788 }
789
790 #[test]
791 fn tool_pipeline_reset_clears_active_and_queues() {
792 let mut pipeline = ToolPipelineState::default();
793 pipeline.pending_tool_queue.push_back(ToolCallRequest {
794 server_id: "s".into(),
795 tool_name: "t".into(),
796 arguments: None,
797 raw_arguments: "{}".into(),
798 tool_call_id: Some("call".into()),
799 });
800 pipeline.active_tool_request = pipeline.pending_tool_queue.front().cloned();
801 pipeline.set_continuation(Vec::new(), Vec::new());
802
803 pipeline.reset();
804
805 assert!(pipeline.pending_tool_queue.is_empty());
806 assert!(pipeline.pending_sampling_queue.is_empty());
807 assert!(pipeline.active_tool_request.is_none());
808 assert!(pipeline.active_sampling_request.is_none());
809 assert!(pipeline.continuation_messages.is_none());
810 }
811
812 #[test]
813 fn tool_pipeline_advance_tool_queue_handles_empty_and_item() {
814 let mut pipeline = ToolPipelineState::default();
815 assert!(pipeline.advance_tool_queue().is_none());
816
817 pipeline.pending_tool_queue.push_back(ToolCallRequest {
818 server_id: "s".into(),
819 tool_name: "t".into(),
820 arguments: None,
821 raw_arguments: "{}".into(),
822 tool_call_id: None,
823 });
824
825 let request = pipeline.advance_tool_queue().expect("advanced");
826 assert_eq!(request.tool_name, "t");
827 assert!(pipeline.pending_tool_queue.is_empty());
828 }
829
830 #[test]
831 fn tool_pipeline_prune_for_assistant_index_removes_matching_records() {
832 let mut pipeline = ToolPipelineState::default();
833 pipeline.tool_result_history.push(ToolResultRecord {
834 tool_name: "keep".into(),
835 server_name: None,
836 server_id: Some("server".into()),
837 status: ToolResultStatus::Success,
838 failure_kind: None,
839 content: "ok".into(),
840 summary: "ok".into(),
841 tool_call_id: Some("keep".into()),
842 raw_arguments: None,
843 assistant_message_index: Some(1),
844 });
845 pipeline.tool_result_history.push(ToolResultRecord {
846 tool_name: "drop".into(),
847 server_name: None,
848 server_id: Some("server".into()),
849 status: ToolResultStatus::Error,
850 failure_kind: Some(ToolFailureKind::ToolError),
851 content: "fail".into(),
852 summary: "fail".into(),
853 tool_call_id: Some("drop".into()),
854 raw_arguments: None,
855 assistant_message_index: Some(3),
856 });
857
858 pipeline.prune_for_assistant_index(3);
859
860 assert_eq!(pipeline.tool_result_history.len(), 1);
861 assert_eq!(pipeline.tool_result_history[0].tool_name, "keep");
862 }
863
864 #[test]
865 fn tool_pipeline_continuation_round_trip_and_drain() {
866 let mut pipeline = ToolPipelineState::default();
867 pipeline.set_continuation(Vec::new(), Vec::new());
868
869 assert!(pipeline.take_continuation().is_some());
870 assert!(pipeline.take_continuation().is_none());
871 }
872
873 #[test]
874 fn mcp_init_state_complete_returns_message_and_clears_progress() {
875 let mut state = McpInitState::default();
876 state.begin();
877 state.deferred_message = Some("hello".into());
878
879 let deferred = state.complete();
880
881 assert_eq!(deferred.as_deref(), Some("hello"));
882 assert!(state.complete);
883 assert!(!state.in_progress);
884 assert!(state.deferred_message.is_none());
885 }
886
887 #[test]
888 fn mcp_init_state_should_defer_only_while_in_progress() {
889 let mut state = McpInitState::default();
890 assert!(!state.should_defer());
891
892 state.begin();
893 assert!(state.should_defer());
894
895 state.complete();
896 assert!(!state.should_defer());
897 }
898
899 #[test]
900 fn mcp_init_state_reset_restores_default() {
901 let mut state = McpInitState {
902 in_progress: true,
903 complete: true,
904 deferred_message: Some("queued".into()),
905 };
906
907 state.reset();
908
909 assert!(!state.in_progress);
910 assert!(!state.complete);
911 assert!(state.deferred_message.is_none());
912 }
913 #[test]
914 fn session_context_set_character() {
915 use crate::character::card::{CharacterCard, CharacterData};
916
917 let mut session = SessionContext {
918 client: Client::new(),
919 model: String::new(),
920 api_key: String::new(),
921 base_url: String::new(),
922 provider_name: String::new(),
923 provider_display_name: String::new(),
924 logging: LoggingState::new(None).unwrap(),
925 stream_cancel_token: None,
926 current_stream_id: 0,
927 last_retry_time: Instant::now(),
928 retrying_message_index: None,
929 is_refining: false,
930 original_refining_content: None,
931 last_refine_prompt: None,
932 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
933 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
934 startup_env_only: false,
935 mcp_disabled: false,
936 active_character: None,
937 character_greeting_shown: false,
938 has_received_assistant_message: false,
939 tool_pipeline: ToolPipelineState::default(),
940 mcp_init: McpInitState::default(),
941 active_assistant_message_index: None,
942 mcp_tools_enabled: false,
943 mcp_tools_unsupported: false,
944 };
945
946 let card = CharacterCard {
947 spec: "chara_card_v2".to_string(),
948 spec_version: "2.0".to_string(),
949 data: CharacterData {
950 name: "Test".to_string(),
951 description: "Test character".to_string(),
952 personality: "Friendly".to_string(),
953 scenario: "Testing".to_string(),
954 first_mes: "Hello!".to_string(),
955 mes_example: String::new(),
956 creator_notes: None,
957 system_prompt: None,
958 post_history_instructions: None,
959 alternate_greetings: None,
960 tags: None,
961 creator: None,
962 character_version: None,
963 },
964 };
965
966 session.set_character(card.clone());
967 assert!(session.active_character.is_some());
968 assert_eq!(session.get_character().unwrap().data.name, "Test");
969 assert!(!session.character_greeting_shown);
970 }
971
972 #[test]
973 fn session_context_clear_character() {
974 use crate::character::card::{CharacterCard, CharacterData};
975
976 let mut session = SessionContext {
977 client: Client::new(),
978 model: String::new(),
979 api_key: String::new(),
980 base_url: String::new(),
981 provider_name: String::new(),
982 provider_display_name: String::new(),
983 logging: LoggingState::new(None).unwrap(),
984 stream_cancel_token: None,
985 current_stream_id: 0,
986 last_retry_time: Instant::now(),
987 retrying_message_index: None,
988 is_refining: false,
989 original_refining_content: None,
990 last_refine_prompt: None,
991 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
992 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
993 startup_env_only: false,
994 mcp_disabled: false,
995 active_character: Some(CharacterCard {
996 spec: "chara_card_v2".to_string(),
997 spec_version: "2.0".to_string(),
998 data: CharacterData {
999 name: "Test".to_string(),
1000 description: "Test character".to_string(),
1001 personality: "Friendly".to_string(),
1002 scenario: "Testing".to_string(),
1003 first_mes: "Hello!".to_string(),
1004 mes_example: String::new(),
1005 creator_notes: None,
1006 system_prompt: None,
1007 post_history_instructions: None,
1008 alternate_greetings: None,
1009 tags: None,
1010 creator: None,
1011 character_version: None,
1012 },
1013 }),
1014 character_greeting_shown: true,
1015 has_received_assistant_message: false,
1016 tool_pipeline: ToolPipelineState::default(),
1017 mcp_init: McpInitState::default(),
1018 active_assistant_message_index: None,
1019 mcp_tools_enabled: false,
1020 mcp_tools_unsupported: false,
1021 };
1022
1023 session.clear_character();
1024 assert!(session.active_character.is_none());
1025 assert!(!session.character_greeting_shown);
1026 }
1027
1028 #[test]
1029 fn session_context_should_show_greeting() {
1030 use crate::character::card::{CharacterCard, CharacterData};
1031
1032 let mut session = SessionContext {
1033 client: Client::new(),
1034 model: String::new(),
1035 api_key: String::new(),
1036 base_url: String::new(),
1037 provider_name: String::new(),
1038 provider_display_name: String::new(),
1039 logging: LoggingState::new(None).unwrap(),
1040 stream_cancel_token: None,
1041 current_stream_id: 0,
1042 last_retry_time: Instant::now(),
1043 retrying_message_index: None,
1044 is_refining: false,
1045 original_refining_content: None,
1046 last_refine_prompt: None,
1047 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1048 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1049 startup_env_only: false,
1050 mcp_disabled: false,
1051 active_character: Some(CharacterCard {
1052 spec: "chara_card_v2".to_string(),
1053 spec_version: "2.0".to_string(),
1054 data: CharacterData {
1055 name: "Test".to_string(),
1056 description: "Test character".to_string(),
1057 personality: "Friendly".to_string(),
1058 scenario: "Testing".to_string(),
1059 first_mes: "Hello!".to_string(),
1060 mes_example: String::new(),
1061 creator_notes: None,
1062 system_prompt: None,
1063 post_history_instructions: None,
1064 alternate_greetings: None,
1065 tags: None,
1066 creator: None,
1067 character_version: None,
1068 },
1069 }),
1070 character_greeting_shown: false,
1071 has_received_assistant_message: false,
1072 tool_pipeline: ToolPipelineState::default(),
1073 mcp_init: McpInitState::default(),
1074 active_assistant_message_index: None,
1075 mcp_tools_enabled: false,
1076 mcp_tools_unsupported: false,
1077 };
1078
1079 assert!(session.should_show_greeting());
1081
1082 session.mark_greeting_shown();
1084 assert!(!session.should_show_greeting());
1085 }
1086
1087 #[test]
1088 fn session_context_should_not_show_empty_greeting() {
1089 use crate::character::card::{CharacterCard, CharacterData};
1090
1091 let session = SessionContext {
1092 client: Client::new(),
1093 model: String::new(),
1094 api_key: String::new(),
1095 base_url: String::new(),
1096 provider_name: String::new(),
1097 provider_display_name: String::new(),
1098 logging: LoggingState::new(None).unwrap(),
1099 stream_cancel_token: None,
1100 current_stream_id: 0,
1101 last_retry_time: Instant::now(),
1102 retrying_message_index: None,
1103 is_refining: false,
1104 original_refining_content: None,
1105 last_refine_prompt: None,
1106 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1107 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1108 startup_env_only: false,
1109 mcp_disabled: false,
1110 active_character: Some(CharacterCard {
1111 spec: "chara_card_v2".to_string(),
1112 spec_version: "2.0".to_string(),
1113 data: CharacterData {
1114 name: "Test".to_string(),
1115 description: "Test character".to_string(),
1116 personality: "Friendly".to_string(),
1117 scenario: "Testing".to_string(),
1118 first_mes: " ".to_string(), mes_example: String::new(),
1120 creator_notes: None,
1121 system_prompt: None,
1122 post_history_instructions: None,
1123 alternate_greetings: None,
1124 tags: None,
1125 creator: None,
1126 character_version: None,
1127 },
1128 }),
1129 character_greeting_shown: false,
1130 has_received_assistant_message: false,
1131 tool_pipeline: ToolPipelineState::default(),
1132 mcp_init: McpInitState::default(),
1133 active_assistant_message_index: None,
1134 mcp_tools_enabled: false,
1135 mcp_tools_unsupported: false,
1136 };
1137
1138 assert!(!session.should_show_greeting());
1140 }
1141
1142 #[test]
1143 fn load_character_for_session_no_character() {
1144 let config = Config::default();
1145 let mut service = crate::character::CharacterService::new();
1146 let outcome =
1147 super::load_character_for_session(None, "openai", "gpt-4", &config, &mut service)
1148 .expect("load_character_for_session");
1149
1150 assert!(outcome.character.is_none());
1151 assert!(outcome.errors.is_empty());
1152 }
1153
1154 #[test]
1155 fn load_character_for_session_cli_takes_precedence() {
1156 use crate::character::card::{CharacterCard, CharacterData};
1157 use std::collections::HashMap;
1158 use std::fs;
1159
1160 let temp_dir = tempdir().expect("tempdir");
1161 let cards_dir = temp_dir.path().join("cards");
1162 fs::create_dir_all(&cards_dir).expect("create cards dir");
1163
1164 let card = CharacterCard {
1166 spec: "chara_card_v2".to_string(),
1167 spec_version: "2.0".to_string(),
1168 data: CharacterData {
1169 name: "TestChar".to_string(),
1170 description: "Test".to_string(),
1171 personality: "Friendly".to_string(),
1172 scenario: "Testing".to_string(),
1173 first_mes: "Hello!".to_string(),
1174 mes_example: String::new(),
1175 creator_notes: None,
1176 system_prompt: None,
1177 post_history_instructions: None,
1178 alternate_greetings: None,
1179 tags: None,
1180 creator: None,
1181 character_version: None,
1182 },
1183 };
1184
1185 let card_path = cards_dir.join("testchar.json");
1186 let card_json = serde_json::to_string(&card).expect("serialize card");
1187 fs::write(&card_path, card_json).expect("write card");
1188
1189 let mut default_chars = HashMap::new();
1191 let mut openai_models = HashMap::new();
1192 openai_models.insert("gpt-4".to_string(), "other-char".to_string());
1193 default_chars.insert("openai".to_string(), openai_models);
1194
1195 let config = Config {
1196 default_characters: default_chars,
1197 ..Default::default()
1198 };
1199
1200 let mut service = crate::character::CharacterService::new();
1205 let result = super::load_character_for_session(
1206 Some(card_path.to_str().unwrap()),
1207 "openai",
1208 "gpt-4",
1209 &config,
1210 &mut service,
1211 );
1212 let outcome = result.expect("cli load");
1213 assert!(outcome.errors.is_empty());
1214 assert_eq!(
1215 outcome.character.expect("character loaded").data.name,
1216 "TestChar"
1217 );
1218 }
1219
1220 #[test]
1221 fn load_character_for_session_filepath_fallback() {
1222 use crate::character::card::{CharacterCard, CharacterData};
1223 use std::fs;
1224
1225 let temp_dir = tempdir().expect("tempdir");
1226
1227 let card = CharacterCard {
1229 spec: "chara_card_v2".to_string(),
1230 spec_version: "2.0".to_string(),
1231 data: CharacterData {
1232 name: "FilePathChar".to_string(),
1233 description: "Loaded from file path".to_string(),
1234 personality: "Friendly".to_string(),
1235 scenario: "Testing".to_string(),
1236 first_mes: "Hello from file!".to_string(),
1237 mes_example: String::new(),
1238 creator_notes: None,
1239 system_prompt: None,
1240 post_history_instructions: None,
1241 alternate_greetings: None,
1242 tags: None,
1243 creator: None,
1244 character_version: None,
1245 },
1246 };
1247
1248 let card_path = temp_dir.path().join("external_card.json");
1249 let card_json = serde_json::to_string(&card).expect("serialize card");
1250 fs::write(&card_path, card_json).expect("write card");
1251
1252 let config = Config::default();
1253 let mut service = crate::character::CharacterService::new();
1254
1255 let result = super::load_character_for_session(
1257 Some(card_path.to_str().unwrap()),
1258 "openai",
1259 "gpt-4",
1260 &config,
1261 &mut service,
1262 );
1263 assert!(result.is_ok());
1264 let outcome = result.unwrap();
1265 assert!(outcome.character.is_some());
1266 assert_eq!(outcome.character.unwrap().data.name, "FilePathChar");
1267 assert!(outcome.errors.is_empty());
1268 }
1269
1270 #[test]
1271 fn load_character_for_session_cards_dir_priority() {
1272 use crate::character::card::{CharacterCard, CharacterData};
1273 use std::fs;
1274
1275 let temp_dir = tempdir().expect("tempdir");
1276
1277 let wrong_card = CharacterCard {
1279 spec: "chara_card_v2".to_string(),
1280 spec_version: "2.0".to_string(),
1281 data: CharacterData {
1282 name: "WrongChar".to_string(),
1283 description: "Should not be loaded".to_string(),
1284 personality: "Wrong".to_string(),
1285 scenario: "Wrong".to_string(),
1286 first_mes: "Wrong!".to_string(),
1287 mes_example: String::new(),
1288 creator_notes: None,
1289 system_prompt: None,
1290 post_history_instructions: None,
1291 alternate_greetings: None,
1292 tags: None,
1293 creator: None,
1294 character_version: None,
1295 },
1296 };
1297
1298 let wrong_path = temp_dir.path().join("data.json");
1299 let wrong_json = serde_json::to_string(&wrong_card).expect("serialize card");
1300 fs::write(&wrong_path, wrong_json).expect("write card");
1301
1302 let config = Config::default();
1303
1304 let mut service = crate::character::CharacterService::new();
1307 let result = super::load_character_for_session(
1308 Some("data"),
1309 "openai",
1310 "gpt-4",
1311 &config,
1312 &mut service,
1313 );
1314
1315 assert!(result.is_err());
1318 }
1319
1320 #[test]
1321 fn session_context_get_character_returns_none_initially() {
1322 let session = SessionContext {
1323 client: Client::new(),
1324 model: String::new(),
1325 api_key: String::new(),
1326 base_url: String::new(),
1327 provider_name: String::new(),
1328 provider_display_name: String::new(),
1329 logging: LoggingState::new(None).unwrap(),
1330 stream_cancel_token: None,
1331 current_stream_id: 0,
1332 last_retry_time: Instant::now(),
1333 retrying_message_index: None,
1334 is_refining: false,
1335 original_refining_content: None,
1336 last_refine_prompt: None,
1337 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1338 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1339 startup_env_only: false,
1340 mcp_disabled: false,
1341 active_character: None,
1342 character_greeting_shown: false,
1343 has_received_assistant_message: false,
1344 tool_pipeline: ToolPipelineState::default(),
1345 mcp_init: McpInitState::default(),
1346 active_assistant_message_index: None,
1347 mcp_tools_enabled: false,
1348 mcp_tools_unsupported: false,
1349 };
1350
1351 assert!(session.get_character().is_none());
1352 assert!(!session.should_show_greeting());
1353 }
1354
1355 #[test]
1356 fn session_context_greeting_lifecycle() {
1357 use crate::character::card::{CharacterCard, CharacterData};
1358
1359 let mut session = SessionContext {
1360 client: Client::new(),
1361 model: String::new(),
1362 api_key: String::new(),
1363 base_url: String::new(),
1364 provider_name: String::new(),
1365 provider_display_name: String::new(),
1366 logging: LoggingState::new(None).unwrap(),
1367 stream_cancel_token: None,
1368 current_stream_id: 0,
1369 last_retry_time: Instant::now(),
1370 retrying_message_index: None,
1371 is_refining: false,
1372 original_refining_content: None,
1373 last_refine_prompt: None,
1374 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1375 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1376 startup_env_only: false,
1377 mcp_disabled: false,
1378 active_character: None,
1379 character_greeting_shown: false,
1380 has_received_assistant_message: false,
1381 tool_pipeline: ToolPipelineState::default(),
1382 mcp_init: McpInitState::default(),
1383 active_assistant_message_index: None,
1384 mcp_tools_enabled: false,
1385 mcp_tools_unsupported: false,
1386 };
1387
1388 assert!(!session.should_show_greeting());
1390
1391 let card = CharacterCard {
1393 spec: "chara_card_v2".to_string(),
1394 spec_version: "2.0".to_string(),
1395 data: CharacterData {
1396 name: "Test".to_string(),
1397 description: "Test character".to_string(),
1398 personality: "Friendly".to_string(),
1399 scenario: "Testing".to_string(),
1400 first_mes: "Hello there!".to_string(),
1401 mes_example: String::new(),
1402 creator_notes: None,
1403 system_prompt: None,
1404 post_history_instructions: None,
1405 alternate_greetings: None,
1406 tags: None,
1407 creator: None,
1408 character_version: None,
1409 },
1410 };
1411
1412 session.set_character(card);
1413
1414 assert!(session.should_show_greeting());
1416
1417 session.mark_greeting_shown();
1419
1420 assert!(!session.should_show_greeting());
1422
1423 session.clear_character();
1425
1426 assert!(!session.should_show_greeting());
1428 }
1429
1430 #[test]
1431 fn session_context_reselecting_same_character_preserves_greeting_flag() {
1432 use crate::character::card::{CharacterCard, CharacterData};
1433
1434 let mut session = SessionContext {
1435 client: Client::new(),
1436 model: String::new(),
1437 api_key: String::new(),
1438 base_url: String::new(),
1439 provider_name: String::new(),
1440 provider_display_name: String::new(),
1441 logging: LoggingState::new(None).unwrap(),
1442 stream_cancel_token: None,
1443 current_stream_id: 0,
1444 last_retry_time: Instant::now(),
1445 retrying_message_index: None,
1446 is_refining: false,
1447 original_refining_content: None,
1448 last_refine_prompt: None,
1449 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1450 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1451 startup_env_only: false,
1452 mcp_disabled: false,
1453 active_character: None,
1454 character_greeting_shown: false,
1455 has_received_assistant_message: false,
1456 tool_pipeline: ToolPipelineState::default(),
1457 mcp_init: McpInitState::default(),
1458 active_assistant_message_index: None,
1459 mcp_tools_enabled: false,
1460 mcp_tools_unsupported: false,
1461 };
1462
1463 let card = CharacterCard {
1464 spec: "chara_card_v2".to_string(),
1465 spec_version: "2.0".to_string(),
1466 data: CharacterData {
1467 name: "Test".to_string(),
1468 description: "Test character".to_string(),
1469 personality: "Friendly".to_string(),
1470 scenario: "Testing".to_string(),
1471 first_mes: "Hello there!".to_string(),
1472 mes_example: String::new(),
1473 creator_notes: None,
1474 system_prompt: None,
1475 post_history_instructions: None,
1476 alternate_greetings: None,
1477 tags: None,
1478 creator: None,
1479 character_version: None,
1480 },
1481 };
1482
1483 session.set_character(card.clone());
1485 assert!(session.should_show_greeting());
1486 session.mark_greeting_shown();
1487 assert!(!session.should_show_greeting());
1488
1489 session.set_character(card);
1491
1492 assert!(!session.should_show_greeting());
1494 assert!(session.character_greeting_shown);
1495 }
1496
1497 #[test]
1498 fn session_context_selecting_different_character_resets_greeting_flag() {
1499 use crate::character::card::{CharacterCard, CharacterData};
1500
1501 let mut session = SessionContext {
1502 client: Client::new(),
1503 model: String::new(),
1504 api_key: String::new(),
1505 base_url: String::new(),
1506 provider_name: String::new(),
1507 provider_display_name: String::new(),
1508 logging: LoggingState::new(None).unwrap(),
1509 stream_cancel_token: None,
1510 current_stream_id: 0,
1511 last_retry_time: Instant::now(),
1512 retrying_message_index: None,
1513 is_refining: false,
1514 original_refining_content: None,
1515 last_refine_prompt: None,
1516 refine_instructions: DEFAULT_REFINE_INSTRUCTIONS.to_string(),
1517 refine_prefix: DEFAULT_REFINE_PREFIX.to_string(),
1518 startup_env_only: false,
1519 mcp_disabled: false,
1520 active_character: None,
1521 character_greeting_shown: false,
1522 has_received_assistant_message: false,
1523 tool_pipeline: ToolPipelineState::default(),
1524 mcp_init: McpInitState::default(),
1525 active_assistant_message_index: None,
1526 mcp_tools_enabled: false,
1527 mcp_tools_unsupported: false,
1528 };
1529
1530 let card1 = CharacterCard {
1531 spec: "chara_card_v2".to_string(),
1532 spec_version: "2.0".to_string(),
1533 data: CharacterData {
1534 name: "Test1".to_string(),
1535 description: "Test character 1".to_string(),
1536 personality: "Friendly".to_string(),
1537 scenario: "Testing".to_string(),
1538 first_mes: "Hello from Test1!".to_string(),
1539 mes_example: String::new(),
1540 creator_notes: None,
1541 system_prompt: None,
1542 post_history_instructions: None,
1543 alternate_greetings: None,
1544 tags: None,
1545 creator: None,
1546 character_version: None,
1547 },
1548 };
1549
1550 let card2 = CharacterCard {
1551 spec: "chara_card_v2".to_string(),
1552 spec_version: "2.0".to_string(),
1553 data: CharacterData {
1554 name: "Test2".to_string(),
1555 description: "Test character 2".to_string(),
1556 personality: "Helpful".to_string(),
1557 scenario: "Testing".to_string(),
1558 first_mes: "Hello from Test2!".to_string(),
1559 mes_example: String::new(),
1560 creator_notes: None,
1561 system_prompt: None,
1562 post_history_instructions: None,
1563 alternate_greetings: None,
1564 tags: None,
1565 creator: None,
1566 character_version: None,
1567 },
1568 };
1569
1570 session.set_character(card1);
1572 session.mark_greeting_shown();
1573 assert!(!session.should_show_greeting());
1574
1575 session.set_character(card2);
1577
1578 assert!(session.should_show_greeting());
1580 assert!(!session.character_greeting_shown);
1581 }
1582}