Finalize SourceForge provider install and update flows
This commit is contained in:
parent
eaa9a3b52d
commit
f260790d91
11 changed files with 718 additions and 65 deletions
|
|
@ -207,8 +207,8 @@ Current implementation status in this branch:
|
||||||
- Phase 1 is complete. The classifier-versus-adapter boundary is now documented explicitly in this addendum.
|
- Phase 1 is complete. The classifier-versus-adapter boundary is now documented explicitly in this addendum.
|
||||||
- Phase 2 is complete. `GitLabCandidate` and `SourceForgeCandidate` now exist in the source model and are produced by classification for the narrow ambiguity cases under test.
|
- Phase 2 is complete. `GitLabCandidate` and `SourceForgeCandidate` now exist in the source model and are produced by classification for the narrow ambiguity cases under test.
|
||||||
- Phase 3 is complete for the first GitLab slice. `https://gitlab.com/<group>/<subgroup>/releases/<repo>` remains a classified candidate, but the GitLab adapter now resolves it as repository semantics with a derived canonical locator.
|
- Phase 3 is complete for the first GitLab slice. `https://gitlab.com/<group>/<subgroup>/releases/<repo>` remains a classified candidate, but the GitLab adapter now resolves it as repository semantics with a derived canonical locator.
|
||||||
- Phase 4 is complete for two SourceForge slices. `https://sourceforge.net/projects/<project>/files/releases/stable/download` remains a classified candidate and now resolves as a provider-owned latest-download source. `https://sourceforge.net/projects/<project>/files/releases/v*/download` is now preserved as a provider-owned candidate and surfaces as `NoInstallableArtifact`.
|
- Phase 4 is complete for the current SourceForge slices. `https://sourceforge.net/projects/<project>/files/releases/stable/download` remains a classified candidate and resolves as a provider-owned install source. The broader single-segment family `https://sourceforge.net/projects/<project>/files/releases/<release-folder>/download` is also preserved as a provider-owned candidate and resolves through installation and update. When the `<release-folder>` segment is clearly an artifact filename, provider resolution canonicalizes the stored SourceForge source to `https://sourceforge.net/projects/<project>/files/releases` while preserving the original typed download URL as the first-install artifact.
|
||||||
- Phase 5 is partially complete. Provider-owned ambiguous inputs now distinguish unsupported-query failures from no-artifact outcomes, and both GitLab and SourceForge have at least one adapter-owned positive resolution path.
|
- Phase 5 is partially complete. Provider-owned ambiguous inputs now distinguish unsupported-query failures from no-artifact outcomes, and both GitLab and SourceForge have adapter-owned positive resolution paths for the accepted provider families.
|
||||||
|
|
||||||
The current intended classifier contract is:
|
The current intended classifier contract is:
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ That contract is intentionally stricter than heuristic best-effort classificatio
|
||||||
What remains intentionally out of scope for this slice:
|
What remains intentionally out of scope for this slice:
|
||||||
|
|
||||||
- additional GitLab candidate families beyond the first repository-style deep path
|
- additional GitLab candidate families beyond the first repository-style deep path
|
||||||
- broader SourceForge folder and version-path families beyond the `releases/stable/download` and narrow `releases/v*/download` rules
|
- broader SourceForge folder families beyond the current single-segment `releases/<release-folder>/download` support contract and the `files/releases` provider root
|
||||||
- any network-backed provider discovery in classification
|
- any network-backed provider discovery in classification
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,57 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() {
|
||||||
assert!(contents.contains("canonical_locator = \"team-app\""));
|
assert!(contents.contains("canonical_locator = \"team-app\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_add_installs_sourceforge_release_folder_with_truthful_origin() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let registry_path = dir.path().join("registry.toml");
|
||||||
|
let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download";
|
||||||
|
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||||
|
|
||||||
|
cmd.arg(query)
|
||||||
|
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||||
|
.env(FIXTURE_MODE_ENV, "1")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("Installed team-app (user)"))
|
||||||
|
.stdout(contains(format!("Source: sourceforge {query}")))
|
||||||
|
.stdout(contains(format!("Artifact: {query}")));
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||||
|
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||||
|
assert!(contents.contains("kind = \"SourceForge\""));
|
||||||
|
assert!(contents.contains(&format!("locator = \"{query}\"")));
|
||||||
|
assert!(contents.contains("canonical_locator = \"team-app\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_preserves_artifact() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let registry_path = dir.path().join("registry.toml");
|
||||||
|
let query =
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download";
|
||||||
|
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||||
|
|
||||||
|
cmd.arg(query)
|
||||||
|
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||||
|
.env(FIXTURE_MODE_ENV, "1")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("Installed team-app (user)"))
|
||||||
|
.stdout(contains(
|
||||||
|
"Source: sourceforge https://sourceforge.net/projects/team-app/files/releases",
|
||||||
|
))
|
||||||
|
.stdout(contains(format!("Artifact: {query}")));
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||||
|
assert!(contents.contains(&format!("source_input = \"{query}\"")));
|
||||||
|
assert!(contents.contains("kind = \"SourceForge\""));
|
||||||
|
assert!(
|
||||||
|
contents.contains("locator = \"https://sourceforge.net/projects/team-app/files/releases\"")
|
||||||
|
);
|
||||||
|
assert!(contents.contains("requested_asset_name = \"team-app-1.0.0.AppImage\""));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_reports_unsupported_source_queries_distinctly() {
|
fn cli_reports_unsupported_source_queries_distinctly() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,23 @@ pub struct SourceForgeAdapter;
|
||||||
|
|
||||||
impl SourceForgeAdapter {
|
impl SourceForgeAdapter {
|
||||||
pub fn artifact_url(source: &SourceRef) -> Option<String> {
|
pub fn artifact_url(source: &SourceRef) -> Option<String> {
|
||||||
if is_resolved_download_locator(&source.locator) {
|
if let Some(asset_name) = source.requested_asset_name.as_deref()
|
||||||
Some(source.locator.clone())
|
&& is_sourceforge_releases_root_locator(&source.locator)
|
||||||
} else {
|
{
|
||||||
None
|
return Some(format!("{}/{asset_name}/download", source.locator));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_latest_download_locator(&source.locator)
|
||||||
|
|| is_sourceforge_release_folder_download_locator(&source.locator)
|
||||||
|
{
|
||||||
|
return Some(source.locator.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_sourceforge_releases_root_locator(&source.locator) {
|
||||||
|
return sourceforge_latest_download_url(&source.locator);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +97,14 @@ impl SourceAdapter for SourceForgeAdapter {
|
||||||
|
|
||||||
fn resolved_source(source: &SourceRef) -> SourceRef {
|
fn resolved_source(source: &SourceRef) -> SourceRef {
|
||||||
let mut resolved = source.clone();
|
let mut resolved = source.clone();
|
||||||
if is_sourceforge_stable_download_locator(&resolved.locator) {
|
if is_sourceforge_file_like_release_download_locator(&resolved.locator) {
|
||||||
|
resolved.locator = sourceforge_releases_root_url(&resolved.locator)
|
||||||
|
.unwrap_or_else(|| resolved.locator.clone());
|
||||||
|
resolved.normalized_kind = NormalizedSourceKind::SourceForge;
|
||||||
|
resolved.tracks_latest = true;
|
||||||
|
} else if is_sourceforge_release_folder_download_locator(&resolved.locator)
|
||||||
|
|| is_sourceforge_releases_root_locator(&resolved.locator)
|
||||||
|
{
|
||||||
resolved.normalized_kind = NormalizedSourceKind::SourceForge;
|
resolved.normalized_kind = NormalizedSourceKind::SourceForge;
|
||||||
resolved.tracks_latest = true;
|
resolved.tracks_latest = true;
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +113,9 @@ fn resolved_source(source: &SourceRef) -> SourceRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_resolved_download_locator(locator: &str) -> bool {
|
fn is_resolved_download_locator(locator: &str) -> bool {
|
||||||
is_latest_download_locator(locator) || is_sourceforge_stable_download_locator(locator)
|
is_latest_download_locator(locator)
|
||||||
|
|| is_sourceforge_release_folder_download_locator(locator)
|
||||||
|
|| is_sourceforge_releases_root_locator(locator)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_latest_download_locator(locator: &str) -> bool {
|
fn is_latest_download_locator(locator: &str) -> bool {
|
||||||
|
|
@ -106,11 +127,140 @@ fn is_latest_download_locator(locator: &str) -> bool {
|
||||||
trimmed.ends_with("/files/latest/download")
|
trimmed.ends_with("/files/latest/download")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_sourceforge_stable_download_locator(locator: &str) -> bool {
|
fn is_sourceforge_release_folder_download_locator(locator: &str) -> bool {
|
||||||
let trimmed = locator
|
let trimmed = locator
|
||||||
.split(['?', '#'])
|
.split(['?', '#'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or(locator)
|
.unwrap_or(locator)
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
trimmed.ends_with("/files/releases/stable/download")
|
|
||||||
|
let parts = trimmed
|
||||||
|
.trim_start_matches("https://sourceforge.net/projects/")
|
||||||
|
.trim_start_matches("http://sourceforge.net/projects/")
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
parts.len() == 5 && parts[1] == "files" && parts[2] == "releases" && parts[4] == "download"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sourceforge_file_like_release_download_locator(locator: &str) -> bool {
|
||||||
|
let trimmed = locator
|
||||||
|
.split(['?', '#'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or(locator)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
|
let parts = trimmed
|
||||||
|
.trim_start_matches("https://sourceforge.net/projects/")
|
||||||
|
.trim_start_matches("http://sourceforge.net/projects/")
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
parts.len() == 5
|
||||||
|
&& parts[1] == "files"
|
||||||
|
&& parts[2] == "releases"
|
||||||
|
&& is_sourceforge_artifact_name(parts[3])
|
||||||
|
&& parts[4] == "download"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sourceforge_artifact_name(segment: &str) -> bool {
|
||||||
|
let lower = segment.to_ascii_lowercase();
|
||||||
|
|
||||||
|
[
|
||||||
|
".appimage",
|
||||||
|
".tar.gz",
|
||||||
|
".tar.xz",
|
||||||
|
".tar.bz2",
|
||||||
|
".zip",
|
||||||
|
".deb",
|
||||||
|
".rpm",
|
||||||
|
".exe",
|
||||||
|
".msi",
|
||||||
|
".dmg",
|
||||||
|
".pkg",
|
||||||
|
".apk",
|
||||||
|
".tgz",
|
||||||
|
".whl",
|
||||||
|
".jar",
|
||||||
|
".nupkg",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|suffix| lower.ends_with(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sourceforge_releases_root_locator(locator: &str) -> bool {
|
||||||
|
let trimmed = locator
|
||||||
|
.split(['?', '#'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or(locator)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
|
let parts = trimmed
|
||||||
|
.trim_start_matches("https://sourceforge.net/projects/")
|
||||||
|
.trim_start_matches("http://sourceforge.net/projects/")
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
parts.len() == 3 && parts[1] == "files" && parts[2] == "releases"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceforge_releases_root_url(locator: &str) -> Option<String> {
|
||||||
|
let trimmed = locator
|
||||||
|
.split(['?', '#'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or(locator)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
|
let prefix = if trimmed.starts_with("https://sourceforge.net/projects/") {
|
||||||
|
"https://sourceforge.net/projects/"
|
||||||
|
} else if trimmed.starts_with("http://sourceforge.net/projects/") {
|
||||||
|
"http://sourceforge.net/projects/"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = trimmed
|
||||||
|
.trim_start_matches("https://sourceforge.net/projects/")
|
||||||
|
.trim_start_matches("http://sourceforge.net/projects/")
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(format!("{}{}/files/releases", prefix, path[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceforge_latest_download_url(locator: &str) -> Option<String> {
|
||||||
|
let trimmed = locator
|
||||||
|
.split(['?', '#'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or(locator)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
|
let prefix = if trimmed.starts_with("https://sourceforge.net/projects/") {
|
||||||
|
"https://sourceforge.net/projects/"
|
||||||
|
} else if trimmed.starts_with("http://sourceforge.net/projects/") {
|
||||||
|
"http://sourceforge.net/projects/"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = trimmed
|
||||||
|
.trim_start_matches("https://sourceforge.net/projects/")
|
||||||
|
.trim_start_matches("http://sourceforge.net/projects/")
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(format!("{}{}/files/latest/download", prefix, path[0]))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ pub fn build_add_plan_with_reporter<T: GitHubTransport + ?Sized>(
|
||||||
let strategy = UpdateStrategy {
|
let strategy = UpdateStrategy {
|
||||||
preferred: crate::domain::update::ChannelPreference {
|
preferred: crate::domain::update::ChannelPreference {
|
||||||
kind: crate::domain::update::UpdateChannelKind::DirectAsset,
|
kind: crate::domain::update::UpdateChannelKind::DirectAsset,
|
||||||
locator: artifact_url,
|
locator: resolution.source.locator.clone(),
|
||||||
reason: "provider-release".to_owned(),
|
reason: "provider-release".to_owned(),
|
||||||
},
|
},
|
||||||
alternates: Vec::new(),
|
alternates: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,12 @@ fn execute_update(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_query(app: &AppRecord) -> Option<String> {
|
fn update_query(app: &AppRecord) -> Option<String> {
|
||||||
|
if let Some(source) = app.source.as_ref()
|
||||||
|
&& source.kind == SourceKind::SourceForge
|
||||||
|
{
|
||||||
|
return Some(source.locator.clone());
|
||||||
|
}
|
||||||
|
|
||||||
app.source_input.clone().or_else(|| {
|
app.source_input.clone().or_else(|| {
|
||||||
app.source.as_ref().map(|source| {
|
app.source.as_ref().map(|source| {
|
||||||
source
|
source
|
||||||
|
|
|
||||||
|
|
@ -171,19 +171,21 @@ fn classify_sourceforge_http(query: &str) -> Option<Result<ClassifiedInput, Clas
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_project_url = parts.len() == 1;
|
let is_project_url = parts.len() == 1;
|
||||||
|
let is_releases_root_url = parts.len() == 3 && parts[1] == "files" && parts[2] == "releases";
|
||||||
let is_latest_download_url =
|
let is_latest_download_url =
|
||||||
parts.len() == 4 && parts[1] == "files" && parts[2] == "latest" && parts[3] == "download";
|
parts.len() == 4 && parts[1] == "files" && parts[2] == "latest" && parts[3] == "download";
|
||||||
let is_root_file_download_url = parts.len() == 4
|
let is_root_file_download_url = parts.len() == 4
|
||||||
&& parts[1] == "files"
|
&& parts[1] == "files"
|
||||||
&& parts[3] == "download"
|
&& parts[3] == "download"
|
||||||
&& !matches!(parts[2], "latest" | "releases");
|
&& !matches!(parts[2], "latest" | "releases");
|
||||||
let is_nested_file_download_url = parts.len() > 4
|
let is_nested_file_download_url = parts.len() > 5
|
||||||
&& parts[1] == "files"
|
&& parts[1] == "files"
|
||||||
&& parts.last() == Some(&"download")
|
&& parts.last() == Some(&"download")
|
||||||
&& parts
|
&& parts
|
||||||
.get(parts.len().saturating_sub(2))
|
.get(parts.len().saturating_sub(2))
|
||||||
.is_some_and(|segment| segment.contains('.'));
|
.is_some_and(|segment| segment.contains('.'));
|
||||||
let is_ambiguous_candidate = is_ambiguous_sourceforge_candidate_path(&parts);
|
let is_ambiguous_candidate = is_ambiguous_sourceforge_candidate_path(&parts);
|
||||||
|
let requested_asset_name = sourceforge_requested_asset_name(&parts);
|
||||||
let is_concrete_download_url =
|
let is_concrete_download_url =
|
||||||
!is_latest_download_url && (is_root_file_download_url || is_nested_file_download_url);
|
!is_latest_download_url && (is_root_file_download_url || is_nested_file_download_url);
|
||||||
if is_concrete_download_url {
|
if is_concrete_download_url {
|
||||||
|
|
@ -198,7 +200,11 @@ fn classify_sourceforge_http(query: &str) -> Option<Result<ClassifiedInput, Clas
|
||||||
tracks_latest: false,
|
tracks_latest: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if !is_project_url && !is_latest_download_url && !is_ambiguous_candidate {
|
if !is_project_url
|
||||||
|
&& !is_releases_root_url
|
||||||
|
&& !is_latest_download_url
|
||||||
|
&& !is_ambiguous_candidate
|
||||||
|
{
|
||||||
return Some(Err(ClassifyInputError::Unsupported));
|
return Some(Err(ClassifyInputError::Unsupported));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,8 +219,8 @@ fn classify_sourceforge_http(query: &str) -> Option<Result<ClassifiedInput, Clas
|
||||||
locator: query.to_owned(),
|
locator: query.to_owned(),
|
||||||
canonical_locator: Some((*project).to_owned()),
|
canonical_locator: Some((*project).to_owned()),
|
||||||
requested_tag: None,
|
requested_tag: None,
|
||||||
requested_asset_name: None,
|
requested_asset_name,
|
||||||
tracks_latest: is_project_url || is_latest_download_url,
|
tracks_latest: is_project_url || is_releases_root_url || is_latest_download_url,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,15 +277,45 @@ fn is_ambiguous_gitlab_candidate_path(parts: &[&str]) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_ambiguous_sourceforge_candidate_path(parts: &[&str]) -> bool {
|
fn is_ambiguous_sourceforge_candidate_path(parts: &[&str]) -> bool {
|
||||||
parts.len() == 5
|
parts.len() == 5 && parts[1] == "files" && parts[2] == "releases" && parts[4] == "download"
|
||||||
&& parts[1] == "files"
|
|
||||||
&& parts[2] == "releases"
|
|
||||||
&& (parts[3] == "stable" || is_version_like_sourceforge_folder(parts[3]))
|
|
||||||
&& parts[4] == "download"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_version_like_sourceforge_folder(segment: &str) -> bool {
|
fn sourceforge_requested_asset_name(parts: &[&str]) -> Option<String> {
|
||||||
segment.starts_with('v') && segment.chars().any(|character| character.is_ascii_digit())
|
if parts.len() == 5
|
||||||
|
&& parts[1] == "files"
|
||||||
|
&& parts[2] == "releases"
|
||||||
|
&& parts[4] == "download"
|
||||||
|
&& is_sourceforge_artifact_name(parts[3])
|
||||||
|
{
|
||||||
|
return Some(parts[3].to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sourceforge_artifact_name(segment: &str) -> bool {
|
||||||
|
let lower = segment.to_ascii_lowercase();
|
||||||
|
|
||||||
|
[
|
||||||
|
".appimage",
|
||||||
|
".tar.gz",
|
||||||
|
".tar.xz",
|
||||||
|
".tar.bz2",
|
||||||
|
".zip",
|
||||||
|
".deb",
|
||||||
|
".rpm",
|
||||||
|
".exe",
|
||||||
|
".msi",
|
||||||
|
".dmg",
|
||||||
|
".pkg",
|
||||||
|
".apk",
|
||||||
|
".tgz",
|
||||||
|
".whl",
|
||||||
|
".jar",
|
||||||
|
".nupkg",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|suffix| lower.ends_with(suffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_github_http(query: &str) -> Option<ClassifiedInput> {
|
fn classify_github_http(query: &str) -> Option<ClassifiedInput> {
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ fn sourceforge_candidate_sources_can_resolve_to_latest_download() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sourceforge_version_folder_candidates_can_return_no_installable_artifact() {
|
fn sourceforge_version_folder_candidates_can_resolve_to_latest_download() {
|
||||||
let adapter: &dyn SourceAdapter = &SourceForgeAdapter;
|
let adapter: &dyn SourceAdapter = &SourceForgeAdapter;
|
||||||
|
|
||||||
let result = adapter
|
let result = adapter
|
||||||
|
|
@ -247,10 +247,112 @@ fn sourceforge_version_folder_candidates_can_return_no_installable_artifact() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolution = adapter.resolve_source(&result).unwrap();
|
let resolution = adapter.resolve_source(&result).unwrap();
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
resolution,
|
resolution,
|
||||||
AdapterResolveOutcome::NoInstallableArtifact { source: result }
|
AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||||
|
source,
|
||||||
|
release: ResolvedRelease { version, .. },
|
||||||
|
}) if source.kind == SourceKind::SourceForge
|
||||||
|
&& source.locator
|
||||||
|
== "https://sourceforge.net/projects/team-app/files/releases/v1-0/download"
|
||||||
|
&& source.normalized_kind == NormalizedSourceKind::SourceForge
|
||||||
|
&& source.tracks_latest
|
||||||
|
&& version == "latest"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_prerelease_folder_candidates_can_resolve_to_latest_download() {
|
||||||
|
let adapter: &dyn SourceAdapter = &SourceForgeAdapter;
|
||||||
|
|
||||||
|
let result = adapter
|
||||||
|
.normalize("https://sourceforge.net/projects/team-app/files/releases/beta/download")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
result.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let resolution = adapter.resolve_source(&result).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
resolution,
|
||||||
|
AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||||
|
source,
|
||||||
|
release: ResolvedRelease { version, .. },
|
||||||
|
}) if source.kind == SourceKind::SourceForge
|
||||||
|
&& source.locator
|
||||||
|
== "https://sourceforge.net/projects/team-app/files/releases/beta/download"
|
||||||
|
&& source.normalized_kind == NormalizedSourceKind::SourceForge
|
||||||
|
&& source.tracks_latest
|
||||||
|
&& version == "latest"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_dotted_release_folder_candidates_can_resolve_to_latest_download() {
|
||||||
|
let adapter: &dyn SourceAdapter = &SourceForgeAdapter;
|
||||||
|
|
||||||
|
let result = adapter
|
||||||
|
.normalize("https://sourceforge.net/projects/team-app/files/releases/2026.03/download")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
result.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolution = adapter.resolve_source(&result).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
resolution,
|
||||||
|
AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||||
|
source,
|
||||||
|
release: ResolvedRelease { version, .. },
|
||||||
|
}) if source.kind == SourceKind::SourceForge
|
||||||
|
&& source.locator
|
||||||
|
== "https://sourceforge.net/projects/team-app/files/releases/2026.03/download"
|
||||||
|
&& source.normalized_kind == NormalizedSourceKind::SourceForge
|
||||||
|
&& source.tracks_latest
|
||||||
|
&& version == "latest"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_file_like_release_candidates_resolve_to_releases_root() {
|
||||||
|
let adapter: &dyn SourceAdapter = &SourceForgeAdapter;
|
||||||
|
|
||||||
|
let result = adapter
|
||||||
|
.normalize(
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
result.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.requested_asset_name.as_deref(),
|
||||||
|
Some("team-app-1.0.0.AppImage")
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolution = adapter.resolve_source(&result).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
resolution,
|
||||||
|
AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||||
|
source,
|
||||||
|
release: ResolvedRelease { version, .. },
|
||||||
|
}) if source.kind == SourceKind::SourceForge
|
||||||
|
&& source.locator
|
||||||
|
== "https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
&& source.normalized_kind == NormalizedSourceKind::SourceForge
|
||||||
|
&& source.tracks_latest
|
||||||
|
&& source.requested_asset_name.as_deref() == Some("team-app-1.0.0.AppImage")
|
||||||
|
&& version == "latest"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -67,26 +67,3 @@ fn supported_sourceforge_project_without_latest_download_reports_no_installable_
|
||||||
other => panic!("expected no-installable-artifact error, got {other:?}"),
|
other => panic!("expected no-installable-artifact error, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn supported_sourceforge_version_folder_candidate_without_installable_artifact_reports_no_installable_artifact()
|
|
||||||
{
|
|
||||||
let error = build_add_plan_with(
|
|
||||||
"https://sourceforge.net/projects/team-app/files/releases/v1-0/download",
|
|
||||||
&FixtureGitHubTransport,
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
match error {
|
|
||||||
BuildAddPlanError::NoInstallableArtifact { source } => {
|
|
||||||
assert_eq!(source.kind, SourceKind::SourceForge);
|
|
||||||
assert_eq!(
|
|
||||||
source.locator,
|
|
||||||
"https://sourceforge.net/projects/team-app/files/releases/v1-0/download"
|
|
||||||
);
|
|
||||||
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
|
||||||
assert_eq!(source.normalized_kind.as_str(), "sourceforge-candidate");
|
|
||||||
}
|
|
||||||
other => panic!("expected no-installable-artifact error, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,28 @@ fn sourceforge_candidate_builds_concrete_install_candidate() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_release_folder_builds_concrete_install_candidate() {
|
||||||
|
let mut reporter = |_event: &OperationEvent| {};
|
||||||
|
let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download";
|
||||||
|
|
||||||
|
let plan = build_add_plan_with_reporter(query, &FixtureGitHubTransport, &mut reporter).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(plan.resolution.source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(plan.resolution.source.locator, query);
|
||||||
|
assert_eq!(
|
||||||
|
plan.resolution.source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForge
|
||||||
|
);
|
||||||
|
assert!(plan.resolution.source.tracks_latest);
|
||||||
|
assert_eq!(plan.resolution.release.version, "latest");
|
||||||
|
assert_eq!(plan.selected_artifact.url, query);
|
||||||
|
assert_eq!(plan.selected_artifact.version, "latest");
|
||||||
|
assert_eq!(plan.selected_artifact.selection_reason, "provider-release");
|
||||||
|
assert_eq!(plan.update_strategy.preferred.locator, query);
|
||||||
|
assert_eq!(plan.update_strategy.preferred.reason, "provider-release");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sourceforge_latest_download_builds_concrete_install_candidate() {
|
fn sourceforge_latest_download_builds_concrete_install_candidate() {
|
||||||
let mut reporter = |_event: &OperationEvent| {};
|
let mut reporter = |_event: &OperationEvent| {};
|
||||||
|
|
@ -431,3 +453,104 @@ fn sourceforge_latest_download_install_preserves_truthful_origin() {
|
||||||
);
|
);
|
||||||
assert_eq!(installed.selected_artifact.url, query);
|
assert_eq!(installed.selected_artifact.url, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_release_folder_install_preserves_truthful_origin() {
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reporter = |_event: &OperationEvent| {};
|
||||||
|
let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download";
|
||||||
|
let plan = build_add_plan_with_reporter(query, &FixtureGitHubTransport, &mut reporter).unwrap();
|
||||||
|
|
||||||
|
let installed =
|
||||||
|
install_app_with_reporter(query, &plan, root.path(), InstallScope::User, &mut reporter)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(installed.record.source_input.as_deref(), Some(query));
|
||||||
|
assert_eq!(
|
||||||
|
installed.record.installed_version.as_deref(),
|
||||||
|
Some("latest")
|
||||||
|
);
|
||||||
|
assert_eq!(installed.source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(installed.source.locator, query);
|
||||||
|
assert_eq!(
|
||||||
|
installed.source.canonical_locator.as_deref(),
|
||||||
|
Some("team-app")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
installed.source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForge
|
||||||
|
);
|
||||||
|
assert_eq!(installed.selected_artifact.url, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_file_like_release_download_uses_releases_root_for_source_and_original_url_for_artifact()
|
||||||
|
{
|
||||||
|
let mut reporter = |_event: &OperationEvent| {};
|
||||||
|
let query =
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download";
|
||||||
|
|
||||||
|
let plan = build_add_plan_with_reporter(query, &FixtureGitHubTransport, &mut reporter).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(plan.resolution.source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
plan.resolution.source.locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plan.resolution.source.requested_asset_name.as_deref(),
|
||||||
|
Some("team-app-1.0.0.AppImage")
|
||||||
|
);
|
||||||
|
assert_eq!(plan.resolution.release.version, "latest");
|
||||||
|
assert_eq!(plan.selected_artifact.url, query);
|
||||||
|
assert_eq!(plan.selected_artifact.version, "latest");
|
||||||
|
assert_eq!(plan.selected_artifact.selection_reason, "provider-release");
|
||||||
|
assert_eq!(
|
||||||
|
plan.update_strategy.preferred.locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(plan.update_strategy.preferred.reason, "provider-release");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_file_like_release_download_install_preserves_input_but_stores_releases_root() {
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reporter = |_event: &OperationEvent| {};
|
||||||
|
let query =
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download";
|
||||||
|
let plan = build_add_plan_with_reporter(query, &FixtureGitHubTransport, &mut reporter).unwrap();
|
||||||
|
|
||||||
|
let installed =
|
||||||
|
install_app_with_reporter(query, &plan, root.path(), InstallScope::User, &mut reporter)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(installed.record.source_input.as_deref(), Some(query));
|
||||||
|
assert_eq!(
|
||||||
|
installed.record.installed_version.as_deref(),
|
||||||
|
Some("latest")
|
||||||
|
);
|
||||||
|
assert_eq!(installed.source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
installed.source.locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
installed.source.requested_asset_name.as_deref(),
|
||||||
|
Some("team-app-1.0.0.AppImage")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
installed.source.canonical_locator.as_deref(),
|
||||||
|
Some("team-app")
|
||||||
|
);
|
||||||
|
assert_eq!(installed.selected_artifact.url, query);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,15 +128,39 @@ fn preserves_direct_url_classification() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_sourceforge_download_url_as_direct_url() {
|
fn classifies_single_segment_sourceforge_release_download_as_candidate() {
|
||||||
let source = resolve_query(
|
let source = resolve_query(
|
||||||
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download",
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(source.kind, SourceKind::DirectUrl);
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
assert_eq!(source.input_kind, SourceInputKind::DirectUrl);
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
assert_eq!(source.normalized_kind, NormalizedSourceKind::DirectUrl);
|
assert_eq!(
|
||||||
|
source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert_eq!(
|
||||||
|
source.requested_asset_name.as_deref(),
|
||||||
|
Some("team-app-1.0.0.AppImage")
|
||||||
|
);
|
||||||
|
assert!(!source.tracks_latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_sourceforge_releases_root_as_provider_source() {
|
||||||
|
let source = resolve_query("https://sourceforge.net/projects/team-app/files/releases").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
|
assert_eq!(source.normalized_kind, NormalizedSourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
source.locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert!(source.tracks_latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -162,15 +186,24 @@ fn preserves_sourceforge_extensionless_root_download_url_as_direct_url() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_sourceforge_download_url_with_query_as_direct_url() {
|
fn classifies_single_segment_sourceforge_release_download_with_query_as_candidate() {
|
||||||
let source = resolve_query(
|
let source = resolve_query(
|
||||||
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download?use_mirror=pilotfiber",
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download?use_mirror=pilotfiber",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(source.kind, SourceKind::DirectUrl);
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
assert_eq!(source.input_kind, SourceInputKind::DirectUrl);
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
assert_eq!(source.normalized_kind, NormalizedSourceKind::DirectUrl);
|
assert_eq!(
|
||||||
|
source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert_eq!(
|
||||||
|
source.requested_asset_name.as_deref(),
|
||||||
|
Some("team-app-1.0.0.AppImage")
|
||||||
|
);
|
||||||
|
assert!(!source.tracks_latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -253,11 +286,18 @@ fn rejects_unsupported_sourceforge_url_shape() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_unsupported_sourceforge_files_shape() {
|
fn classifies_sourceforge_files_releases_shape_as_provider_source() {
|
||||||
let error =
|
let source = resolve_query("https://sourceforge.net/projects/team-app/files/releases").unwrap();
|
||||||
resolve_query("https://sourceforge.net/projects/team-app/files/releases").unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
|
assert_eq!(source.normalized_kind, NormalizedSourceKind::SourceForge);
|
||||||
|
assert_eq!(
|
||||||
|
source.locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert!(source.tracks_latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -285,12 +325,19 @@ fn classifies_ambiguous_sourceforge_nested_folder_download_as_candidate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_unsupported_sourceforge_nested_extensionless_download_shape() {
|
fn classifies_extensionless_sourceforge_release_folder_download_as_candidate() {
|
||||||
let error =
|
let source =
|
||||||
resolve_query("https://sourceforge.net/projects/team-app/files/releases/team-app/download")
|
resolve_query("https://sourceforge.net/projects/team-app/files/releases/team-app/download")
|
||||||
.unwrap_err();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
|
assert_eq!(
|
||||||
|
source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert!(!source.tracks_latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -308,3 +355,35 @@ fn classifies_ambiguous_sourceforge_version_folder_download_as_candidate() {
|
||||||
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
assert!(!source.tracks_latest);
|
assert!(!source.tracks_latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_prerelease_named_sourceforge_release_folder_download_as_candidate() {
|
||||||
|
let source =
|
||||||
|
resolve_query("https://sourceforge.net/projects/team-app/files/releases/beta/download")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
|
assert_eq!(
|
||||||
|
source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert!(!source.tracks_latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_dotted_sourceforge_release_folder_download_as_candidate() {
|
||||||
|
let source =
|
||||||
|
resolve_query("https://sourceforge.net/projects/team-app/files/releases/2026.03/download")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source.kind, SourceKind::SourceForge);
|
||||||
|
assert_eq!(source.input_kind, SourceInputKind::SourceForgeUrl);
|
||||||
|
assert_eq!(
|
||||||
|
source.normalized_kind,
|
||||||
|
NormalizedSourceKind::SourceForgeCandidate
|
||||||
|
);
|
||||||
|
assert_eq!(source.canonical_locator.as_deref(), Some("team-app"));
|
||||||
|
assert!(!source.tracks_latest);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,3 +238,132 @@ fn update_execution_rebuilds_gitlab_source_without_rewriting_origin() {
|
||||||
Some("example/team-app")
|
Some("example/team-app")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_execution_rebuilds_sourceforge_release_folder_without_rewriting_origin() {
|
||||||
|
let install_home = tempdir().unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous = AppRecord {
|
||||||
|
stable_id: "team-app".to_owned(),
|
||||||
|
display_name: "team-app".to_owned(),
|
||||||
|
source_input: Some(
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/beta/download".to_owned(),
|
||||||
|
),
|
||||||
|
source: Some(SourceRef {
|
||||||
|
kind: SourceKind::SourceForge,
|
||||||
|
locator: "https://sourceforge.net/projects/team-app/files/releases/beta/download"
|
||||||
|
.to_owned(),
|
||||||
|
input_kind: SourceInputKind::SourceForgeUrl,
|
||||||
|
normalized_kind: NormalizedSourceKind::SourceForge,
|
||||||
|
canonical_locator: Some("team-app".to_owned()),
|
||||||
|
requested_tag: None,
|
||||||
|
requested_asset_name: None,
|
||||||
|
tracks_latest: true,
|
||||||
|
}),
|
||||||
|
installed_version: Some("latest".to_owned()),
|
||||||
|
update_strategy: Some(UpdateStrategy {
|
||||||
|
preferred: ChannelPreference {
|
||||||
|
kind: UpdateChannelKind::DirectAsset,
|
||||||
|
locator: "https://sourceforge.net/projects/team-app/files/releases/beta/download"
|
||||||
|
.to_owned(),
|
||||||
|
reason: "provider-release".to_owned(),
|
||||||
|
},
|
||||||
|
alternates: Vec::new(),
|
||||||
|
}),
|
||||||
|
metadata: Vec::new(),
|
||||||
|
install: Some(InstallMetadata {
|
||||||
|
scope: InstallScope::User,
|
||||||
|
payload_path: None,
|
||||||
|
desktop_entry_path: None,
|
||||||
|
icon_path: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = execute_updates(std::slice::from_ref(&previous), install_home.path()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.updated_count(), 1);
|
||||||
|
assert_eq!(result.failed_count(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
result.apps[0].source.as_ref().unwrap().kind,
|
||||||
|
SourceKind::SourceForge
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.apps[0].source.as_ref().unwrap().locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/beta/download"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.apps[0]
|
||||||
|
.source
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.canonical_locator
|
||||||
|
.as_deref(),
|
||||||
|
Some("team-app")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_execution_uses_stored_sourceforge_releases_root_for_file_like_inputs() {
|
||||||
|
let install_home = tempdir().unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous = AppRecord {
|
||||||
|
stable_id: "team-app".to_owned(),
|
||||||
|
display_name: "team-app".to_owned(),
|
||||||
|
source_input: Some(
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download"
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
source: Some(SourceRef {
|
||||||
|
kind: SourceKind::SourceForge,
|
||||||
|
locator: "https://sourceforge.net/projects/team-app/files/releases".to_owned(),
|
||||||
|
input_kind: SourceInputKind::SourceForgeUrl,
|
||||||
|
normalized_kind: NormalizedSourceKind::SourceForge,
|
||||||
|
canonical_locator: Some("team-app".to_owned()),
|
||||||
|
requested_tag: None,
|
||||||
|
requested_asset_name: Some("team-app-1.0.0.AppImage".to_owned()),
|
||||||
|
tracks_latest: true,
|
||||||
|
}),
|
||||||
|
installed_version: Some("latest".to_owned()),
|
||||||
|
update_strategy: Some(UpdateStrategy {
|
||||||
|
preferred: ChannelPreference {
|
||||||
|
kind: UpdateChannelKind::DirectAsset,
|
||||||
|
locator: "https://sourceforge.net/projects/team-app/files/releases".to_owned(),
|
||||||
|
reason: "provider-release".to_owned(),
|
||||||
|
},
|
||||||
|
alternates: Vec::new(),
|
||||||
|
}),
|
||||||
|
metadata: Vec::new(),
|
||||||
|
install: Some(InstallMetadata {
|
||||||
|
scope: InstallScope::User,
|
||||||
|
payload_path: None,
|
||||||
|
desktop_entry_path: None,
|
||||||
|
icon_path: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = execute_updates(std::slice::from_ref(&previous), install_home.path()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.updated_count(), 1);
|
||||||
|
assert_eq!(result.failed_count(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
result.apps[0].source.as_ref().unwrap().locator,
|
||||||
|
"https://sourceforge.net/projects/team-app/files/releases"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.apps[0]
|
||||||
|
.source
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.requested_asset_name
|
||||||
|
.as_deref(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue