feat(mod-api): expose developer mode

This commit is contained in:
MaxTomahawk
2026-08-24 10:37:10 +02:00
parent e88f2ef060
commit 58681fe62e
6 changed files with 278 additions and 2 deletions
@@ -0,0 +1,66 @@
-- No-mod and API-v1 parity for the additive mod.developer surface.
--
-- The production break this catches is a developer-mode loader path that
-- mutates vanilla data, creates mod state, or changes existing API-v1
-- behavior merely because the new public signal exists.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local function pristine()
return {
pokemon = { KEEP = { hp = 7 } },
moves = {},
}
end
for _, dev in ipairs({ false, true }) do
local data = pristine()
local files = {}
local run = T.sdk.loadNone({
data = data,
fs = T.sdk.memfs(files),
dev = dev,
})
T.eq(#run.errors, 0,
"no-mod load stays clean with developer=" .. tostring(dev))
T.eq(next(run.loader.mods), nil,
"no-mod load discovers nothing with developer=" .. tostring(dev))
T.eq(data.pokemon.KEEP.hp, 7,
"no-mod load preserves vanilla data with developer=" .. tostring(dev))
T.eq(next(files), nil,
"no-mod load creates no files with developer=" .. tostring(dev))
run.release()
end
local V1 = {
["mods/v1_probe/manifest.json"] = [[{
"id": "v1_probe",
"name": "V1 Probe",
"version": "1.0.0",
"entry": "main.lua",
"api": 1
}]],
["mods/v1_probe/main.lua"] = [[
local mod = ...
mod.exports.identity = mod.id .. "@" .. mod.version
mod.exports.payload = mod:read("payload.txt")
mod.options:define({ { key = "enabled", type = "toggle", default = true } })
mod.exports.defaultOption = mod.options:get("enabled")
]],
["mods/v1_probe/payload.txt"] = "unchanged-v1",
}
local legacy = T.sdk.loadMods({ "mods/v1_probe" }, {
fs = T.sdk.memfs(V1),
dev = false,
})
T.eq(#legacy.errors, 0, "existing API-v1 mod loads unchanged")
local out = legacy.loader.exports.v1_probe
T.eq(out.identity, "v1_probe@1.0.0", "API-v1 identity stays unchanged")
T.eq(out.payload, "unchanged-v1", "API-v1 mod:read stays unchanged")
T.eq(out.defaultOption, true, "API-v1 options stay unchanged")
legacy.release()
T.finish("mod developer mode parity")
+86
View File
@@ -0,0 +1,86 @@
-- Public load-time developer-mode signal for sandboxed mods.
--
-- The production break this catches is a loader that computes dev mode but
-- does not expose the same fixed answer to the public mod object before the
-- entry chunk runs. It also protects the data-only contract: the public
-- value is a boolean snapshot, not a live loader or environment handle.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local FILES = {
["mods/dev_probe/manifest.json"] = [[{
"id": "dev_probe",
"name": "Developer Mode Probe",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": ["all"]
}]],
["mods/dev_probe/main.lua"] = [[
local mod = ...
mod.exports.seenAtLoad = mod.developer
mod.exports.kind = type(mod.developer)
if mod.developer then
mod.commands:register("dev_probe:diagnostics", function() return true end)
end
]],
}
local function load(dev, generation)
return T.sdk.loadMods({ "mods/dev_probe" }, {
fs = T.sdk.memfs(FILES),
dev = dev,
generation = generation,
})
end
do
local run = load(true)
T.eq(#run.errors, 0, "developer-mode public probe loads clean")
local out = run.loader.exports.dev_probe
T.eq(out.seenAtLoad, true,
"sandboxed entry code sees developer mode at load time")
T.eq(out.kind, "boolean", "developer mode is exposed as plain data")
T.check(run.loader.content.commands:get("dev_probe:diagnostics") ~= nil,
"entry code can register diagnostics only in developer mode")
run.release()
end
do
local run = load(false)
T.eq(#run.errors, 0, "production-mode public probe loads clean")
local out = run.loader.exports.dev_probe
T.eq(out.seenAtLoad, false,
"sandboxed entry code sees production mode at load time")
T.eq(out.kind, "boolean", "production mode is exposed as plain data")
T.eq(run.loader.content.commands:get("dev_probe:diagnostics"), nil,
"production load does not register developer diagnostics")
run.release()
end
do
local run = load(true, 2)
T.eq(#run.errors, 0, "Gen 2 developer-mode public probe loads clean")
local out = run.loader.exports.dev_probe
T.eq(out.seenAtLoad, true,
"Gen 2 entry code sees the same developer-mode answer")
T.check(run.loader.content.commands:get("dev_probe:diagnostics") ~= nil,
"Gen 2 entry code can gate diagnostics on the same signal")
run.release()
end
do
local saved = _G.POKEPORT_DEV_MODE
_G.POKEPORT_DEV_MODE = true
local ok, run = pcall(load, nil)
_G.POKEPORT_DEV_MODE = saved
if not ok then error(run, 0) end
T.eq(#run.errors, 0, "command-line developer-mode probe loads clean")
T.eq(run.loader.exports.dev_probe.seenAtLoad, true,
"the --developer boot decision reaches the public signal")
run.release()
end
T.finish("mod developer mode public API")