From 13b55a797ee6eb4e528ffda02a11e131bbd97d6f Mon Sep 17 00:00:00 2001 From: Andrew Quenehen Date: Sat, 1 Aug 2026 03:20:12 -0300 Subject: [PATCH] feat(import): show NX inbox path and rescan action Co-authored-by: Cursor --- tests/rom_importer_nx_inbox_test.lua | 215 +++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/rom_importer_nx_inbox_test.lua diff --git a/tests/rom_importer_nx_inbox_test.lua b/tests/rom_importer_nx_inbox_test.lua new file mode 100644 index 00000000..c07bb6b5 --- /dev/null +++ b/tests/rom_importer_nx_inbox_test.lua @@ -0,0 +1,215 @@ +-- NX writable-inbox import: path hint, rescan, validate, hash route (SWNX-05..09). +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 inbox") +local eq = S.eq +local check = S.check + +local GameVersion = require("src.core.GameVersion") +local RomImporter = require("src.import.RomImporter") + +local MiB = 1024 * 1024 +local redData = string.rep("R", MiB) +local blueData = string.rep("B", MiB) +local yellowData = string.rep("Y", MiB) +local badSizeData = string.rep("?", MiB - 1) +local unknownData = string.rep("?", MiB) + +love.data = love.data or {} +love.system = love.system or {} +love.filesystem = love.filesystem or {} + +local saved = { + hash = love.data.hash, + encode = love.data.encode, + getOS = love.system.getOS, + getSaveDirectory = love.filesystem.getSaveDirectory, + createDirectory = love.filesystem.createDirectory, + remove = love.filesystem.remove, +} + +love.data.hash = function(_, data) + return { tag = data:sub(1, 1) } +end +love.data.encode = function(_, _, digest) + if type(digest) == "table" and digest.tag == "R" then + return GameVersion.info("red").sha1 + end + if type(digest) == "table" and digest.tag == "B" then + return GameVersion.info("blue").sha1 + end + if type(digest) == "table" and digest.tag == "Y" then + return GameVersion.info("yellow").sha1 + end + return "0000000000000000000000000000000000000000" +end + +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") + +-- mtpHintPath strips only validated sdmc:/ prefix +eq(RomImporter.mtpHintPath("sdmc:/switch/gen1recomp/pokemon-love2d"), + "switch/gen1recomp/pokemon-love2d", "mtpHintPath strips sdmc:/") +eq(RomImporter.mtpHintPath("/save/pokemon-love2d"), + "/save/pokemon-love2d", "mtpHintPath leaves non-sdmc paths alone") +eq(RomImporter.mtpHintPath("sdmc:"), "sdmc:", + "mtpHintPath does not strip bare sdmc: without slash") + +local function clearInbox() + for _, name in ipairs(love.filesystem.getDirectoryItems("imports") or {}) do + love.filesystem.remove("imports/" .. name) + end + for _, name in ipairs(love.filesystem.getDirectoryItems("") or {}) do + if name:lower():match("%.gbc?$") then love.filesystem.remove(name) end + end +end + +local function freshImporter(ready) + clearInbox() + createdDirs = {} + removed = {} + package.loaded["src.import.RomImporter"] = nil + RomImporter = require("src.import.RomImporter") + return setmetatable({ + isNX = true, + romImportMode = "save-directory", + mobileFileBridge = false, + android = false, + launcher = true, + workState = nil, + tab = "red", + ready = { + red = ready.red and true or false, + blue = ready.blue and true or false, + yellow = ready.yellow and true or false, + }, + notice = nil, + chooseVersion = nil, + startData = function(self, data, displayName) + self._started = { data = data, name = displayName } + if #data ~= MiB then + self.workState = "error" + self.detail = "size" + elseif not GameVersion.forSha1( + love.data.encode("string", "hex", love.data.hash("sha1", data))) then + self.workState = "error" + self.detail = "hash" + end + end, + setError = function(self, message) + self.workState = "error" + self.detail = message + end, + ensureImportsDir = RomImporter.ensureImportsDir, + _setNxInboxNotice = RomImporter._setNxInboxNotice, + scanInbox = RomImporter.scanInbox, + rescanAction = RomImporter.rescanAction, + }, RomImporter) +end + +-- First open creates imports/ and shows save path + MTP hint +createdDirs = {} +local ri = freshImporter({ red = false, blue = false, yellow = false }) +ri:ensureImportsDir() +check(createdDirs.imports, "ensureImportsDir creates imports/") +ri:_setNxInboxNotice("red") +check(ri.notice ~= nil, "NX notice is set") +check(ri.notice.detail:find("sdmc:/switch/gen1recomp/pokemon-love2d/imports/", 1, true), + "notice contains runtime save path") +check(ri.notice.detail:find("DBI MTP", 1, true) ~= nil, + "notice contains OpenMTP-oriented hint") +check(ri.notice.detail:find("switch/gen1recomp/pokemon-love2d/imports/", 1, true), + "hint uses sdmc-stripped relative path") + +-- Empty inbox rescan refreshes notice +ri = freshImporter({ red = false, blue = false, yellow = false }) +ri:rescanAction("red") +check(ri.notice ~= nil, "empty inbox rescan shows notice") +check(ri._started == nil, "empty inbox does not start import") + +-- Bad extension ignored +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/readme.txt", "nope") +ri:rescanAction("red") +check(ri._started == nil, "non-ROM extension is ignored") + +-- Bad size rejected +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/small.gb", badSizeData) +ri:rescanAction("red") +check(ri._started ~= nil, "undersized ROM triggers import attempt") +eq(ri.workState, "error", "undersized ROM is rejected") +check(ri._started.name == "small.gb", "bad size uses basename") + +-- Unknown hash rejected +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/hacked.gb", unknownData) +ri:rescanAction("red") +check(ri._started ~= nil, "unknown hash ROM is routed through startData") +eq(ri.workState, "error", "unknown hash is rejected") + +-- Valid Red from imports/ +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/pokemon red.gb", redData) +ri:rescanAction("red") +check(ri._started ~= nil, "valid Red stub imports") +eq(ri._started.name, "pokemon red.gb", "unicode/space filename preserved") +eq(ri._started.data, redData, "Red bytes passed through") +check(not removed["pokemon red.gb"], "NX retains source dump after import start") + +-- imports/ scanned before save root +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/blue.gbc", blueData) +love.filesystem.write("red_root.gb", redData) +ri:rescanAction("blue") +eq(ri._started.name, "blue.gbc", "imports/ wins over save root ordering") + +-- Already-imported dump skipped so another version can import +ri = freshImporter({ red = true, blue = false, yellow = false }) +love.filesystem.write("imports/pokemon red.gb", redData) +love.filesystem.write("imports/pokemon blue.gb", blueData) +ri:rescanAction("blue") +eq(ri._started.name, "pokemon blue.gb", "ready Red dump ignored for pending Blue") + +-- Yellow valid stub +ri = freshImporter({ red = false, blue = false, yellow = false }) +love.filesystem.write("imports/pika.gbc", yellowData) +ri:rescanAction("yellow") +eq(ri._started.name, "pika.gbc", "valid Yellow stub imports") + +-- Cleanup + restore stubs shared with other suites +love.filesystem.remove("imports/readme.txt") +love.filesystem.remove("imports/small.gb") +love.filesystem.remove("imports/hacked.gb") +love.filesystem.remove("imports/pokemon red.gb") +love.filesystem.remove("imports/blue.gbc") +love.filesystem.remove("imports/pokemon blue.gb") +love.filesystem.remove("imports/pika.gbc") +love.filesystem.remove("red_root.gb") +love.data.hash = saved.hash +love.data.encode = saved.encode +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()