mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
test(nx): move ROM-free NX suites into the engine tier
platform_nx_* and rom_importer_nx_* run with the love stub and must execute in CI's ROM-free lane via tests/run_engine.lua, not only T3. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
-- NX mods zip inbox: ensure imports/mods/, MTP hint (NXMOD-01..05).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local S = require("tests.harness").suite("rom importer NX mods inbox")
|
||||
local eq = S.eq
|
||||
local check = S.check
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
|
||||
love.system = love.system or {}
|
||||
love.filesystem = love.filesystem or {}
|
||||
|
||||
local saved = {
|
||||
getOS = love.system.getOS,
|
||||
getSaveDirectory = love.filesystem.getSaveDirectory,
|
||||
createDirectory = love.filesystem.createDirectory,
|
||||
remove = love.filesystem.remove,
|
||||
}
|
||||
|
||||
love.system.getOS = function() return "NX" end
|
||||
love.filesystem.getSaveDirectory = function()
|
||||
return "sdmc:/switch/gen1recomp/pokemon-love2d"
|
||||
end
|
||||
|
||||
local createdDirs = {}
|
||||
love.filesystem.createDirectory = function(name)
|
||||
createdDirs[name] = true
|
||||
return true
|
||||
end
|
||||
|
||||
local removed = {}
|
||||
love.filesystem.remove = function(name)
|
||||
removed[name] = true
|
||||
return saved.remove(name)
|
||||
end
|
||||
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
package.loaded["src.import.RomImporter"] = nil
|
||||
RomImporter = require("src.import.RomImporter")
|
||||
|
||||
local function clearModsInbox()
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems("imports/mods") or {}) do
|
||||
love.filesystem.remove("imports/mods/" .. name)
|
||||
end
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems("imports") or {}) do
|
||||
love.filesystem.remove("imports/" .. name)
|
||||
end
|
||||
end
|
||||
|
||||
local function freshImporter()
|
||||
clearModsInbox()
|
||||
createdDirs = {}
|
||||
removed = {}
|
||||
package.loaded["src.import.RomImporter"] = nil
|
||||
RomImporter = require("src.import.RomImporter")
|
||||
return setmetatable({
|
||||
isNX = true,
|
||||
android = false,
|
||||
launcher = true,
|
||||
workState = nil,
|
||||
tab = "mods",
|
||||
modNotice = nil,
|
||||
mods = {},
|
||||
ready = { red = false, blue = false, yellow = false },
|
||||
ensureImportsDir = RomImporter.ensureImportsDir,
|
||||
ensureModsInboxDir = RomImporter.ensureModsInboxDir,
|
||||
_setNxModsInboxNotice = RomImporter._setNxModsInboxNotice,
|
||||
scanModsInbox = RomImporter.scanModsInbox,
|
||||
scanInbox = RomImporter.scanInbox,
|
||||
rescanModsAction = RomImporter.rescanModsAction,
|
||||
chooseMod = RomImporter.chooseMod,
|
||||
_installMod = RomImporter._installMod,
|
||||
_refreshMods = function(self)
|
||||
self._refreshed = (self._refreshed or 0) + 1
|
||||
self.mods = self.mods or {}
|
||||
end,
|
||||
}, RomImporter)
|
||||
end
|
||||
|
||||
-- NXMOD-01: ensureModsInboxDir creates imports/mods/ under save FS
|
||||
createdDirs = {}
|
||||
local ri = freshImporter()
|
||||
ri:ensureModsInboxDir()
|
||||
check(createdDirs.imports or createdDirs["imports/mods"],
|
||||
"ensureModsInboxDir creates parent imports/ or nested path")
|
||||
check(createdDirs["imports/mods"],
|
||||
"ensureModsInboxDir creates imports/mods/")
|
||||
|
||||
-- NXMOD-01: notice/hint includes save dir + relative imports/mods/ MTP path
|
||||
ri = freshImporter()
|
||||
ri:_setNxModsInboxNotice()
|
||||
check(ri.modNotice ~= nil, "NX mods inbox notice is set")
|
||||
check(ri.modNotice.text:find("sdmc:/switch/gen1recomp/pokemon-love2d/imports/mods/", 1, true),
|
||||
"mods notice contains runtime save path + imports/mods/")
|
||||
check(ri.modNotice.text:find("DBI MTP", 1, true) ~= nil,
|
||||
"mods notice contains OpenMTP-oriented hint")
|
||||
check(ri.modNotice.text:find("switch/gen1recomp/pokemon-love2d/imports/mods/", 1, true),
|
||||
"hint uses sdmc-stripped relative imports/mods/ path")
|
||||
|
||||
-- NXMOD-02: scanModsInbox returns only *.zip under imports/mods/
|
||||
ri = freshImporter()
|
||||
love.filesystem.write("imports/mods/valid.zip", "ZIPDATA")
|
||||
love.filesystem.write("imports/mods/readme.txt", "nope")
|
||||
love.filesystem.write("imports/mods/cart.gb", string.rep("R", 16))
|
||||
love.filesystem.write("imports/other.zip", "WRONGDIR")
|
||||
local zips = ri:scanModsInbox()
|
||||
eq(#zips, 1, "scanModsInbox returns one zip candidate")
|
||||
eq(zips[1], "imports/mods/valid.zip", "scanModsInbox path is under imports/mods/")
|
||||
|
||||
-- ROM scanInbox must not treat .zip as ROM
|
||||
ri = freshImporter()
|
||||
love.filesystem.write("imports/modpack.zip", "ZIPROM")
|
||||
love.filesystem.write("imports/mods/also.zip", "ZIPMOD")
|
||||
local roms = ri:scanInbox(ri.ready)
|
||||
for _, path in ipairs(roms) do
|
||||
check(not path:lower():match("%.zip$"),
|
||||
"ROM scanInbox ignores zip: " .. tostring(path))
|
||||
end
|
||||
eq(#roms, 0, "ROM scanInbox finds no zip-only inbox entries")
|
||||
|
||||
-- Edge: ROM inbox with .gb alongside .zip still ignores zip (spec edge)
|
||||
ri = freshImporter()
|
||||
love.filesystem.write("imports/cart.gb", string.rep("G", 16))
|
||||
love.filesystem.write("imports/sidecar.zip", "NOTAROM")
|
||||
roms = ri:scanInbox(ri.ready)
|
||||
local sawGb, sawZip = false, false
|
||||
for _, path in ipairs(roms) do
|
||||
if path:lower():match("%.zip$") then sawZip = true end
|
||||
if path:lower():match("%.gb$") then sawGb = true end
|
||||
end
|
||||
check(sawGb, "ROM scan still finds .gb when zip present")
|
||||
check(not sawZip, "ROM scan never lists .zip even beside .gb")
|
||||
|
||||
-- Stub LauncherMods.installZip for rescan tests (NXMOD-02..04)
|
||||
local installCalls = {}
|
||||
local installBehavior = {} -- path -> {ok=bool, id=string|err}
|
||||
package.loaded["src.mods.LauncherMods"] = {
|
||||
installZip = function(source)
|
||||
installCalls[#installCalls + 1] = source
|
||||
local b = installBehavior[source]
|
||||
if not b then return false, "unexpected source: " .. tostring(source) end
|
||||
if b.ok then return true, b.id or "mod-id" end
|
||||
return false, b.err or "bad zip"
|
||||
end,
|
||||
}
|
||||
|
||||
-- Empty inbox rescan → MTP notice, no install
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 0, "empty mods inbox does not call installZip")
|
||||
check(ri.modNotice ~= nil and ri.modNotice.text:find("imports/mods/", 1, true),
|
||||
"empty rescan shows mods MTP notice")
|
||||
|
||||
-- Success → refresh; zip retained (no remove)
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
removed = {}
|
||||
love.filesystem.write("imports/mods/good.zip", "GOODZIP")
|
||||
installBehavior["imports/mods/good.zip"] = { ok = true, id = "good-mod" }
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 1, "success path calls installZip once")
|
||||
eq(installCalls[1], "imports/mods/good.zip", "installZip receives inbox path")
|
||||
check(ri._refreshed and ri._refreshed >= 1, "success refreshes mods list")
|
||||
check(ri.modNotice and ri.modNotice.ok, "success sets ok notice")
|
||||
check(not removed["imports/mods/good.zip"], "success retains inbox zip")
|
||||
check(love.filesystem.read("imports/mods/good.zip") == "GOODZIP",
|
||||
"success leaves zip bytes in inbox")
|
||||
|
||||
-- Failure → clear notice; zip retained
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
removed = {}
|
||||
love.filesystem.write("imports/mods/bad.zip", "BADZIP")
|
||||
installBehavior["imports/mods/bad.zip"] = { ok = false, err = "missing manifest" }
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 1, "failure path still attempts installZip")
|
||||
check(ri.modNotice and not ri.modNotice.ok, "failure sets clear error notice")
|
||||
check(ri.modNotice.text:find("missing manifest", 1, true),
|
||||
"failure notice includes installZip error")
|
||||
check(not removed["imports/mods/bad.zip"], "failure does not remove inbox zip")
|
||||
check(love.filesystem.read("imports/mods/bad.zip") == "BADZIP",
|
||||
"failure leaves zip in inbox")
|
||||
|
||||
-- Mixed valid/invalid: attempt each; no zip deleted
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
removed = {}
|
||||
love.filesystem.write("imports/mods/a-bad.zip", "BAD")
|
||||
love.filesystem.write("imports/mods/b-good.zip", "GOOD")
|
||||
installBehavior["imports/mods/a-bad.zip"] = { ok = false, err = "no manifest" }
|
||||
installBehavior["imports/mods/b-good.zip"] = { ok = true, id = "b-mod" }
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 2, "mixed inbox attempts each zip")
|
||||
check(not removed["imports/mods/a-bad.zip"], "mixed: bad zip retained")
|
||||
check(not removed["imports/mods/b-good.zip"], "mixed: good zip retained")
|
||||
check(love.filesystem.read("imports/mods/a-bad.zip") ~= nil, "mixed bad still present")
|
||||
check(love.filesystem.read("imports/mods/b-good.zip") ~= nil, "mixed good still present")
|
||||
check(ri.modNotice and ri.modNotice.ok, "mixed keeps overall success when one zip installs")
|
||||
check(ri.modNotice.text:find("failed", 1, true),
|
||||
"mixed success notice still surfaces sibling failure")
|
||||
check(ri.modNotice.text:find("no manifest", 1, true),
|
||||
"mixed success notice includes the failure reason")
|
||||
|
||||
-- Mac MTP AppleDouble (._*.zip) must not be install candidates
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
love.filesystem.write("imports/mods/._DRAMATIC_SHAPE-1.4.0.zip", "APPL")
|
||||
love.filesystem.write("imports/mods/DRAMATIC_SHAPE-1.4.0.zip", "GOOD")
|
||||
installBehavior["imports/mods/DRAMATIC_SHAPE-1.4.0.zip"] = { ok = true, id = "dramatic_shape" }
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 1, "AppleDouble ._*.zip is skipped")
|
||||
eq(installCalls[1], "imports/mods/DRAMATIC_SHAPE-1.4.0.zip",
|
||||
"only the real zip is installed")
|
||||
check(ri.modNotice and ri.modNotice.ok, "AppleDouble skip still shows install success")
|
||||
check(not (ri.modNotice.text or ""):find("failed", 1, true),
|
||||
"AppleDouble-only sibling does not invent a mixed failure line")
|
||||
|
||||
-- Mac MTP AppleDouble ROM sidecar must not be ROM inbox candidates
|
||||
ri = freshImporter()
|
||||
love.filesystem.write("imports/._cart.gb", string.rep("X", 16))
|
||||
love.filesystem.write("imports/cart.gb", string.rep("G", 16))
|
||||
roms = ri:scanInbox(ri.ready)
|
||||
local sawHidden, sawReal = false, false
|
||||
for _, path in ipairs(roms) do
|
||||
if path:find("._cart", 1, true) then sawHidden = true end
|
||||
if path == "imports/cart.gb" then sawReal = true end
|
||||
end
|
||||
check(not sawHidden, "ROM scanInbox skips AppleDouble ._*.gb")
|
||||
check(sawReal, "ROM scanInbox still finds the real .gb")
|
||||
love.filesystem.remove("imports/._cart.gb")
|
||||
love.filesystem.remove("imports/cart.gb")
|
||||
|
||||
-- NXMOD-05: chooseMod on NX routes to inbox rescan; no HostShell/chooseZip
|
||||
local hostShellCalls = 0
|
||||
package.loaded["src.core.HostShell"] = {
|
||||
run = function()
|
||||
hostShellCalls = hostShellCalls + 1
|
||||
error("HostShell must not run on NX chooseMod")
|
||||
end,
|
||||
available = function() return false end,
|
||||
}
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
love.filesystem.write("imports/mods/from-choose.zip", "CHOOSE")
|
||||
installBehavior["imports/mods/from-choose.zip"] = { ok = true, id = "choose-mod" }
|
||||
ri:chooseMod()
|
||||
eq(hostShellCalls, 0, "NX chooseMod does not require HostShell")
|
||||
eq(#installCalls, 1, "NX chooseMod rescans and installs inbox zip")
|
||||
eq(installCalls[1], "imports/mods/from-choose.zip",
|
||||
"NX chooseMod installs from imports/mods/")
|
||||
check(ri.modNotice and ri.modNotice.ok, "NX chooseMod success notice")
|
||||
|
||||
-- NXMOD-01 UI: NX MODS panel label + hints mention imports/mods/
|
||||
ri = freshImporter()
|
||||
eq(ri:_modsImportButtonLabel(), "Scan again",
|
||||
"NX MODS button label is Scan again")
|
||||
local defaultHint = ri:_modsDefaultHint()
|
||||
check(defaultHint:find("imports/mods/", 1, true),
|
||||
"NX default hint mentions imports/mods/")
|
||||
check(defaultHint:find("DBI MTP", 1, true),
|
||||
"NX default hint mentions DBI MTP")
|
||||
local emptyHint = ri:_modsEmptyHint()
|
||||
check(emptyHint:find("imports/mods/", 1, true),
|
||||
"NX empty-state hint mentions imports/mods/")
|
||||
check(emptyHint:find("Scan again", 1, true),
|
||||
"NX empty-state hint mentions Scan again")
|
||||
|
||||
-- Desktop keeps Import mod .zip (non-NX)
|
||||
local desk = setmetatable({
|
||||
isNX = false, android = false,
|
||||
_modsImportButtonLabel = RomImporter._modsImportButtonLabel,
|
||||
_modsDefaultHint = RomImporter._modsDefaultHint,
|
||||
_modsEmptyHint = RomImporter._modsEmptyHint,
|
||||
}, RomImporter)
|
||||
eq(desk:_modsImportButtonLabel(), "Import mod .zip",
|
||||
"desktop MODS button stays Import mod .zip")
|
||||
check(desk:_modsDefaultHint():find("drop a mod", 1, true),
|
||||
"desktop default hint stays drop-oriented")
|
||||
|
||||
-- Edge: id-already-exists conflict retains inbox zip (no silent delete)
|
||||
ri = freshImporter()
|
||||
installCalls = {}
|
||||
removed = {}
|
||||
love.filesystem.write("imports/mods/dup.zip", "DUP")
|
||||
installBehavior["imports/mods/dup.zip"] = {
|
||||
ok = false, err = "mod id already installed",
|
||||
}
|
||||
ri:rescanModsAction()
|
||||
eq(#installCalls, 1, "conflict still attempts installZip")
|
||||
check(ri.modNotice and not ri.modNotice.ok, "conflict surfaces notice")
|
||||
check(not removed["imports/mods/dup.zip"], "conflict retains inbox zip")
|
||||
check(love.filesystem.read("imports/mods/dup.zip") == "DUP",
|
||||
"conflict leaves zip bytes intact")
|
||||
|
||||
-- Cleanup + restore stubs
|
||||
clearModsInbox()
|
||||
love.filesystem.remove("imports/other.zip")
|
||||
love.filesystem.remove("imports/modpack.zip")
|
||||
love.filesystem.remove("imports/cart.gb")
|
||||
love.filesystem.remove("imports/sidecar.zip")
|
||||
package.loaded["src.mods.LauncherMods"] = nil
|
||||
package.loaded["src.core.HostShell"] = nil
|
||||
love.system.getOS = saved.getOS
|
||||
love.filesystem.getSaveDirectory = saved.getSaveDirectory
|
||||
love.filesystem.createDirectory = saved.createDirectory
|
||||
love.filesystem.remove = saved.remove
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
package.loaded["src.import.RomImporter"] = nil
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user