I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.

1 //! Model cache setup, local model discovery, and selected-model persistence.
2 //!
3 //! On macOS the CLI shares a HuggingFace cache with Onde desktop apps via an
4 //! App Group container (`~/Library/Group Containers/group.com.ondeinference.apps/models/`).
5 //! On other platforms it falls back to `~/.cache/huggingface/`.
6 //!
7 //! Must run before anything touches `ChatEngine` or `hf-hub` because they
8 //! read the env vars once at init.
9
10 use std::path::{Path, PathBuf};
11
12 /// shared across siGit, Rumi, GT8, etc.
13 #[cfg(target_os = "macos")]
14 const APP_GROUP_IDENTIFIER: &str = "group.com.ondeinference.apps";
15
16 /// point `HF_HOME` / `HF_HUB_CACHE` at the shared container. no-ops if
17 /// the user already set them.
18 pub fn setup_shared_model_cache() {
19 if let Some(shared_dir) = resolve_shared_container() {
20 let models_home = shared_dir.join("models");
21 let model_hub = models_home.join("hub");
22
23 if let Err(error) = std::fs::create_dir_all(&model_hub) {
24 log::warn!(
25 "Failed to create shared model cache at {}: {error} — falling back to default",
26 model_hub.display()
27 );
28 return;
29 }
30
31 if std::env::var("HF_HOME").is_err() {
32 // SAFETY: called once at startup before any threads are spawned.
33 unsafe { std::env::set_var("HF_HOME", &models_home) };
34 log::info!("HF_HOME → shared App Group: {}", models_home.display());
35 } else {
36 log::debug!(
37 "HF_HOME already set by user: {}",
38 std::env::var("HF_HOME").unwrap_or_default()
39 );
40 }
41
42 // mistral.rs reads HF_HUB_CACHE directly instead of deriving from HF_HOME
43 if std::env::var("HF_HUB_CACHE").is_err() {
44 // SAFETY: called once at startup before any threads are spawned.
45 unsafe { std::env::set_var("HF_HUB_CACHE", &model_hub) };
46 log::info!("HF_HUB_CACHE → shared App Group: {}", model_hub.display());
47 }
48 } else {
49 log::debug!("Shared App Group container not available — using default HF cache");
50 }
51 }
52
53 const SELECTED_MODEL_FILE_NAME: &str = "selected-model.txt";
54
55 /// persisted identifier for a selected model (model_id + gguf filename).
56 #[derive(Debug, Clone, PartialEq, Eq)]
57 pub struct SelectedModel {
58 /// e.g. `bartowski/Qwen_Qwen3-4B-GGUF`
59 pub model_id: String,
60
61 pub gguf_file: String,
62 }
63
64 impl SelectedModel {
65 fn from_discovered(model: &DiscoveredModel) -> Self {
66 Self {
67 model_id: model.model_id.clone(),
68 gguf_file: model.gguf_file.clone(),
69 }
70 }
71
72 fn matches(&self, model: &DiscoveredModel) -> bool {
73 self.model_id == model.model_id && self.gguf_file == model.gguf_file
74 }
75 }
76
77 /// what we know about the model before the full UI is up.
78 #[derive(Debug, Clone, PartialEq, Eq)]
79 pub struct StartupModelSelection {
80 /// shown in the loading screen
81 pub display_name: String,
82
83 pub selected_model: Option<SelectedModel>,
84 }
85
86 /// a GGUF model found on disk.
87 #[derive(Debug, Clone, PartialEq, Eq)]
88 pub struct DiscoveredModel {
89 /// e.g. `bartowski/Qwen_Qwen3-4B-GGUF`
90 pub model_id: String,
91 /// filename inside the snapshot dir
92 pub gguf_file: String,
93
94 pub display_name: String,
95
96 pub snapshot_path: PathBuf,
97
98 pub gguf_path: PathBuf,
99
100 pub from_app_group: bool,
101
102 pub cache_health: ModelCacheHealth,
103 }
104
105 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
106 pub enum ModelCacheHealth {
107 Complete,
108 Incomplete,
109 NotDownloaded,
110 }
111
112 /// Where the model store lives, resolved once and handed to the readers.
113 ///
114 /// Resolving it from the environment is a global decision, so a caller that
115 /// needs a store of its own — a test pointing at a scratch directory — passes
116 /// the roots in rather than mutating `HF_HUB_CACHE`, `HF_HOME` and `HOME` out
117 /// from under whatever else is running in the same process.
118 #[derive(Debug, Clone, Default)]
119 struct ModelRoots {
120 /// Onde App Group model hub, when the signed app has run at least once.
121 app_group: Option<PathBuf>,
122 /// `HF_HUB_CACHE`, or `HF_HOME/hub`, when set and present.
123 configured_cache: Option<PathBuf>,
124 /// `~/.cache/huggingface/hub`, when present.
125 default_cache: Option<PathBuf>,
126 /// File holding the persisted selection.
127 selected_model_file: Option<PathBuf>,
128 }
129
130 impl ModelRoots {
131 fn from_env() -> Self {
132 Self {
133 app_group: app_group_models_root(),
134 configured_cache: hf_cache_root(),
135 default_cache: default_hf_cache_root(),
136 selected_model_file: selected_model_file_path(),
137 }
138 }
139
140 /// One cache dir and one selection file, with no environment lookups at all.
141 #[cfg(test)]
142 fn in_dir(cache_root: &Path, selected_model_file: &Path) -> Self {
143 Self {
144 configured_cache: Some(cache_root.to_path_buf()),
145 selected_model_file: Some(selected_model_file.to_path_buf()),
146 ..Self::default()
147 }
148 }
149 }
150
151 /// find all GGUF models on disk. checks Onde app group first, then HF cache.
152 pub fn discover_local_models() -> Vec<DiscoveredModel> {
153 discover_local_models_in(&ModelRoots::from_env())
154 }
155
156 fn discover_local_models_in(roots: &ModelRoots) -> Vec<DiscoveredModel> {
157 let mut models = Vec::new();
158 let mut seen_roots = Vec::new();
159
160 if let Some(app_group_models) = roots.app_group.as_ref() {
161 seen_roots.push(app_group_models.clone());
162 collect_models_from_cache_root(app_group_models, true, &mut models);
163 }
164
165 if let Some(hf_cache) = roots.configured_cache.as_ref()
166 && !seen_roots.iter().any(|root| root == hf_cache)
167 {
168 seen_roots.push(hf_cache.clone());
169 collect_models_from_cache_root(hf_cache, false, &mut models);
170 }
171
172 if let Some(default_hf_cache) = roots.default_cache.as_ref()
173 && !seen_roots.iter().any(|root| root == default_hf_cache)
174 {
175 collect_models_from_cache_root(default_hf_cache, false, &mut models);
176 }
177
178 models.sort_by(|left, right| {
179 right
180 .from_app_group
181 .cmp(&left.from_app_group)
182 .then_with(|| {
183 left.display_name
184 .to_lowercase()
185 .cmp(&right.display_name.to_lowercase())
186 })
187 .then_with(|| left.model_id.cmp(&right.model_id))
188 .then_with(|| left.gguf_file.cmp(&right.gguf_file))
189 });
190
191 models.dedup_by(|left, right| left.gguf_path == right.gguf_path);
192 models
193 }
194
195 fn collect_models_from_cache_root(
196 cache_root: &Path,
197 from_app_group: bool,
198 models: &mut Vec<DiscoveredModel>,
199 ) {
200 let entries = match std::fs::read_dir(cache_root) {
201 Ok(entries) => entries,
202 Err(error) => {
203 log::debug!(
204 "Skipping unreadable model cache root {}: {error}",
205 cache_root.display()
206 );
207 return;
208 }
209 };
210
211 for entry in entries.flatten() {
212 let repo_dir = entry.path();
213 if !repo_dir.is_dir() {
214 continue;
215 }
216
217 let dir_name = match entry.file_name().to_str() {
218 Some(name) => name.to_string(),
219 None => continue,
220 };
221
222 if !dir_name.starts_with("models--") {
223 continue;
224 }
225
226 let model_id = dir_name["models--".len()..].replace("--", "/");
227 let snapshots_dir = repo_dir.join("snapshots");
228 let snapshots = match std::fs::read_dir(&snapshots_dir) {
229 Ok(entries) => entries,
230 Err(_) => continue,
231 };
232
233 for snapshot in snapshots.flatten() {
234 let snapshot_path = snapshot.path();
235 if !snapshot_path.is_dir() {
236 continue;
237 }
238
239 let files = match std::fs::read_dir(&snapshot_path) {
240 Ok(entries) => entries,
241 Err(_) => continue,
242 };
243
244 let mut gguf_files = Vec::new();
245
246 for file in files.flatten() {
247 let file_path = file.path();
248 if !file_path.is_file() {
249 continue;
250 }
251
252 let file_name = match file.file_name().to_str() {
253 Some(name) => name.to_string(),
254 None => continue,
255 };
256
257 let extension = file_path
258 .extension()
259 .and_then(|ext| ext.to_str())
260 .unwrap_or_default();
261
262 if extension.eq_ignore_ascii_case("gguf") {
263 gguf_files.push((file_name, file_path));
264 }
265 }
266
267 if gguf_files.is_empty() {
268 // snapshot dir exists but no .gguf yet (download in progress or
269 // only metadata). mark incomplete so the picker can show it disabled.
270 models.push(DiscoveredModel {
271 display_name: display_name_for_model(&model_id, ""),
272 model_id: model_id.clone(),
273 gguf_file: String::new(),
274 snapshot_path: snapshot_path.clone(),
275 // unused for loading; incomplete models are filtered out before config
276 gguf_path: snapshot_path.clone(),
277 from_app_group,
278 cache_health: ModelCacheHealth::Incomplete,
279 });
280 } else {
281 for (gguf_file, file_path) in gguf_files {
282 models.push(DiscoveredModel {
283 display_name: display_name_for_model(&model_id, &gguf_file),
284 model_id: model_id.clone(),
285 gguf_file,
286 snapshot_path: snapshot_path.clone(),
287 gguf_path: file_path,
288 from_app_group,
289 cache_health: ModelCacheHealth::Complete,
290 });
291 }
292 }
293 }
294 }
295 }
296
297 fn display_name_for_model(model_id: &str, gguf_file: &str) -> String {
298 let repo_name = model_id
299 .rsplit('/')
300 .next()
301 .unwrap_or(model_id)
302 .replace('_', " ");
303
304 let file_name = gguf_file.strip_suffix(".gguf").unwrap_or(gguf_file);
305
306 if file_name.contains(&repo_name.replace(' ', "_")) || file_name.contains(&repo_name) {
307 repo_name
308 } else {
309 format!("{repo_name} — {file_name}")
310 }
311 }
312
313 fn app_group_models_root() -> Option<PathBuf> {
314 resolve_shared_container().map(|dir| dir.join("models").join("hub"))
315 }
316
317 fn hf_cache_root() -> Option<PathBuf> {
318 if let Ok(cache) = std::env::var("HF_HUB_CACHE") {
319 let path = PathBuf::from(cache);
320 if path.is_dir() {
321 return Some(path);
322 }
323 }
324
325 if let Ok(home) = std::env::var("HF_HOME") {
326 let path = PathBuf::from(home).join("hub");
327 if path.is_dir() {
328 return Some(path);
329 }
330 }
331
332 None
333 }
334
335 fn default_hf_cache_root() -> Option<PathBuf> {
336 let home = std::env::var("HOME").ok()?;
337 let path = PathBuf::from(home)
338 .join(".cache")
339 .join("huggingface")
340 .join("hub");
341
342 path.is_dir().then_some(path)
343 }
344
345 pub fn load_selected_model() -> Option<SelectedModel> {
346 load_selected_model_from(&selected_model_file_path()?)
347 }
348
349 fn load_selected_model_from(path: &Path) -> Option<SelectedModel> {
350 let contents = std::fs::read_to_string(path).ok()?;
351 let trimmed = contents.trim();
352 if trimmed.is_empty() {
353 return None;
354 }
355
356 let mut parts = trimmed.splitn(2, '\n');
357 let model_id = parts.next()?.trim();
358 let gguf_file = parts.next()?.trim();
359
360 if model_id.is_empty() || gguf_file.is_empty() {
361 return None;
362 }
363
364 Some(SelectedModel {
365 model_id: model_id.to_string(),
366 gguf_file: gguf_file.to_string(),
367 })
368 }
369
370 #[allow(dead_code)]
371 pub fn load_selected_model_name() -> Option<String> {
372 let selected = load_selected_model()?;
373 discover_local_models()
374 .into_iter()
375 .find(|model| selected.matches(model))
376 .map(|model| model.display_name)
377 }
378
379 /// pick a model for startup: saved selection > first local model > none.
380 /// if we fall back to a local model, persist it so ACP and TUI agree next time.
381 pub fn startup_model_selection() -> Option<StartupModelSelection> {
382 startup_model_selection_in(&ModelRoots::from_env())
383 }
384
385 fn startup_model_selection_in(roots: &ModelRoots) -> Option<StartupModelSelection> {
386 let discovered = discover_local_models_in(roots);
387
388 if let Some(saved_model) = roots
389 .selected_model_file
390 .as_deref()
391 .and_then(load_selected_model_from)
392 && let Some(model) = discovered.iter().find(|model| {
393 saved_model.matches(model) && model.cache_health == ModelCacheHealth::Complete
394 })
395 {
396 return Some(StartupModelSelection {
397 display_name: model.display_name.clone(),
398 selected_model: Some(saved_model),
399 });
400 }
401
402 discovered
403 .into_iter()
404 .find(|model| model.cache_health == ModelCacheHealth::Complete)
405 .map(|model| {
406 let selected_model = SelectedModel::from_discovered(&model);
407 if let Some(path) = roots.selected_model_file.as_deref() {
408 let _ = save_selected_model_to(path, &selected_model);
409 }
410 StartupModelSelection {
411 display_name: model.display_name.clone(),
412 selected_model: Some(selected_model),
413 }
414 })
415 }
416
417 pub fn save_selected_model(selected_model: &SelectedModel) -> Result<(), String> {
418 let path = selected_model_file_path()
419 .ok_or_else(|| "Could not determine where to store the selected model.".to_string())?;
420
421 save_selected_model_to(&path, selected_model)
422 }
423
424 fn save_selected_model_to(path: &Path, selected_model: &SelectedModel) -> Result<(), String> {
425 if let Some(parent) = path.parent()
426 && !parent.exists()
427 {
428 std::fs::create_dir_all(parent)
429 .map_err(|error| format!("Could not create preferences directory: {error}"))?;
430 }
431
432 let contents = format!(
433 "{}\n{}\n",
434 selected_model.model_id, selected_model.gguf_file
435 );
436
437 std::fs::write(path, contents)
438 .map_err(|error| format!("Could not save selected model: {error}"))
439 }
440
441 fn selected_model_file_path() -> Option<PathBuf> {
442 if let Some(shared_dir) = resolve_shared_container() {
443 return Some(shared_dir.join(SELECTED_MODEL_FILE_NAME));
444 }
445
446 if let Ok(home) = std::env::var("HF_HOME") {
447 let path = PathBuf::from(home);
448 if path.is_dir() || path.parent().is_some() {
449 return Some(path.join(SELECTED_MODEL_FILE_NAME));
450 }
451 }
452
453 let home = std::env::var("HOME").ok()?;
454 Some(
455 PathBuf::from(home)
456 .join(".cache")
457 .join("sigit")
458 .join(SELECTED_MODEL_FILE_NAME),
459 )
460 }
461
462 /// macOS only creates this dir when a signed app in the group first runs,
463 /// so it won't exist until the user has launched siGit desktop or another Onde app.
464 #[cfg(target_os = "macos")]
465 fn resolve_shared_container() -> Option<PathBuf> {
466 let home = std::env::var("HOME").ok()?;
467 let container = PathBuf::from(home)
468 .join("Library")
469 .join("Group Containers")
470 .join(APP_GROUP_IDENTIFIER);
471
472 if container.is_dir() {
473 log::debug!("App Group container found: {}", container.display());
474 Some(container)
475 } else {
476 log::debug!(
477 "App Group container does not exist at {} — \
478 has siGit desktop been launched at least once?",
479 container.display()
480 );
481 None
482 }
483 }
484
485 #[cfg(not(target_os = "macos"))]
486 fn resolve_shared_container() -> Option<PathBuf> {
487 None
488 }
489
490 #[cfg(test)]
491 mod tests {
492 use super::*;
493
494 /// A cache dir and selection file owned by this test. The model store is
495 /// resolved from `HF_HUB_CACHE`/`HF_HOME`/`HOME`, which are process-global
496 /// and read by the picker on other threads, so tests hand their roots in
497 /// instead of aiming those variables at a scratch directory.
498 fn test_roots(cache_root: &Path, selection_dir: &Path) -> ModelRoots {
499 ModelRoots::in_dir(cache_root, &selection_dir.join(SELECTED_MODEL_FILE_NAME))
500 }
501
502 /// A scratch directory no other test in this run can land on. The clock
503 /// alone is not enough: its resolution is coarse on some platforms and
504 /// parallel test threads can read the same tick, so the pid and a
505 /// process-local counter are mixed in.
506 fn unique_temp_dir(name: &str) -> PathBuf {
507 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
508
509 let nanos = std::time::SystemTime::now()
510 .duration_since(std::time::UNIX_EPOCH)
511 .expect("system time before unix epoch")
512 .as_nanos();
513 let seq = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
514
515 std::env::temp_dir().join(format!(
516 "sigit-setup-tests-{name}-{}-{seq}-{nanos}",
517 std::process::id()
518 ))
519 }
520
521 fn create_snapshot(
522 cache_root: &Path,
523 model_id: &str,
524 snapshot_name: &str,
525 gguf_file: &str,
526 complete: bool,
527 ) -> PathBuf {
528 let repo_dir = cache_root.join(format!("models--{}", model_id.replace('/', "--")));
529 let snapshot_dir = repo_dir.join("snapshots").join(snapshot_name);
530 std::fs::create_dir_all(&snapshot_dir).expect("create snapshot dir");
531
532 // Health is determined solely by the presence of a .gguf file.
533 // A complete snapshot has one; an incomplete snapshot has none
534 // (e.g. a partial download where only metadata files arrived).
535 if complete {
536 std::fs::write(snapshot_dir.join(gguf_file), b"gguf placeholder").expect("write gguf");
537 } else {
538 // Simulate a snapshot directory that exists but has no GGUF yet.
539 std::fs::write(snapshot_dir.join("config.json"), b"{}")
540 .expect("write config placeholder");
541 }
542
543 snapshot_dir
544 }
545
546 #[test]
547 fn discover_local_models_marks_complete_and_incomplete_snapshots() {
548 let root = unique_temp_dir("discover-health");
549 let cache_root = root.join("hf-cache");
550 std::fs::create_dir_all(&cache_root).expect("create cache root");
551
552 create_snapshot(
553 &cache_root,
554 "bartowski/Qwen_Qwen3-4B-GGUF",
555 "complete",
556 "Qwen_Qwen3-4B-Q4_K_M.gguf",
557 true,
558 );
559 create_snapshot(
560 &cache_root,
561 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF",
562 "incomplete",
563 "Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf",
564 false,
565 );
566
567 let models = discover_local_models_in(&test_roots(&cache_root, &root));
568
569 assert_eq!(models.len(), 2);
570
571 let complete = models
572 .iter()
573 .find(|model| model.model_id == "bartowski/Qwen_Qwen3-4B-GGUF")
574 .expect("complete model discovered");
575 assert_eq!(complete.cache_health, ModelCacheHealth::Complete);
576 assert!(!complete.from_app_group);
577
578 let incomplete = models
579 .iter()
580 .find(|model| model.model_id == "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF")
581 .expect("incomplete model discovered");
582 assert_eq!(incomplete.cache_health, ModelCacheHealth::Incomplete);
583 assert!(!incomplete.from_app_group);
584
585 std::fs::remove_dir_all(root).expect("remove temp dir");
586 }
587
588 #[test]
589 fn startup_model_selection_skips_saved_incomplete_model_and_picks_complete_one() {
590 let root = unique_temp_dir("startup-selection");
591 let cache_root = root.join("hf-cache");
592 std::fs::create_dir_all(&cache_root).expect("create cache root");
593
594 create_snapshot(
595 &cache_root,
596 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF",
597 "broken",
598 "Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf",
599 false,
600 );
601 create_snapshot(
602 &cache_root,
603 "bartowski/Qwen_Qwen3-4B-GGUF",
604 "ready",
605 "Qwen_Qwen3-4B-Q4_K_M.gguf",
606 true,
607 );
608
609 let roots = test_roots(&cache_root, &root);
610 let selected_path = roots
611 .selected_model_file
612 .clone()
613 .expect("selected model path");
614 std::fs::write(
615 &selected_path,
616 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF\nQwen2.5-Coder-3B-Instruct-Q4_K_M.gguf\n",
617 )
618 .expect("write selected model");
619
620 let selection = startup_model_selection_in(&roots).expect("startup selection");
621
622 assert_eq!(
623 selection.display_name,
624 "Qwen Qwen3-4B-GGUF — Qwen_Qwen3-4B-Q4_K_M"
625 );
626 let selected = selection.selected_model.expect("selected model");
627 assert_eq!(selected.model_id, "bartowski/Qwen_Qwen3-4B-GGUF");
628 assert_eq!(selected.gguf_file, "Qwen_Qwen3-4B-Q4_K_M.gguf");
629
630 std::fs::remove_dir_all(root).expect("remove temp dir");
631 }
632
633 #[test]
634 fn discover_empty_cache_returns_no_models() {
635 let root = unique_temp_dir("discover-empty");
636 let cache_root = root.join("hf-cache");
637 std::fs::create_dir_all(&cache_root).expect("create cache root");
638
639 let models = discover_local_models_in(&test_roots(&cache_root, &root));
640 assert!(models.is_empty());
641
642 std::fs::remove_dir_all(root).expect("remove temp dir");
643 }
644
645 #[test]
646 fn complete_models_sort_before_incomplete() {
647 let root = unique_temp_dir("sort-order");
648 let cache_root = root.join("hf-cache");
649 std::fs::create_dir_all(&cache_root).expect("create cache root");
650
651 create_snapshot(
652 &cache_root,
653 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF",
654 "snap1",
655 "Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf",
656 false,
657 );
658 create_snapshot(
659 &cache_root,
660 "bartowski/Qwen_Qwen3-4B-GGUF",
661 "snap2",
662 "Qwen_Qwen3-4B-Q4_K_M.gguf",
663 true,
664 );
665
666 let models = discover_local_models_in(&test_roots(&cache_root, &root));
667 assert_eq!(models.len(), 2);
668 assert_eq!(models[0].cache_health, ModelCacheHealth::Complete);
669 assert_eq!(models[1].cache_health, ModelCacheHealth::Incomplete);
670
671 std::fs::remove_dir_all(root).expect("remove temp dir");
672 }
673
674 #[test]
675 fn load_selected_model_roundtrip() {
676 let root = unique_temp_dir("persistence-roundtrip");
677 let path = root.join(SELECTED_MODEL_FILE_NAME);
678
679 let original = SelectedModel {
680 model_id: "bartowski/Qwen_Qwen3-4B-GGUF".to_string(),
681 gguf_file: "Qwen_Qwen3-4B-Q4_K_M.gguf".to_string(),
682 };
683
684 // The parent dir does not exist yet: saving has to create it.
685 save_selected_model_to(&path, &original).expect("save");
686 let loaded = load_selected_model_from(&path).expect("load");
687
688 assert_eq!(loaded.model_id, original.model_id);
689 assert_eq!(loaded.gguf_file, original.gguf_file);
690
691 std::fs::remove_dir_all(root).expect("remove temp dir");
692 }
693
694 #[test]
695 fn load_selected_model_empty_file_returns_none() {
696 let root = unique_temp_dir("persistence-empty");
697 let path = root.join(SELECTED_MODEL_FILE_NAME);
698 std::fs::create_dir_all(&root).expect("create root");
699 std::fs::write(&path, "").expect("write empty");
700
701 assert!(load_selected_model_from(&path).is_none());
702
703 std::fs::remove_dir_all(root).expect("remove temp dir");
704 }
705
706 #[test]
707 fn display_name_deduplicates_repo_and_file_name() {
708 // When the file name contains the repo name, only the repo name is shown.
709 let name = display_name_for_model(
710 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF",
711 "Qwen2.5-Coder-3B-Instruct-GGUF-Q4_K_M.gguf",
712 );
713 assert_eq!(name, "Qwen2.5-Coder-3B-Instruct-GGUF");
714
715 // When the file name does NOT contain the repo name, both are shown.
716 let name = display_name_for_model(
717 "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF",
718 "Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf",
719 );
720 assert_eq!(
721 name,
722 "Qwen2.5-Coder-3B-Instruct-GGUF — Qwen2.5-Coder-3B-Instruct-Q4_K_M"
723 );
724 }
725
726 #[test]
727 fn display_name_includes_file_when_different() {
728 let name =
729 display_name_for_model("bartowski/Qwen_Qwen3-4B-GGUF", "Qwen_Qwen3-4B-Q4_K_M.gguf");
730 assert_eq!(name, "Qwen Qwen3-4B-GGUF — Qwen_Qwen3-4B-Q4_K_M");
731 }
732 }