-- The self-updater's host gate: which builds may hand off to a downloaded -- payload. A packaged build updates in place -- fused (AppImage, Flatpak's -- game.love) or unpacked (every PortMaster-style port, which runs -- `love ` and so reports isFused() false). A dev / source checkout must -- not, and an unknown host must fail closed. -- -- This gate is the difference between the PortMaster SBC port applying an -- update and silently ignoring it after the launcher already said "Restart to -- update", so the unpacked case is pinned here. -- luajit tests/engine/update_boot_host_gate.lua package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") local check, eq = T.check, T.eq local Boot = require("src.update.Boot") local love_stub = require("tests.love_stub") local VERSION_KEY = "src.core.Version" -- A packaged build stamps a real engine (X.Y.Z); a working tree keeps the -- "0.0.0-dev" placeholder, which is what Version.isDev() reports. local function withVersion(isDev, fn) local saved = package.loaded[VERSION_KEY] package.loaded[VERSION_KEY] = { isDev = function() return isDev end } local ok, err = pcall(fn) package.loaded[VERSION_KEY] = saved if not ok then error(err, 0) end end -- Stand in for the running LÖVE process: nil (no love at all) or a filesystem -- that reports the given fused state. local function withLove(fs, fn) local saved = _G.love _G.love = fs and { filesystem = fs } or nil local ok, err = pcall(fn) _G.love = saved if not ok then error(err, 0) end end local function fsWith(isFused) if isFused == nil then return {} end return { isFused = function() return isFused end } end -- no love at all (headless host): never update withLove(nil, function() eq(Boot.canUpdateInPlace(), false, "no love.filesystem -> no in-place update") end) -- fused wins outright: a binary carrying its own game archive is a packaged -- build whatever the engine string says withLove(fsWith(true), function() withVersion(false, function() eq(Boot.canUpdateInPlace(), true, "fused packaged build updates in place") end) withVersion(true, function() eq(Boot.canUpdateInPlace(), true, "fused build updates even with a dev engine string") end) end) -- the fix: an unpacked packaged build (PortMaster SBC / RG34XXSP) is not fused, -- but it carries a released engine and its source on disk is never rewritten, -- so a downloaded payload can be mounted over it withLove(fsWith(false), function() withVersion(false, function() eq(Boot.canUpdateInPlace(), true, "unpacked released build (love ) updates in place") end) withVersion(true, function() eq(Boot.canUpdateInPlace(), false, "unpacked dev checkout (engine 0.0.0-dev) never updates") end) end) -- a filesystem that cannot answer the fused question at all still updates when -- the engine proves this is a packaged build withLove(fsWith(nil), function() withVersion(false, function() eq(Boot.canUpdateInPlace(), true, "released build without isFused() still updates") end) end) -- fail closed: an unreadable / malformed Version must not open the gate withLove(fsWith(false), function() local saved = package.loaded[VERSION_KEY] package.loaded[VERSION_KEY] = "not a module" eq(Boot.canUpdateInPlace(), false, "non-table Version -> no in-place update") package.loaded[VERSION_KEY] = { engine = "1.4.2" } eq(Boot.canUpdateInPlace(), false, "Version without isDev -> no in-place update") package.loaded[VERSION_KEY] = nil eq(Boot.canUpdateInPlace(), false, "unloadable Version -> no in-place update") package.loaded[VERSION_KEY] = saved end) -- Boot.run is the first line of love.load: in a dev checkout it must return -- false immediately, leaving the bundled game to boot as normal withLove(love_stub.filesystem, function() withVersion(true, function() eq(Boot.run(nil), false, "Boot.run is a no-op in a dev checkout") end) end) T.finish("update_boot_host_gate")