pub fn is_newer_version(current: &str, last_seen: &str) -> bool {
parse_semver(current)
.zip(parse_semver(last_seen))
.is_some_and(|(c, l)| c > l)
}
pub fn is_valid_version(s: &str) -> bool {
parse_semver(s).is_some()
}
pub struct BootDecision {
pub should_show_popup: bool,
pub should_persist: bool,
}
pub fn boot_decision(current_ver: &str, last_seen: Option<&str>) -> BootDecision {
let last_seen_parseable = last_seen.is_some_and(is_valid_version);
let should_show_popup = match last_seen {
Some(last) if last_seen_parseable => {
is_newer_version(current_ver, last) && release_notes(current_ver).is_some()
}
_ => false,
};
let should_persist = should_show_popup || last_seen.is_none() || !last_seen_parseable;
BootDecision {
should_show_popup,
should_persist,
}
}
fn parse_semver(v: &str) -> Option<(u64, u64, u64, u8)> {
let mut parts = v.splitn(3, '.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch_str = parts.next().unwrap_or("0");
let (patch_num, is_release) = match patch_str.split_once('-') {
Some((num, _prerelease)) => (num.parse().ok()?, 0u8),
None => (patch_str.parse().ok()?, 1u8),
};
Some((major, minor, patch_num, is_release))
}
pub fn release_notes(version: &str) -> Option<&'static [&'static str]> {
match version {
"0.6.0" => Some(&[
"Windows support — native hook transport, installer, and release builds",
"Install via npm — `npm i -g pixtuoid` now works on macOS, Linux & Windows",
"Reasonix sessions now visualized — re-run `pixtuoid install-hooks` to wire it",
"Sharper agent activity — fewer ghost & duplicate sprites, and Codex stays active during web & tool search",
"Diagnostics you can see — source-death footer warnings, config warnings on stderr, an always-on log file",
"New project site — live demos, architecture & contributing docs, weather gallery",
]),
"0.4.0" => Some(&[
"Renamed from ascii-agents to pixtuoid",
"Run `pixtuoid install-hooks` to update hooks",
"New env vars: PIXTUOID_SOCKET/HOOK/LOG",
"Flaky startup test fixed + 250ms rescan",
]),
"0.4.1" => Some(&[
"Per-floor boot capacity fixes invisible-agent edge case",
"install-hooks now strips legacy `_ascii_agents` entries again",
"Resize mid-slide lands on destination floor, not source",
"Version popup URL no longer mis-clicks on narrow terminals",
"Corrupted last_seen_version self-heals on next launch",
]),
"0.5.0" => Some(&[
"Now visualizes Codex sessions too — re-run `pixtuoid install-hooks`",
"Office overhaul: unified furniture + smarter approach/seating pathfinding",
"Glass meeting rooms, denser desk pods, day/night lighting",
"Physics-grounded weather: storms, lightning, moonlight",
"Real-physics walking, animated floor transitions, emergent meeting chitchat",
"Custom pet names via `[[pets]]` config",
]),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn newer_version_detected() {
assert!(is_newer_version("0.2.0", "0.1.0"));
}
#[test]
fn same_version_not_newer() {
assert!(!is_newer_version("0.1.0", "0.1.0"));
}
#[test]
fn older_not_newer() {
assert!(!is_newer_version("0.1.0", "0.2.0"));
}
#[test]
fn major_bump_detected() {
assert!(is_newer_version("1.0.0", "0.9.9"));
}
#[test]
fn minor_bump_detected() {
assert!(is_newer_version("0.5.0", "0.4.0"));
}
#[test]
fn patch_bump_detected() {
assert!(is_newer_version("0.4.1", "0.4.0"));
}
#[test]
fn bad_input_safe() {
assert!(!is_newer_version("not-semver", "0.1.0"));
assert!(!is_newer_version("0.1.0", "garbage"));
assert!(!is_newer_version("", ""));
}
#[test]
fn prerelease_newer_than_older_release() {
assert!(is_newer_version("0.5.0-alpha", "0.4.0"));
}
#[test]
fn release_newer_than_prerelease_of_same_version() {
assert!(is_newer_version("0.5.0", "0.5.0-rc1"));
assert!(!is_newer_version("0.5.0-rc1", "0.5.0"));
}
#[test]
fn release_notes_known_version() {
assert!(release_notes("0.4.0").is_some());
}
#[test]
fn release_notes_unknown_version() {
assert!(release_notes("9.9.9").is_none());
}
#[test]
fn current_version_has_release_notes() {
let current = env!("CARGO_PKG_VERSION");
assert!(
release_notes(current).is_some(),
"release_notes({current:?}) returned None — add an arm for the current version"
);
}
#[test]
fn path_dep_version_tracks_crate_version() {
let manifest = include_str!("../Cargo.toml");
let dep_line = manifest
.lines()
.find(|l| l.trim_start().starts_with("pixtuoid-core") && l.contains("path ="))
.expect("a pixtuoid-core path-dependency line in crates/pixtuoid/Cargo.toml");
let dep_version = dep_line
.split_once("version = \"")
.and_then(|(_, rest)| rest.split('"').next())
.expect("a version requirement on the pixtuoid-core path-dep");
assert_eq!(
dep_version,
env!("CARGO_PKG_VERSION"),
"pixtuoid-core path-dep version ({dep_version}) != crate version ({}) — run `just bump` (see #110)",
env!("CARGO_PKG_VERSION")
);
}
#[test]
fn is_valid_version_accepts_well_formed() {
assert!(is_valid_version("0.4.0"));
assert!(is_valid_version("1.2.3"));
assert!(is_valid_version("0.5.0-rc1"));
}
#[test]
fn is_valid_version_rejects_corrupted() {
assert!(!is_valid_version("v0.4.0"), "leading v is not semver");
assert!(!is_valid_version("garbage"));
assert!(!is_valid_version(""));
}
#[test]
fn boot_decision_overwrites_corrupted_last_seen() {
let d = boot_decision("0.4.1", Some("v0.4.0"));
assert!(
!d.should_show_popup,
"can't show popup when comparison fails"
);
assert!(
d.should_persist,
"corrupted last_seen must be overwritten to recover"
);
}
#[test]
fn boot_decision_first_run_persists_silently() {
let d = boot_decision("0.4.1", None);
assert!(!d.should_show_popup);
assert!(d.should_persist);
}
#[test]
fn boot_decision_upgrade_shows_popup_and_persists() {
let d = boot_decision("0.4.0", Some("0.3.0"));
assert!(d.should_show_popup);
assert!(d.should_persist);
}
#[test]
fn boot_decision_same_version_no_action() {
let d = boot_decision("0.4.0", Some("0.4.0"));
assert!(!d.should_show_popup);
assert!(!d.should_persist);
}
#[test]
fn boot_decision_downgrade_no_action() {
let d = boot_decision("0.3.0", Some("0.4.0"));
assert!(!d.should_show_popup);
assert!(!d.should_persist);
}
}