use semver::Version;
pub struct PostInitOutcome {
pub upgrade_toast: Option<String>,
}
pub fn evaluate(paths: Option<&crate::runtime::env::Paths>) -> PostInitOutcome {
let current = match Version::parse(env!("CARGO_PKG_VERSION")) {
Ok(v) => v,
Err(_) => {
return PostInitOutcome {
upgrade_toast: None,
};
}
};
let last = crate::preferences::load_last_seen_version(paths)
.ok()
.flatten()
.and_then(|s| Version::parse(s.as_str()).ok());
if last.is_none() {
return PostInitOutcome {
upgrade_toast: None,
};
}
if let Some(ref seen) = last {
if seen >= ¤t {
return PostInitOutcome {
upgrade_toast: None,
};
}
}
let sections = crate::changelog::cached();
let shown = crate::changelog::versions_to_show(sections, last.as_ref(), ¤t, 5);
if shown.is_empty() {
return PostInitOutcome {
upgrade_toast: None,
};
}
log::debug!(
"[purple] queued upgrade toast: {} sections (last_seen={:?}, current={})",
shown.len(),
last.as_ref().map(|v| v.to_string()),
current
);
PostInitOutcome {
upgrade_toast: Some(crate::messages::whats_new_toast::upgraded(
¤t.to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::preferences;
use crate::runtime::env::Paths;
fn current() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[test]
fn first_launch_returns_no_toast() {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
let outcome = evaluate(Some(&paths));
assert!(
outcome.upgrade_toast.is_none(),
"first launch must not show upgrade toast"
);
}
#[test]
fn up_to_date_returns_no_toast() {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
preferences::save_last_seen_version(Some(&paths), ¤t()).unwrap();
let outcome = evaluate(Some(&paths));
assert!(outcome.upgrade_toast.is_none());
assert_eq!(
preferences::load_last_seen_version(Some(&paths))
.unwrap()
.as_deref(),
Some(current().as_str()),
"evaluate() must not touch last_seen_version when up-to-date"
);
}
#[test]
fn downgrade_returns_no_toast() {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
preferences::save_last_seen_version(Some(&paths), "999.0.0").unwrap();
let outcome = evaluate(Some(&paths));
assert!(outcome.upgrade_toast.is_none());
}
#[test]
fn upgrade_with_new_sections_returns_toast() {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
preferences::save_last_seen_version(Some(&paths), "0.0.1").unwrap();
let outcome = evaluate(Some(&paths));
let fragment = crate::messages::whats_new_toast::INVITE_FRAGMENT;
assert!(
outcome
.upgrade_toast
.as_deref()
.is_some_and(|t| t.contains(fragment)),
"expected upgrade toast with invite fragment"
);
}
#[test]
fn evaluate_never_writes_last_seen_version() {
let scenarios: &[(&str, Option<&str>)] = &[
("first_launch", None),
("same_version", Some(env!("CARGO_PKG_VERSION"))),
("downgrade", Some("999.0.0")),
("older_version", Some("0.0.1")),
("unparseable", Some("not-a-semver")),
];
for (label, input) in scenarios {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
if let Some(v) = input {
preferences::save_last_seen_version(Some(&paths), v).unwrap();
}
let _ = evaluate(Some(&paths));
let after = preferences::load_last_seen_version(Some(&paths)).unwrap();
assert_eq!(
after.as_deref(),
*input,
"[{}] evaluate() must not touch last_seen_version",
label
);
}
}
#[test]
fn unparseable_last_seen_falls_through_to_first_launch() {
let dir = tempfile::tempdir().unwrap();
let paths = Paths::new(dir.path());
preferences::save_last_seen_version(Some(&paths), "not-a-semver").unwrap();
let outcome = evaluate(Some(&paths));
assert!(
outcome.upgrade_toast.is_none(),
"garbled last_seen must be treated as first launch, not surface a toast"
);
}
}