1use semver::Version;
2
3pub struct PostInitOutcome {
4 pub upgrade_toast: Option<String>,
5}
6
7pub fn evaluate(paths: Option<&crate::runtime::env::Paths>) -> PostInitOutcome {
8 let current = match Version::parse(env!("CARGO_PKG_VERSION")) {
9 Ok(v) => v,
10 Err(_) => {
11 return PostInitOutcome {
12 upgrade_toast: None,
13 };
14 }
15 };
16 let last = crate::preferences::load_last_seen_version(paths)
17 .ok()
18 .flatten()
19 .and_then(|s| Version::parse(s.as_str()).ok());
20
21 if last.is_none() {
26 return PostInitOutcome {
27 upgrade_toast: None,
28 };
29 }
30
31 if let Some(ref seen) = last {
32 if seen >= ¤t {
33 return PostInitOutcome {
34 upgrade_toast: None,
35 };
36 }
37 }
38
39 let sections = crate::changelog::cached();
40 let shown = crate::changelog::versions_to_show(sections, last.as_ref(), ¤t, 5);
41 if shown.is_empty() {
42 return PostInitOutcome {
48 upgrade_toast: None,
49 };
50 }
51
52 log::debug!(
53 "[purple] queued upgrade toast: {} sections (last_seen={:?}, current={})",
54 shown.len(),
55 last.as_ref().map(|v| v.to_string()),
56 current
57 );
58 PostInitOutcome {
59 upgrade_toast: Some(crate::messages::whats_new_toast::upgraded(
60 ¤t.to_string(),
61 )),
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::preferences;
69 use crate::runtime::env::Paths;
70
71 fn current() -> String {
72 env!("CARGO_PKG_VERSION").to_string()
73 }
74
75 #[test]
76 fn first_launch_returns_no_toast() {
77 let dir = tempfile::tempdir().unwrap();
78 let paths = Paths::new(dir.path());
79 let outcome = evaluate(Some(&paths));
80 assert!(
81 outcome.upgrade_toast.is_none(),
82 "first launch must not show upgrade toast"
83 );
84 }
85
86 #[test]
87 fn up_to_date_returns_no_toast() {
88 let dir = tempfile::tempdir().unwrap();
89 let paths = Paths::new(dir.path());
90 preferences::save_last_seen_version(Some(&paths), ¤t()).unwrap();
91 let outcome = evaluate(Some(&paths));
92 assert!(outcome.upgrade_toast.is_none());
93 assert_eq!(
97 preferences::load_last_seen_version(Some(&paths))
98 .unwrap()
99 .as_deref(),
100 Some(current().as_str()),
101 "evaluate() must not touch last_seen_version when up-to-date"
102 );
103 }
104
105 #[test]
106 fn downgrade_returns_no_toast() {
107 let dir = tempfile::tempdir().unwrap();
108 let paths = Paths::new(dir.path());
109 preferences::save_last_seen_version(Some(&paths), "999.0.0").unwrap();
110 let outcome = evaluate(Some(&paths));
111 assert!(outcome.upgrade_toast.is_none());
112 }
113
114 #[test]
115 fn upgrade_with_new_sections_returns_toast() {
116 let dir = tempfile::tempdir().unwrap();
117 let paths = Paths::new(dir.path());
118 preferences::save_last_seen_version(Some(&paths), "0.0.1").unwrap();
119 let outcome = evaluate(Some(&paths));
120 let fragment = crate::messages::whats_new_toast::INVITE_FRAGMENT;
121 assert!(
122 outcome
123 .upgrade_toast
124 .as_deref()
125 .is_some_and(|t| t.contains(fragment)),
126 "expected upgrade toast with invite fragment"
127 );
128 }
129
130 #[test]
131 fn evaluate_never_writes_last_seen_version() {
132 let scenarios: &[(&str, Option<&str>)] = &[
146 ("first_launch", None),
147 ("same_version", Some(env!("CARGO_PKG_VERSION"))),
148 ("downgrade", Some("999.0.0")),
149 ("older_version", Some("0.0.1")),
150 ("unparseable", Some("not-a-semver")),
151 ];
152 for (label, input) in scenarios {
153 let dir = tempfile::tempdir().unwrap();
154 let paths = Paths::new(dir.path());
155 if let Some(v) = input {
156 preferences::save_last_seen_version(Some(&paths), v).unwrap();
157 }
158 let _ = evaluate(Some(&paths));
159 let after = preferences::load_last_seen_version(Some(&paths)).unwrap();
160 assert_eq!(
161 after.as_deref(),
162 *input,
163 "[{}] evaluate() must not touch last_seen_version",
164 label
165 );
166 }
167 }
168
169 #[test]
170 fn unparseable_last_seen_falls_through_to_first_launch() {
171 let dir = tempfile::tempdir().unwrap();
172 let paths = Paths::new(dir.path());
173 preferences::save_last_seen_version(Some(&paths), "not-a-semver").unwrap();
174 let outcome = evaluate(Some(&paths));
175 assert!(
176 outcome.upgrade_toast.is_none(),
177 "garbled last_seen must be treated as first launch, not surface a toast"
178 );
179 }
180}