Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions src/commands/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,6 @@ fn update(name: Option<String>, 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;
Expand Down Expand Up @@ -356,7 +353,7 @@ fn update(name: Option<String>, 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;
}
Expand Down Expand Up @@ -388,19 +385,16 @@ fn update(name: Option<String>, 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.
registry::install_plugin(
&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.",
Expand Down
4 changes: 4 additions & 0 deletions src/commands/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
6 changes: 0 additions & 6 deletions src/plugins/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
175 changes: 175 additions & 0 deletions src/utils/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,10 @@ pub fn install_template_package(
version,
cli_version_min,
cli_version_max,
None,
None,
None,
None,
)
}

Expand Down Expand Up @@ -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"
);
}
}