Files
gen1recomp/tests/engine/update_boot_host_gate.lua
T
Shane McGovern 93e071bcaa Fix self-update on the PortMaster SBC build
The SBC and RG34XXSP ports hand LÖVE a source *directory*
(`love <dir>`), so `love.filesystem.isFused()` is false there and
`Boot.run` bailed on its first line.  The launcher still offered
"Update vX.Y.Z" and "Restart to update", so an update downloaded,
verified, and then was silently ignored forever.

- Boot.canUpdateInPlace() replaces the fused-only gate: a packaged
  build updates whether it is fused (AppImage, Flatpak game.love) or
  unpacked, and only a dev / source checkout (engine "0.0.0-dev") is
  excluded.  Fails closed when the host cannot be established.
  Prelaunch.updateAllowed now delegates to it, so the boot gate and the
  `--update` gate cannot disagree.
- Check.hostPort() reads the release-target marker, with
  POKEPORT_HANDHELD as a legacy fallback (the SBC launcher has always
  exported it, so packs predating this change still identify
  themselves).  The marker was read from the environment but never
  exported, so a full-package fallback on a handheld resolved to a
  desktop AppImage that cannot run there.
- A handheld now fetches its own package ("Download port update",
  reusing the worker's cross-platform download_full) instead of
  offering a URL it has no browser to open, and reports "Update package
  ready" once it is in the save directory for a manual re-extract.
- Launchers export POKEPORT_PORTMASTER / POKEPORT_RG34XXSP.

An in-place update only ever mounts a payload over the running source,
so relaxing the gate is less invasive than the fused path it joins.
A runtime bump still needs a full package, which the minShell gate
already reports as needs_full.

Tests: tests/engine/update_boot_host_gate.lua (new, 10 checks) pins the
gate; update_check_tests.lua pins both port asset names and hostPort().
scripts/test.sh --quick: all tiers passed; scripts/lint.sh: 0 errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-19 22:15:02 +01:00

107 lines
3.9 KiB
Lua

-- 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 <dir>` 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 <dir>) 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")