1use semver::Version;
2
3pub struct PostInitOutcome {
4 pub upgrade_toast: Option<String>,
5}
6
7pub fn evaluate() -> 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()
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
70 fn current() -> String {
71 env!("CARGO_PKG_VERSION").to_string()
72 }
73
74 #[test]
75 fn first_launch_returns_no_toast() {
76 preferences::tests_helpers::with_temp_prefs("onboarding_first", |_| {
77 let outcome = evaluate();
78 assert!(
79 outcome.upgrade_toast.is_none(),
80 "first launch must not show upgrade toast"
81 );
82 });
83 }
84
85 #[test]
86 fn up_to_date_returns_no_toast() {
87 preferences::tests_helpers::with_temp_prefs("onboarding_up_to_date", |_| {
88 preferences::save_last_seen_version(¤t()).unwrap();
89 let outcome = evaluate();
90 assert!(outcome.upgrade_toast.is_none());
91 assert_eq!(
95 preferences::load_last_seen_version().unwrap().as_deref(),
96 Some(current().as_str()),
97 "evaluate() must not touch last_seen_version when up-to-date"
98 );
99 });
100 }
101
102 #[test]
103 fn downgrade_returns_no_toast() {
104 preferences::tests_helpers::with_temp_prefs("onboarding_downgrade", |_| {
105 preferences::save_last_seen_version("999.0.0").unwrap();
106 let outcome = evaluate();
107 assert!(outcome.upgrade_toast.is_none());
108 });
109 }
110
111 #[test]
112 fn upgrade_with_new_sections_returns_toast() {
113 preferences::tests_helpers::with_temp_prefs("onboarding_upgrade_toast", |_| {
114 preferences::save_last_seen_version("0.0.1").unwrap();
115 let outcome = evaluate();
116 let fragment = crate::messages::whats_new_toast::INVITE_FRAGMENT;
117 assert!(
118 outcome
119 .upgrade_toast
120 .as_deref()
121 .is_some_and(|t| t.contains(fragment)),
122 "expected upgrade toast with invite fragment"
123 );
124 });
125 }
126
127 #[test]
128 fn evaluate_never_writes_last_seen_version() {
129 let scenarios: &[(&str, Option<&str>)] = &[
143 ("first_launch", None),
144 ("same_version", Some(env!("CARGO_PKG_VERSION"))),
145 ("downgrade", Some("999.0.0")),
146 ("older_version", Some("0.0.1")),
147 ("unparseable", Some("not-a-semver")),
148 ];
149 for (label, input) in scenarios {
150 preferences::tests_helpers::with_temp_prefs(
151 &format!("onboarding_no_writes_{label}"),
152 |_| {
153 if let Some(v) = input {
154 preferences::save_last_seen_version(v).unwrap();
155 }
156 let _ = evaluate();
157 let after = preferences::load_last_seen_version().unwrap();
158 assert_eq!(
159 after.as_deref(),
160 *input,
161 "[{}] evaluate() must not touch last_seen_version",
162 label
163 );
164 },
165 );
166 }
167 }
168
169 #[test]
170 fn unparseable_last_seen_falls_through_to_first_launch() {
171 preferences::tests_helpers::with_temp_prefs("onboarding_unparseable", |_| {
172 preferences::save_last_seen_version("not-a-semver").unwrap();
173 let outcome = evaluate();
174 assert!(
175 outcome.upgrade_toast.is_none(),
176 "garbled last_seen must be treated as first launch, not surface a toast"
177 );
178 });
179 }
180}