fix: 修复自选目录实例的模组同步路径
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled

This commit is contained in:
Disy committed 2026-09-26 11:00:01 +08:00
1 parent c273734aec
commit 2359b80250
1 file changed
+101 -8
@@ -87,11 +87,6 @@ pub(crate) async fn sync_instance_content_files(
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.is_some_and(|linked| !linked.is_empty()); .is_some_and(|linked| !linked.is_empty());
let instance_files_root = if is_direct_linked {
content_root.clone()
} else {
state.directories.instances_dir().join(&instance.path)
};
let cache_key_path = if is_direct_linked { let cache_key_path = if is_direct_linked {
content_root.to_string_lossy().into_owned() content_root.to_string_lossy().into_owned()
} else { } else {
@@ -168,8 +163,9 @@ pub(crate) async fn sync_instance_content_files(
let hash_key = file.hash_cache_key.trim_end_matches(".disabled"); let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let existing_file = existing_files_by_path.get(&file.relative_path); let existing_file = existing_files_by_path.get(&file.relative_path);
let (scanned_sha1, scanned_size) = if existing_file.is_some() { let (scanned_sha1, scanned_size) = if existing_file.is_some() {
let path = // Read from the resolved game directory. Managed StarLight
join_content_path(&instance_files_root, &file.relative_path); // instances may keep their files outside the launcher data root.
let path = join_content_path(&content_root, &file.relative_path);
let (_, sha1) = fetch::sha1_file_async(&path).await?; let (_, sha1) = fetch::sha1_file_async(&path).await?;
(sha1, file.size) (sha1, file.size)
} else { } else {
@@ -748,7 +744,9 @@ mod tests {
base_path: minecraft.path().to_path_buf(), base_path: minecraft.path().to_path_buf(),
instance_folder: "versions/1.12.2-linked".to_string(), instance_folder: "versions/1.12.2-linked".to_string(),
instance_path: None, instance_path: None,
game_dir_mode: None, game_dir_mode: Some(
crate::launcher::ExternalGameDirMode::Shared,
),
}, },
&state, &state,
) )
@@ -875,6 +873,59 @@ mod tests {
); );
} }
#[tokio::test]
async fn refresh_reconciles_externally_replaced_profile_mods() {
let state = global_state().await;
let metadata = crate::api::instance::create(
format!("sync-external {}", uuid::Uuid::new_v4()),
"1.20.1".to_string(),
crate::state::ModLoader::Vanilla,
None,
None,
crate::state::InstanceLink::Unmanaged,
None,
None,
)
.await
.unwrap();
let instance = &metadata.instance;
let mods = state.directories.instance_game_dir(instance).join("mods");
fs::create_dir_all(&mods).unwrap();
fs::write(mods.join("old-version.jar"), "old").unwrap();
fs::write(mods.join("same-name.jar"), "old").unwrap();
let initial =
sync_instance_content_files(instance, &state).await.unwrap();
assert_eq!(initial.len(), 2);
fs::remove_file(mods.join("old-version.jar")).unwrap();
fs::write(mods.join("new-version.jar"), "new version").unwrap();
fs::write(mods.join("same-name.jar"), "new").unwrap();
let refreshed =
sync_instance_content_files(instance, &state).await.unwrap();
assert_eq!(refreshed.len(), 2);
assert!(
refreshed
.iter()
.any(|file| file.relative_path == "mods/new-version.jar")
);
assert!(
!refreshed
.iter()
.any(|file| file.relative_path == "mods/old-version.jar")
);
let before = initial
.iter()
.find(|file| file.relative_path == "mods/same-name.jar")
.unwrap();
let after = refreshed
.iter()
.find(|file| file.relative_path == "mods/same-name.jar")
.unwrap();
assert_eq!(before.id, after.id);
assert_ne!(before.sha1, after.sha1);
assert_eq!(before.size, after.size);
}
/// Regression probe: an instance whose `game_dir_override` points to an /// Regression probe: an instance whose `game_dir_override` points to an
/// external `.minecraft` root must have its content scanned from that root, /// external `.minecraft` root must have its content scanned from that root,
/// not from the (empty) managed instance folder. This is the split-brain /// not from the (empty) managed instance folder. This is the split-brain
@@ -947,6 +998,48 @@ mod tests {
"expected the override-root mod to be scanned" "expected the override-root mod to be scanned"
); );
assert!(files[0].relative_path.ends_with("mods/my-mod.jar")); assert!(files[0].relative_path.ends_with("mods/my-mod.jar"));
let refreshed =
sync_instance_content_files(&instance, &state).await.expect(
"repeat refresh must hash files in the actual game directory",
);
assert_eq!(refreshed[0].sha1, files[0].sha1);
fs::write(mods_dir.join("my-mod.jar"), "new").unwrap();
fs::write(mods_dir.join("added.jar"), "added mod").unwrap();
let replaced = sync_instance_content_files(&instance, &state)
.await
.unwrap();
assert_eq!(replaced.len(), 2);
let updated = replaced
.iter()
.find(|file| file.relative_path == "mods/my-mod.jar")
.unwrap();
assert_eq!(updated.id, files[0].id);
assert_ne!(updated.sha1, files[0].sha1);
assert_eq!(updated.size, files[0].size);
fs::remove_file(mods_dir.join("my-mod.jar")).unwrap();
fs::rename(
mods_dir.join("added.jar"),
mods_dir.join("added.jar.disabled"),
)
.unwrap();
let removed = sync_instance_content_files(&instance, &state)
.await
.unwrap();
assert_eq!(removed.len(), 1);
assert_eq!(removed[0].relative_path, "mods/added.jar.disabled");
assert!(!removed[0].enabled);
let stored =
sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
.await
.unwrap();
assert!(
stored
.iter()
.any(|file| file.id == files[0].id && file.missing)
);
} }
#[test] #[test]