Skip to main content

upstream_rs/application/operations/
hooks_operation.rs

1#[cfg(unix)]
2use crate::services::integration::{nushell_paths_file_contains_path, render_nushell_paths_file};
3use crate::services::{
4    integration::CompletionManager,
5    storage::{
6        config_storage::ConfigStorage, manifest_storage::ManifestStorage,
7        trust_storage::TrustStorage,
8    },
9};
10#[cfg(unix)]
11use crate::utils::platform::shells::installed_shell_commands;
12use crate::utils::static_paths::UpstreamPaths;
13use crate::{output, output::Status};
14#[cfg(windows)]
15use anyhow::Context;
16use anyhow::Result;
17#[cfg(unix)]
18use std::collections::BTreeSet;
19use std::fmt;
20use std::fs;
21use std::io;
22#[cfg(unix)]
23use std::io::Write;
24#[cfg(unix)]
25use std::path::Path;
26
27// Unix shell source lines
28#[cfg(unix)]
29const SOURCE_LINE_BASH: &str =
30    "[ -f $HOME/.upstream/metadata/paths.sh ] && source $HOME/.upstream/metadata/paths.sh";
31#[cfg(unix)]
32const SOURCE_LINE_FISH: &str =
33    "test -f $HOME/.upstream/metadata/paths.sh; and source $HOME/.upstream/metadata/paths.sh";
34#[cfg(unix)]
35const SOURCE_LINE_NUSHELL: &str = r#"const upstream_paths_nu = if ("~/.upstream/metadata/paths.nu" | path expand | path exists) { ("~/.upstream/metadata/paths.nu" | path expand) } else { null }; source-env $upstream_paths_nu"#;
36
37pub struct InitCheckReport {
38    pub ok: bool,
39    pub messages: Vec<String>,
40}
41
42fn check_ok(report: &mut InitCheckReport, detail: impl fmt::Display) {
43    report
44        .messages
45        .push(format!("{} {}", output::status_cell(Status::Ok), detail));
46}
47
48fn check_fail(report: &mut InitCheckReport, detail: impl fmt::Display) {
49    report.ok = false;
50    report
51        .messages
52        .push(format!("{} {}", output::status_cell(Status::Fail), detail));
53}
54
55#[cfg(windows)]
56fn normalize_windows_path(path: &str) -> String {
57    let mut normalized = path.replace('/', "\\").trim().to_ascii_lowercase();
58    while normalized.ends_with('\\') {
59        normalized.pop();
60    }
61    normalized
62}
63
64pub fn initialize(paths: &UpstreamPaths) -> Result<()> {
65    create_package_dirs(paths)?;
66    create_manifest_file(paths)?;
67    create_trust_file(paths)?;
68    create_metadata_files(paths)?;
69    create_default_config_file(paths)?;
70
71    #[cfg(windows)]
72    add_to_windows_path(paths)?;
73
74    #[cfg(unix)]
75    update_shell_profiles(paths)?;
76
77    Ok(())
78}
79
80fn create_manifest_file(paths: &UpstreamPaths) -> Result<()> {
81    ManifestStorage::new(&ManifestStorage::path_for_root(&paths.dirs.data_dir))?.ensure_current()
82}
83
84fn create_trust_file(paths: &UpstreamPaths) -> Result<()> {
85    TrustStorage::new(&paths.config.trust_file)?.ensure_exists()
86}
87
88pub fn purge_data(paths: &UpstreamPaths) -> Result<()> {
89    if paths.dirs.data_dir.exists() {
90        fs::remove_dir_all(&paths.dirs.data_dir)?;
91    }
92    Ok(())
93}
94
95pub fn check(paths: &UpstreamPaths) -> Result<InitCheckReport> {
96    let mut report = InitCheckReport {
97        ok: true,
98        messages: Vec::new(),
99    };
100
101    for (label, path) in [
102        ("config directory", &paths.dirs.config_dir),
103        ("data directory", &paths.dirs.data_dir),
104        ("metadata directory", &paths.dirs.metadata_dir),
105        ("symlinks directory", &paths.integration.symlinks_dir),
106        ("appimages directory", &paths.install.appimages_dir),
107        ("binaries directory", &paths.install.binaries_dir),
108        ("archives directory", &paths.install.archives_dir),
109    ] {
110        if path.exists() {
111            check_ok(&mut report, format!("{} exists: {}", label, path.display()));
112        } else {
113            check_fail(
114                &mut report,
115                format!("{} missing: {}", label, path.display()),
116            );
117        }
118    }
119
120    let completion_manager = CompletionManager::new(paths);
121    let completion_dirs = completion_manager.installed_shell_completion_dirs();
122    if completion_dirs.is_empty() {
123        check_ok(
124            &mut report,
125            "no supported shells detected for completion installation",
126        );
127    }
128    for (shell, path) in completion_dirs {
129        let label = format!("{shell} completions directory");
130        if path.exists() {
131            check_ok(&mut report, format!("{} exists: {}", label, path.display()));
132        } else {
133            check_fail(
134                &mut report,
135                format!("{} missing: {}", label, path.display()),
136            );
137        }
138    }
139
140    if paths.config.config_file.exists() {
141        check_ok(
142            &mut report,
143            format!("config file exists: {}", paths.config.config_file.display()),
144        );
145    } else {
146        check_fail(
147            &mut report,
148            format!(
149                "config file missing: {}",
150                paths.config.config_file.display()
151            ),
152        );
153    }
154
155    let manifest_file = ManifestStorage::path_for_root(&paths.dirs.data_dir);
156    if manifest_file.exists() {
157        check_ok(
158            &mut report,
159            format!("manifest file exists: {}", manifest_file.display()),
160        );
161    } else {
162        check_fail(
163            &mut report,
164            format!("manifest file missing: {}", manifest_file.display()),
165        );
166    }
167
168    if paths.config.trust_file.exists() {
169        check_ok(
170            &mut report,
171            format!(
172                "trust metadata file exists: {}",
173                paths.config.trust_file.display()
174            ),
175        );
176    } else {
177        check_fail(
178            &mut report,
179            format!(
180                "trust metadata file missing: {}",
181                paths.config.trust_file.display()
182            ),
183        );
184    }
185
186    #[cfg(unix)]
187    check_unix_integration(paths, &mut report)?;
188
189    #[cfg(windows)]
190    check_windows_integration(paths, &mut report)?;
191
192    Ok(report)
193}
194
195#[cfg(windows)]
196fn add_to_windows_path(paths: &UpstreamPaths) -> Result<()> {
197    use winreg::RegKey;
198    use winreg::enums::*;
199
200    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
201    let env_key = hkcu
202        .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
203        .context("Failed to open registry key")?;
204
205    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
206    let symlinks_norm = normalize_windows_path(&symlinks_path);
207
208    // Get current PATH
209    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
210
211    // Check if our path is already in PATH
212    let path_entries: Vec<&str> = current_path.split(';').collect();
213    if path_entries
214        .iter()
215        .any(|&p| normalize_windows_path(p) == symlinks_norm)
216    {
217        return Ok(()); // Already in PATH
218    }
219
220    // Add our path to the beginning
221    let new_path = if current_path.is_empty() {
222        symlinks_path
223    } else {
224        format!("{};{}", symlinks_path, current_path)
225    };
226
227    env_key
228        .set_value("Path", &new_path)
229        .context("Failed to set PATH")?;
230
231    // Broadcast WM_SETTINGCHANGE to notify other applications
232    broadcast_environment_change();
233
234    Ok(())
235}
236
237#[cfg(windows)]
238fn broadcast_environment_change() {
239    use std::ptr;
240    use winapi::shared::minwindef::LPARAM;
241    use winapi::um::winuser::{
242        HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
243    };
244
245    unsafe {
246        let env_string: Vec<u16> = "Environment\0".encode_utf16().collect();
247        SendMessageTimeoutW(
248            HWND_BROADCAST,
249            WM_SETTINGCHANGE,
250            0,
251            env_string.as_ptr() as LPARAM,
252            SMTO_ABORTIFHUNG,
253            5000,
254            ptr::null_mut(),
255        );
256    }
257}
258
259fn create_package_dirs(paths: &UpstreamPaths) -> io::Result<()> {
260    fs::create_dir_all(&paths.dirs.config_dir)?;
261    fs::create_dir_all(&paths.dirs.data_dir)?;
262    fs::create_dir_all(&paths.dirs.packages_dir)?;
263    fs::create_dir_all(&paths.dirs.cache_dir)?;
264    fs::create_dir_all(&paths.dirs.metadata_dir)?;
265    fs::create_dir_all(&paths.install.appimages_dir)?;
266    fs::create_dir_all(&paths.install.binaries_dir)?;
267    fs::create_dir_all(&paths.install.archives_dir)?;
268    fs::create_dir_all(&paths.install.tmp_dir)?;
269    fs::create_dir_all(&paths.integration.icons_dir)?;
270    fs::create_dir_all(&paths.integration.symlinks_dir)?;
271    for (_shell, dir) in CompletionManager::new(paths).installed_shell_completion_dirs() {
272        fs::create_dir_all(dir)?;
273    }
274    Ok(())
275}
276
277fn create_default_config_file(paths: &UpstreamPaths) -> Result<()> {
278    if paths.config.config_file.exists() {
279        return Ok(());
280    }
281
282    let storage = ConfigStorage::new(&paths.config.config_file)?;
283    storage.save_config()?;
284    Ok(())
285}
286
287#[cfg(unix)]
288fn create_metadata_files(paths: &UpstreamPaths) -> io::Result<()> {
289    if !paths.config.paths_file.exists() {
290        let export_line = format!(
291            r#"export PATH="{}:$PATH""#,
292            paths.integration.symlinks_dir.display()
293        );
294        fs::write(
295            &paths.config.paths_file,
296            format!(
297                "#!/bin/bash\n# Upstream managed PATH additions\n{}\n",
298                export_line
299            ),
300        )?;
301    }
302    if !paths.config.paths_nu_file.exists() {
303        fs::write(
304            &paths.config.paths_nu_file,
305            render_nushell_paths_file(&[paths.integration.symlinks_dir.display().to_string()]),
306        )?;
307    }
308    Ok(())
309}
310
311#[cfg(windows)]
312fn create_metadata_files(_paths: &UpstreamPaths) -> io::Result<()> {
313    // On Windows, we use registry-based PATH, so no metadata files needed
314    Ok(())
315}
316
317#[cfg(unix)]
318fn update_shell_profiles(paths: &UpstreamPaths) -> io::Result<()> {
319    for shell in installed_shell_commands() {
320        match shell.as_str() {
321            "bash" | "sh" => {
322                add_line_to_profile(paths, ".bashrc", SOURCE_LINE_BASH)?;
323            }
324            "zsh" => {
325                add_line_to_profile(paths, ".zshrc", SOURCE_LINE_BASH)?;
326            }
327            "fish" => {
328                let fish_config = Path::new(".config").join("fish").join("config.fish");
329                add_line_to_profile(paths, &fish_config.to_string_lossy(), SOURCE_LINE_FISH)?;
330            }
331            "nu" => {
332                let nushell_config = Path::new(".config").join("nushell").join("config.nu");
333                add_line_to_profile(
334                    paths,
335                    &nushell_config.to_string_lossy(),
336                    SOURCE_LINE_NUSHELL,
337                )?;
338            }
339            _ => {}
340        }
341    }
342    Ok(())
343}
344
345#[cfg(unix)]
346fn check_unix_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> io::Result<()> {
347    let expected_line = format!(
348        r#"export PATH="{}:$PATH""#,
349        paths.integration.symlinks_dir.display()
350    );
351
352    if !paths.config.paths_file.exists() {
353        check_fail(
354            report,
355            format!(
356                "PATH metadata file missing: {}",
357                paths.config.paths_file.display()
358            ),
359        );
360    } else {
361        let content = fs::read_to_string(&paths.config.paths_file)?;
362        if content.contains(&expected_line) {
363            check_ok(
364                report,
365                format!(
366                    "PATH metadata file contains symlink export: {}",
367                    paths.config.paths_file.display()
368                ),
369            );
370        } else {
371            check_fail(
372                report,
373                format!(
374                    "PATH metadata file missing expected export line: {}",
375                    paths.config.paths_file.display()
376                ),
377            );
378        }
379    }
380
381    let expected_nushell_path = paths.integration.symlinks_dir.display().to_string();
382
383    if !paths.config.paths_nu_file.exists() {
384        check_fail(
385            report,
386            format!(
387                "Nushell PATH metadata file missing: {}",
388                paths.config.paths_nu_file.display()
389            ),
390        );
391    } else {
392        let content = fs::read_to_string(&paths.config.paths_nu_file)?;
393        if nushell_paths_file_contains_path(&content, &expected_nushell_path) {
394            check_ok(
395                report,
396                format!(
397                    "Nushell PATH metadata file contains symlink path: {}",
398                    paths.config.paths_nu_file.display()
399                ),
400            );
401        } else {
402            check_fail(
403                report,
404                format!(
405                    "Nushell PATH metadata file missing expected symlink path: {}",
406                    paths.config.paths_nu_file.display()
407                ),
408            );
409        }
410    }
411
412    let mut profiles_to_check: BTreeSet<(String, String)> = BTreeSet::new();
413    for shell in installed_shell_commands() {
414        match shell.as_str() {
415            "bash" | "sh" => {
416                profiles_to_check.insert((".bashrc".to_string(), SOURCE_LINE_BASH.to_string()));
417            }
418            "zsh" => {
419                profiles_to_check.insert((".zshrc".to_string(), SOURCE_LINE_BASH.to_string()));
420            }
421            "fish" => {
422                profiles_to_check.insert((
423                    ".config/fish/config.fish".to_string(),
424                    SOURCE_LINE_FISH.to_string(),
425                ));
426            }
427            "nu" => {
428                profiles_to_check.insert((
429                    ".config/nushell/config.nu".to_string(),
430                    SOURCE_LINE_NUSHELL.to_string(),
431                ));
432            }
433            _ => {}
434        }
435    }
436
437    for (profile_rel, expected_line) in profiles_to_check {
438        let profile_path = paths.dirs.user_dir.join(&profile_rel);
439        if !profile_path.exists() {
440            check_fail(
441                report,
442                format!("Shell profile missing: {}", profile_path.display()),
443            );
444            continue;
445        }
446
447        let content = fs::read_to_string(&profile_path)?;
448        if content.contains(&expected_line) {
449            check_ok(
450                report,
451                format!(
452                    "Shell profile contains upstream hook: {}",
453                    profile_path.display()
454                ),
455            );
456        } else {
457            check_fail(
458                report,
459                format!(
460                    "Shell profile missing upstream hook: {}",
461                    profile_path.display()
462                ),
463            );
464        }
465    }
466
467    Ok(())
468}
469
470#[cfg(unix)]
471fn add_line_to_profile(paths: &UpstreamPaths, relative_path: &str, line: &str) -> io::Result<()> {
472    let profile_path = paths.dirs.user_dir.join(relative_path);
473
474    // Ensure parent directory exists
475    if let Some(parent) = profile_path.parent() {
476        fs::create_dir_all(parent)?;
477    }
478
479    // Backup original file
480    if profile_path.exists() {
481        let backup_path = profile_path.with_extension("bak");
482        if !backup_path.exists() {
483            fs::copy(&profile_path, &backup_path)?;
484        }
485    }
486
487    if !profile_path.exists() {
488        fs::write(&profile_path, format!("{}\n", line))?;
489        return Ok(());
490    }
491
492    let content = fs::read_to_string(&profile_path)?;
493    if !content.contains(line) {
494        let mut file = fs::OpenOptions::new().append(true).open(&profile_path)?;
495        writeln!(file, "\n{}", line)?;
496    }
497
498    Ok(())
499}
500
501#[cfg(unix)]
502pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
503    for shell in installed_shell_commands() {
504        let profile = match shell.as_str() {
505            "bash" | "sh" => Some(".bashrc"),
506            "zsh" => Some(".zshrc"),
507            "fish" => Some(".config/fish/config.fish"),
508            "nu" => Some(".config/nushell/config.nu"),
509            _ => None,
510        };
511        if let Some(profile_rel) = profile {
512            let profile_path = paths.dirs.user_dir.join(profile_rel);
513            if !profile_path.exists() {
514                continue;
515            }
516            let mut content = fs::read_to_string(&profile_path)?;
517            content = content
518                .replace(&format!("{}\n", SOURCE_LINE_BASH), "")
519                .replace(SOURCE_LINE_BASH, "")
520                .replace(&format!("{}\n", SOURCE_LINE_FISH), "")
521                .replace(SOURCE_LINE_FISH, "")
522                .replace(&format!("{}\n", SOURCE_LINE_NUSHELL), "")
523                .replace(SOURCE_LINE_NUSHELL, "");
524            fs::write(&profile_path, content)?;
525        }
526    }
527    Ok(())
528}
529
530#[cfg(windows)]
531pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
532    remove_from_windows_path(paths)
533}
534
535#[cfg(windows)]
536fn remove_from_windows_path(paths: &UpstreamPaths) -> Result<()> {
537    use winreg::RegKey;
538    use winreg::enums::*;
539
540    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
541    let env_key = hkcu
542        .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
543        .context("Failed to open registry key")?;
544
545    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
546    let symlinks_norm = normalize_windows_path(&symlinks_path);
547
548    // Get current PATH
549    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
550
551    // Remove our path from PATH
552    let path_entries: Vec<&str> = current_path
553        .split(';')
554        .filter(|&p| normalize_windows_path(p) != symlinks_norm)
555        .collect();
556
557    let new_path = path_entries.join(";");
558
559    env_key
560        .set_value("Path", &new_path)
561        .context("Failed to set PATH")?;
562
563    // Broadcast WM_SETTINGCHANGE to notify other applications
564    broadcast_environment_change();
565
566    Ok(())
567}
568
569#[cfg(windows)]
570fn check_windows_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> Result<()> {
571    use winreg::RegKey;
572    use winreg::enums::*;
573
574    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
575    let env_key = hkcu
576        .open_subkey_with_flags("Environment", KEY_READ)
577        .context("Failed to open PATH")?;
578
579    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
580    let symlinks_norm = normalize_windows_path(&symlinks_path);
581    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
582
583    let in_path = current_path
584        .split(';')
585        .any(|p| normalize_windows_path(p) == symlinks_norm);
586
587    if in_path {
588        check_ok(report, "Windows PATH contains upstream symlinks directory");
589    } else {
590        check_fail(report, "Windows PATH missing upstream symlinks directory");
591    }
592
593    Ok(())
594}
595
596#[cfg(test)]
597mod tests {
598    use super::purge_data;
599    use crate::services::storage::manifest_storage::{CURRENT_LAYOUT_VERSION, MANIFEST_FILE_NAME};
600    use crate::utils::static_paths::{
601        AppDirs, ConfigPaths, InstallPaths, IntegrationPaths, UpstreamPaths,
602    };
603    use std::path::{Path, PathBuf};
604    use std::time::{SystemTime, UNIX_EPOCH};
605    use std::{fs, io};
606
607    fn temp_root(name: &str) -> PathBuf {
608        let nanos = SystemTime::now()
609            .duration_since(UNIX_EPOCH)
610            .map(|d| d.as_nanos())
611            .unwrap_or(0);
612        std::env::temp_dir().join(format!("upstream-init-test-{name}-{nanos}"))
613    }
614
615    fn test_paths(root: &Path) -> UpstreamPaths {
616        let dirs = AppDirs {
617            user_dir: root.to_path_buf(),
618            config_dir: root.join("config"),
619            data_dir: root.join(".upstream"),
620            packages_dir: root.join(".upstream/packages"),
621            cache_dir: root.join(".upstream/cache"),
622            metadata_dir: root.join(".upstream/metadata"),
623        };
624
625        UpstreamPaths {
626            config: ConfigPaths {
627                config_file: dirs.config_dir.join("config.toml"),
628                packages_file: dirs.metadata_dir.join("packages.json"),
629                metadata_file: dirs.metadata_dir.join("metadata.json"),
630                trust_file: dirs.metadata_dir.join("trust.json"),
631                paths_file: dirs.metadata_dir.join("paths.sh"),
632                paths_nu_file: dirs.metadata_dir.join("paths.nu"),
633            },
634            install: InstallPaths {
635                appimages_dir: dirs.packages_dir.join("appimages"),
636                binaries_dir: dirs.packages_dir.join("binaries"),
637                archives_dir: dirs.packages_dir.join("archives"),
638                rollback_dir: dirs.data_dir.join("rollback"),
639                tmp_dir: dirs.data_dir.join("tmp"),
640            },
641            integration: IntegrationPaths {
642                symlinks_dir: dirs.data_dir.join("symlinks"),
643                xdg_applications_dir: dirs.user_dir.join(".local/share/applications"),
644                icons_dir: dirs.data_dir.join("icons"),
645                bash_completions_dir: dirs
646                    .user_dir
647                    .join(".local/share/bash-completion/completions"),
648                fish_completions_dir: dirs.user_dir.join(".config/fish/completions"),
649                zsh_completions_dir: dirs.user_dir.join(".local/share/zsh/site-functions"),
650            },
651            dirs,
652        }
653    }
654
655    fn cleanup(path: &Path) -> io::Result<()> {
656        if path.exists() {
657            fs::remove_dir_all(path)?;
658        }
659        Ok(())
660    }
661
662    #[test]
663    fn create_manifest_file_writes_current_layout_manifest() {
664        let root = temp_root("manifest-file");
665        let paths = test_paths(&root);
666
667        super::create_manifest_file(&paths).expect("create manifest file");
668
669        let manifest_path = paths.dirs.data_dir.join(MANIFEST_FILE_NAME);
670        let manifest: serde_json::Value =
671            serde_json::from_slice(&fs::read(&manifest_path).expect("read manifest"))
672                .expect("parse manifest");
673        assert_eq!(
674            manifest["layout_version"].as_u64(),
675            Some(CURRENT_LAYOUT_VERSION as u64)
676        );
677        assert_eq!(
678            manifest["platform"]["os"].as_str(),
679            Some(std::env::consts::OS)
680        );
681
682        cleanup(&root).expect("cleanup");
683    }
684
685    #[test]
686    fn create_trust_file_writes_empty_trust_storage() {
687        let root = temp_root("trust-file");
688        let paths = test_paths(&root);
689
690        super::create_trust_file(&paths).expect("create trust file");
691
692        let trust: serde_json::Value =
693            serde_json::from_slice(&fs::read(&paths.config.trust_file).expect("read trust file"))
694                .expect("parse trust file");
695        assert_eq!(trust["version"].as_u64(), Some(1));
696        assert_eq!(
697            trust["minisign_public_keys"].as_array().map(Vec::len),
698            Some(0)
699        );
700        assert_eq!(
701            trust["cosign_public_keys"].as_array().map(Vec::len),
702            Some(0)
703        );
704
705        cleanup(&root).expect("cleanup");
706    }
707
708    #[test]
709    fn check_reports_manifest_file_status() {
710        let root = temp_root("manifest-check");
711        let paths = test_paths(&root);
712
713        let missing_report = super::check(&paths).expect("check missing manifest");
714        assert!(
715            missing_report
716                .messages
717                .iter()
718                .map(|message| console::strip_ansi_codes(message).to_string())
719                .any(|message| message.contains("[fail]")
720                    && message.contains("manifest file missing"))
721        );
722
723        fs::create_dir_all(&paths.dirs.data_dir).expect("create data dir");
724        super::create_manifest_file(&paths).expect("create manifest");
725        let present_report = super::check(&paths).expect("check present manifest");
726        assert!(
727            present_report
728                .messages
729                .iter()
730                .map(|message| console::strip_ansi_codes(message).to_string())
731                .any(|message| message.contains("[ok]") && message.contains("manifest file exists"))
732        );
733
734        cleanup(&root).expect("cleanup");
735    }
736
737    #[test]
738    fn check_reports_trust_file_status() {
739        let root = temp_root("trust-check");
740        let paths = test_paths(&root);
741
742        let missing_report = super::check(&paths).expect("check missing trust");
743        assert!(
744            missing_report
745                .messages
746                .iter()
747                .map(|message| console::strip_ansi_codes(message).to_string())
748                .any(|message| message.contains("[fail]")
749                    && message.contains("trust metadata file missing"))
750        );
751
752        fs::create_dir_all(&paths.dirs.metadata_dir).expect("create metadata dir");
753        super::create_trust_file(&paths).expect("create trust");
754        let present_report = super::check(&paths).expect("check present trust");
755        assert!(
756            present_report
757                .messages
758                .iter()
759                .map(|message| console::strip_ansi_codes(message).to_string())
760                .any(|message| message.contains("[ok]")
761                    && message.contains("trust metadata file exists"))
762        );
763
764        cleanup(&root).expect("cleanup");
765    }
766
767    #[cfg(unix)]
768    #[test]
769    fn create_metadata_files_creates_posix_and_nushell_path_files() {
770        let root = temp_root("metadata-files");
771        let paths = test_paths(&root);
772        fs::create_dir_all(&paths.dirs.metadata_dir).expect("create metadata dir");
773
774        super::create_metadata_files(&paths).expect("create metadata files");
775
776        let posix_content = fs::read_to_string(&paths.config.paths_file).expect("read paths.sh");
777        assert!(posix_content.contains("export PATH="));
778        assert!(posix_content.contains(&paths.integration.symlinks_dir.display().to_string()));
779
780        let nushell_content =
781            fs::read_to_string(&paths.config.paths_nu_file).expect("read paths.nu");
782        assert!(nushell_content.contains("let upstream_paths = ["));
783        assert!(nushell_content.contains("$env.PATH = ($upstream_paths ++ $env.PATH)"));
784        assert!(nushell_content.contains(&paths.integration.symlinks_dir.display().to_string()));
785
786        cleanup(&root).expect("cleanup");
787    }
788
789    #[test]
790    fn purge_data_removes_data_dir_but_keeps_config_dir() {
791        let root = temp_root("purge");
792        let paths = test_paths(&root);
793        fs::create_dir_all(&paths.dirs.data_dir).expect("create data dir");
794        fs::create_dir_all(&paths.dirs.config_dir).expect("create config dir");
795        fs::write(paths.dirs.data_dir.join("data"), b"data").expect("write data");
796        fs::write(paths.dirs.config_dir.join("config.toml"), b"").expect("write config");
797
798        purge_data(&paths).expect("purge data");
799
800        assert!(!paths.dirs.data_dir.exists());
801        assert!(paths.dirs.config_dir.exists());
802
803        cleanup(&root).expect("cleanup");
804    }
805}