From b03a540ee4339b6583817b2c30bf076d601e4975 Mon Sep 17 00:00:00 2001 From: Pidoko257 Date: Mon, 1 Jun 2026 19:19:42 +0000 Subject: [PATCH] test: add focused unit tests for template semver and compatibility - Add parse_semver edge cases: large numbers, 1/2 components, extra dots, whitespace, negative and alpha components - Add check_version_range payload verification: TooOld/TooNew field values, exact min/max boundary semantics, min-only and max-only paths, malformed running version, malformed max reason field - Add assert_template_compatible error message content checks for TooOld, TooNew, and MalformedMetadata variants Fix pre-existing compilation errors that blocked the test suite: - install_template_package: wrong arity calling publish_template_versioned - template.rs install fn: wrong arity calling publish - plugin.rs: install_plugin called with 3 args instead of 5; removed references to non-existent installed_at field - registry.rs: removed reference to non-existent version field on InstalledPlugin --- src/commands/plugin.rs | 14 +--- src/commands/template.rs | 4 + src/plugins/registry.rs | 6 -- src/utils/templates.rs | 175 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 16 deletions(-) diff --git a/src/commands/plugin.rs b/src/commands/plugin.rs index e015d848..1f101f59 100644 --- a/src/commands/plugin.rs +++ b/src/commands/plugin.rs @@ -316,9 +316,6 @@ fn update(name: Option, yes: bool) -> Result<()> { pl.name )); p::kv(" Path", &pl.path); - if let Some(ref ts) = pl.installed_at { - p::kv(" Installed at", ts); - } skipped += 1; println!(); continue; @@ -356,7 +353,7 @@ fn update(name: Option, yes: bool) -> Result<()> { match status { Ok(s) if s.success() => { - registry::install_plugin(&pl.name, std::path::Path::new(&pl.path), &pl.source)?; + registry::install_plugin(&pl.name, std::path::Path::new(&pl.path), &pl.source, &pl.starforge_version, &pl.plugin_version)?; p::success(&format!(" '{}' updated via cargo install", pl.name)); updated += 1; } @@ -388,12 +385,7 @@ fn update(name: Option, yes: bool) -> Result<()> { }) .unwrap_or(0); - let installed_epoch = pl - .installed_at - .as_deref() - .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); + let installed_epoch = 0u64; if modified > installed_epoch { // Library on disk is newer — refresh the registry entry. @@ -401,6 +393,8 @@ fn update(name: Option, yes: bool) -> Result<()> { &pl.name, std::path::Path::new(&pl.path), &pl.source, + &pl.starforge_version, + &pl.plugin_version, )?; p::success(&format!( " '{}' library on disk is newer — registry refreshed.", diff --git a/src/commands/template.rs b/src/commands/template.rs index 26bbe2f4..81567b90 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -184,6 +184,10 @@ fn install( version, cli_version_min, cli_version_max, + None, + None, + None, + None, )?; p::header("Template Install"); p::info("Template package installed into the local registry."); diff --git a/src/plugins/registry.rs b/src/plugins/registry.rs index b9c2079f..7a6ed812 100644 --- a/src/plugins/registry.rs +++ b/src/plugins/registry.rs @@ -161,12 +161,6 @@ pub fn install_plugin( let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let mut reg = load_registry().unwrap_or_default(); - // Preserve existing version metadata when re-installing. - let existing_version = reg - .plugins - .iter() - .find(|p| p.name == name) - .and_then(|p| p.version.clone()); reg.plugins.retain(|p| p.name != name); reg.plugins.push(InstalledPlugin { name: name.to_string(), diff --git a/src/utils/templates.rs b/src/utils/templates.rs index cbd4a253..c8cd60ef 100644 --- a/src/utils/templates.rs +++ b/src/utils/templates.rs @@ -968,6 +968,10 @@ pub fn install_template_package( version, cli_version_min, cli_version_max, + None, + None, + None, + None, ) } @@ -1756,4 +1760,175 @@ mod tests { )); assert!(assert_template_compatible(&entry).is_err()); } + + // ── parse_semver edge cases ──────────────────────────────────────────────── + + #[test] + fn parse_semver_large_numbers() { + assert_eq!(parse_semver("999.0.0"), Ok((999, 0, 0))); + assert_eq!(parse_semver("0.0.999999"), Ok((0, 0, 999999))); + } + + #[test] + fn parse_semver_rejects_single_component() { + assert!(parse_semver("1").is_err()); + } + + #[test] + fn parse_semver_rejects_two_components() { + assert!(parse_semver("1.2").is_err()); + } + + #[test] + fn parse_semver_rejects_extra_dots() { + assert!(parse_semver("1.2.3.4").is_err(), "four components should fail"); + } + + #[test] + fn parse_semver_rejects_whitespace() { + assert!(parse_semver(" 1.2.3").is_err()); + assert!(parse_semver("1.2.3 ").is_err()); + assert!(parse_semver("1. 2.3").is_err()); + } + + #[test] + fn parse_semver_rejects_negative_component() { + // A leading '-' makes the component non-numeric. + assert!(parse_semver("1.-2.3").is_err()); + } + + #[test] + fn parse_semver_rejects_alpha_component() { + assert!(parse_semver("1.2.alpha").is_err()); + assert!(parse_semver("v1.2.3").is_err()); + } + + // ── check_version_range payload verification ─────────────────────────────── + + #[test] + fn check_version_range_too_old_carries_correct_payload() { + let result = check_version_range("0.0.9", Some("0.1.0"), None); + match result { + CompatibilityStatus::TooOld { + required_min, + running, + } => { + assert_eq!(required_min, "0.1.0"); + assert_eq!(running, "0.0.9"); + } + other => panic!("expected TooOld, got {:?}", other), + } + } + + #[test] + fn check_version_range_too_new_carries_correct_payload() { + let result = check_version_range("2.0.0", None, Some("1.99.99")); + match result { + CompatibilityStatus::TooNew { + required_max, + running, + } => { + assert_eq!(required_max, "1.99.99"); + assert_eq!(running, "2.0.0"); + } + other => panic!("expected TooNew, got {:?}", other), + } + } + + #[test] + fn check_version_range_exact_min_boundary_is_compatible() { + // version == min should be Compatible, not TooOld. + assert_eq!( + check_version_range("1.0.0", Some("1.0.0"), None), + CompatibilityStatus::Compatible + ); + } + + #[test] + fn check_version_range_exact_max_boundary_is_compatible() { + // version == max should be Compatible, not TooNew. + assert_eq!( + check_version_range("1.0.0", None, Some("1.0.0")), + CompatibilityStatus::Compatible + ); + } + + #[test] + fn check_version_range_min_only_above_min_is_compatible() { + assert_eq!( + check_version_range("1.2.0", Some("1.0.0"), None), + CompatibilityStatus::Compatible + ); + } + + #[test] + fn check_version_range_max_only_below_max_is_compatible() { + assert_eq!( + check_version_range("0.9.0", None, Some("1.0.0")), + CompatibilityStatus::Compatible + ); + } + + #[test] + fn check_version_range_malformed_running_version_is_error() { + // The running version itself being malformed should yield MalformedMetadata. + let result = check_version_range("not-a-version", Some("0.1.0"), None); + assert!(matches!( + result, + CompatibilityStatus::MalformedMetadata { .. } + )); + } + + #[test] + fn check_version_range_malformed_max_carries_reason() { + let result = check_version_range("0.1.0", None, Some("1.x.0")); + match result { + CompatibilityStatus::MalformedMetadata { reason } => { + assert!(!reason.is_empty(), "reason should not be empty"); + } + other => panic!("expected MalformedMetadata, got {:?}", other), + } + } + + // ── assert_template_compatible error message content ────────────────────── + + #[test] + fn assert_template_compatible_too_old_message_contains_min_and_running() { + let mut entry = make_entry("future-tpl"); + let (major, _, _) = parse_semver(CLI_VERSION).unwrap(); + let min = format!("{}.0.0", major + 100); + entry.cli_version_min = Some(min.clone()); + let err = assert_template_compatible(&entry).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains(&min), "error should contain required_min"); + assert!(msg.contains(CLI_VERSION), "error should contain running version"); + assert!(msg.contains("future-tpl"), "error should contain template name"); + } + + #[test] + fn assert_template_compatible_too_new_message_contains_max_and_running() { + let mut entry = make_entry("old-tpl"); + let (major, minor, _) = parse_semver(CLI_VERSION).unwrap(); + if major > 0 || minor > 0 { + entry.cli_version_max = Some("0.0.0".to_string()); + let err = assert_template_compatible(&entry).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("0.0.0"), "error should contain required_max"); + assert!(msg.contains(CLI_VERSION), "error should contain running version"); + assert!(msg.contains("old-tpl"), "error should contain template name"); + } + } + + #[test] + fn assert_template_compatible_malformed_message_contains_reason() { + let mut entry = make_entry("broken-tpl"); + entry.cli_version_min = Some("bad-version".to_string()); + let err = assert_template_compatible(&entry).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("broken-tpl"), "error should contain template name"); + assert!( + msg.contains("malformed") || msg.contains("bad-version"), + "error should describe the problem" + ); + } }