feat(mods): enhance mod management with profile controls and dependency checks

- Added dedicated profile control in the MODS panel for easier profile management.
- Implemented dependency checking during mod installation and updates to ensure compatibility.
- Improved manifest parsing to support GitHub repository hints for dependencies.
- Enhanced UI interactions for saving and renaming profiles.
- Updated tests to cover new functionality and ensure stability.
This commit is contained in:
1jamie
2026-08-13 17:33:29 -05:00
parent 26e9e1d597
commit 500d8c2c07
7 changed files with 1218 additions and 85 deletions
+39 -3
View File
@@ -138,7 +138,25 @@ check(not pcall(Manifest.parseGithub, "not a repo"),
check(not pcall(Manifest.validate, {
id = "badgh", name = "Bad", version = "1.0.0", entry = "main.lua",
github = "ftp://example.com/x",
}), "a bad github field fails manifest validation")
}), "an unsupported github URL fails validation")
-- ------- dependency github repo spec hints & dependency resolver
local depGh = Manifest.validate({
id = "depgh", name = "DepGH", version = "1.0.0", entry = "main.lua",
dependencies = { "colorlib@^1.2.0#Acme/ColorLib", "soundpack#Acme/SoundPack" },
dependency_sources = { helper = "Acme/Helper" },
})
check(depGh.dependencySpecs[1].id == "colorlib" and depGh.dependencySpecs[1].github == "Acme/ColorLib",
"dependency spec hash hint parses github owner/repo")
check(depGh.dependencySpecs[2].id == "soundpack" and depGh.dependencySpecs[2].github == "Acme/SoundPack",
"dependency spec hash hint without range parses github owner/repo")
local LauncherMods = require("src.mods.LauncherMods")
local depCheck = LauncherMods.checkDependencies(depGh)
check(depCheck.hasIssues == true, "missing dependencies trigger issues verdict")
check(#depCheck.deps == 2, "dependency check lists all specs")
check(depCheck.deps[1].status == "missing", "absent dependency reports missing")
check(depCheck.deps[1].safeUrl == "https://github.com/Acme/ColorLib", "safeUrl built from validated github repo")
local v1 = Manifest.validate({
id = "v1", name = "V1", version = "1.0.0", entry = "main.lua",
@@ -519,8 +537,26 @@ local emptyLoader = Loader.new({ fs = memfs({}) })
check(emptyLoader:load(pristine) == true, "an empty mods dir still loads clean")
check(#emptyLoader:status().errors == 0, "no mods means no diagnostics")
check(#emptyLoader.order == 0, "no mods means an empty load order")
check(pristine.pokemon.A.hp == 1 and next(pristine.items) == nil,
"no-mod load leaves data untouched")
-- ------- dependency resolver conflict detection test
local LauncherMods = require("src.mods.LauncherMods")
local testTargetManifest = Manifest.validate({
id = "new_mod",
name = "New Mod",
version = "1.0.0",
entry = "main.lua",
incompatible = { "colorlib" },
}, "mods/new_mod")
local installedColorlib = Manifest.validate({
id = "colorlib",
name = "Color Lib",
version = "1.0.0",
entry = "main.lua",
}, "mods/colorlib")
local depConflictCheck = LauncherMods.checkDependencies(testTargetManifest, nil, nil, { installedColorlib })
check(depConflictCheck.hasIssues == true, "conflict with colorlib triggers hasIssues")
check(#depConflictCheck.deps == 1 and depConflictCheck.deps[1].status == "conflict",
"incompatible mod is flagged as status conflict")
check(depConflictCheck.deps[1].kind == "conflict", "conflict item carries kind conflict")
Runtime.install(savedEvents, savedHooks)
+12
View File
@@ -1162,6 +1162,18 @@ seedOpts.modProfiles = {}
ModProfile.ensureFirst(seedOpts, ms.status.available, {})
check(#seedOpts.modProfiles == 0, "seeding never runs twice")
local LauncherMods = require("src.mods.LauncherMods")
local testProfOpts = { activeProfile = "P1", modProfiles = { { name = "P1", enabled = { a = true } } } }
local dupSnap = LauncherMods.duplicateProfile("P1", testProfOpts)
check(dupSnap and dupSnap.name == "P1 (Copy)" and testProfOpts.activeProfile == "P1 (Copy)",
"duplicateProfile creates P1 (Copy) and activates it")
check(LauncherMods.renameProfile("P1 (Copy)", "RenamedP", testProfOpts) == true,
"renameProfile renames active profile")
check(testProfOpts.activeProfile == "RenamedP", "activeProfile updates on rename")
check(LauncherMods.deleteProfile("RenamedP", testProfOpts) == true, "deleteProfile removes profile")
check(#testProfOpts.modProfiles == 1 and testProfOpts.modProfiles[1].name == "P1", "only original profile remains")
check(testProfOpts.activeProfile == "P1", "activeProfile falls back to remaining profile")
-- permissions rows
local permy = manifest("permy", { permissions = { "network" } })
local msP = ManagerState.new(managerGame(fakeLoader({ permy })))