mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
93e071bcaa
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>
145 lines
7.1 KiB
Lua
145 lines
7.1 KiB
Lua
-- Pure-surface coverage for src/update/Check.lua (the self-update release
|
|
-- check / payload download module). The network, hashing and archive-probe
|
|
-- logic lives in src/update/check_worker.lua and needs love + curl; these are
|
|
-- the love-free extraction/parsing seams the worker and UI both trust.
|
|
-- luajit tests/engine/update_check_tests.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
local Check = require("src.update.Check")
|
|
local Json = require("src.link.Json")
|
|
|
|
-- releaseUrl is the fixed public landing page the UI links on needs_full
|
|
eq(Check.releaseUrl(),
|
|
"https://github.com/bryanthaboi/gen1recomp/releases/latest",
|
|
"releaseUrl points at the repo's latest release")
|
|
|
|
-- parseRelease: a well-formed release with the .love payload and its sums
|
|
local body = Json.encode({
|
|
tag_name = "v1.4.2",
|
|
assets = {
|
|
{ name = "gen1recomp-1.4.2-macos.zip", browser_download_url = "http://x/mac", size = 10 },
|
|
{ name = "gen1recomp-1.4.2.love", browser_download_url = "http://x/love", size = 12345 },
|
|
{ name = "sha256sums.txt", browser_download_url = "http://x/sums", size = 99 },
|
|
},
|
|
})
|
|
local rel = Check.parseRelease(body)
|
|
check(rel ~= nil, "parseRelease accepts a valid release")
|
|
eq(rel.version, "1.4.2", "leading v stripped from tag_name")
|
|
eq(rel.payloadName, "gen1recomp-1.4.2.love", "payload name derived from version")
|
|
eq(rel.payload.url, "http://x/love", "payload asset url picked")
|
|
eq(rel.payload.size, 12345, "payload asset size picked")
|
|
eq(rel.sums.url, "http://x/sums", "sums asset url picked")
|
|
eq(rel.notes, "", "missing release body becomes empty notes")
|
|
|
|
-- Native package assets are target-specific. This is intentionally separate
|
|
-- from the generic .love payload, which a new shell may be unable to host.
|
|
local android = Check.parseRelease(body, nil, { os = "Android", arch = "arm64" })
|
|
eq(android.fullName, "gen1recomp-1.4.2-android.apk", "Android package name derived")
|
|
eq(android.full, nil, "missing Android package is surfaced as nil")
|
|
eq(Check.fullAssetName("1.4.2", "Android", "arm64"),
|
|
"gen1recomp-1.4.2-android.apk", "Android full asset mapping")
|
|
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64"),
|
|
"gen1recomp-1.4.2-linux-arm64.AppImage", "Linux ARM package mapping")
|
|
eq(Check.fullAssetName("1.4.2", "Linux", "x64"),
|
|
"gen1recomp-1.4.2-linux-x86_64.AppImage", "Linux x86_64 AppImage mapping")
|
|
eq(Check.fullAssetName("1.4.2", "Linux", "x64", "flatpak"),
|
|
"gen1recomp-1.4.2-linux.flatpak", "Linux Flatpak package mapping")
|
|
-- Handheld ports run aarch64 Linux, so the port tag must win over the arch
|
|
-- mapping: a desktop AppImage cannot be installed on a PortMaster device.
|
|
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64", "portmaster"),
|
|
"gen1recomp-1.4.2-sbc-portmaster.zip", "PortMaster SBC package mapping")
|
|
eq(Check.fullAssetName("1.4.2", "Linux", "aarch64", "rg34xxsp"),
|
|
"gen1recomp-1.4.2-rg34xxsp-stockos64-mod.zip", "RG34XXSP package mapping")
|
|
eq(Check.fullAssetName("1.4.2", "iOS", "arm64"),
|
|
"gen1recomp++-1.4.2-ios.ipa", "iOS package mapping")
|
|
eq(Check.fullAssetName("not-a-version", "Android", "arm64"), nil,
|
|
"invalid full-package version rejected")
|
|
|
|
-- The port tag comes from the launcher's environment. POKEPORT_HANDHELD is the
|
|
-- marker the SBC launcher has always exported, so a pack built before the
|
|
-- explicit POKEPORT_PORTMASTER existed is still identified as the PortMaster
|
|
-- port rather than a desktop Linux build.
|
|
do
|
|
local realGetenv = os.getenv
|
|
local function portWith(vars)
|
|
os.getenv = function(name) return vars[name] end
|
|
local port = Check.hostPort()
|
|
os.getenv = realGetenv
|
|
return port
|
|
end
|
|
eq(portWith({ POKEPORT_PORTMASTER = "1" }), "portmaster",
|
|
"PortMaster launcher marker identifies the SBC port")
|
|
eq(portWith({ POKEPORT_RG34XXSP = "1" }), "rg34xxsp",
|
|
"RG34XXSP launcher marker identifies that port")
|
|
eq(portWith({ POKEPORT_HANDHELD = "1" }), "portmaster",
|
|
"legacy handheld marker identifies the SBC port")
|
|
eq(portWith({ PORTMASTER = "1" }), nil,
|
|
"PORTMASTER alone is not the release-target marker")
|
|
eq(portWith({}), nil, "a desktop Linux build has no port tag")
|
|
end
|
|
|
|
-- A legacy Android shell needs one manual package update even when its
|
|
-- downloaded payload already reports the latest engine version.
|
|
eq(Check.androidNeedsInstallerBootstrap("Android", false), true,
|
|
"legacy Android shell requires one bootstrap package")
|
|
eq(Check.androidNeedsInstallerBootstrap("Android", true), false,
|
|
"current Android shell keeps the selective updater")
|
|
eq(Check.androidNeedsInstallerBootstrap("Windows", false), false,
|
|
"non-Android update behavior remains unchanged")
|
|
|
|
local withNotes = Check.parseRelease(Json.encode({
|
|
tag_name = "v1.4.2",
|
|
body = "## Issues closed\n\n- #1 cart padding",
|
|
assets = {},
|
|
}))
|
|
eq(withNotes.notes, "## Issues closed\n\n- #1 cart padding",
|
|
"parseRelease keeps the GitHub release body")
|
|
|
|
-- a newer release that ships no .love yet: parses, but the payload/sums are nil
|
|
-- so the worker will route to needs_full rather than an in-place update
|
|
local noPayload = Check.parseRelease(Json.encode({ tag_name = "2.0.0", assets = {} }))
|
|
check(noPayload ~= nil, "parseRelease accepts a payload-less release")
|
|
eq(noPayload.version, "2.0.0", "version parsed without assets")
|
|
eq(noPayload.payload, nil, "no payload asset -> nil")
|
|
eq(noPayload.sums, nil, "no sums asset -> nil")
|
|
|
|
-- rejects: non-semver tag, and a document with no tag at all
|
|
local bad, badErr = Check.parseRelease(Json.encode({ tag_name = "nightly" }))
|
|
eq(bad, nil, "non-X.Y.Z tag rejected")
|
|
check(badErr ~= nil, "rejection carries an error string")
|
|
eq(Check.parseRelease(Json.encode({ foo = 1 })), nil, "missing tag_name rejected")
|
|
|
|
-- Network failures can return a plain-text or HTML body instead of JSON. The
|
|
-- launcher must describe that response rather than leaking the decoder's
|
|
-- low-level "unexpected character 'E'" assertion.
|
|
do
|
|
local release, err = Check.parseRelease("Error: API rate limit exceeded")
|
|
check(release == nil and tostring(err):find("not JSON", 1, true) ~= nil,
|
|
"plain-text update failure is reported as non-JSON")
|
|
release, err = Check.parseRelease("<!DOCTYPE html><html>502 Bad Gateway</html>")
|
|
check(release == nil and tostring(err):find("HTML", 1, true) ~= nil,
|
|
"HTML update failure is identified")
|
|
check(tostring(err):find("unexpected character", 1, true) == nil,
|
|
"decoder assertion is not exposed")
|
|
end
|
|
|
|
-- parseSums: shasum -a 256 format, tolerating the '*' binary marker, a './'
|
|
-- prefix and CRLF line endings; unrelated lines are skipped
|
|
local sums =
|
|
"aaaa1111 gen1recomp-1.4.2.love\n" ..
|
|
"BBBB2222 *./sha256sums.txt\r\n" ..
|
|
"not a checksum line\n"
|
|
local map = Check.parseSums(sums)
|
|
eq(map["gen1recomp-1.4.2.love"], "aaaa1111", "bare-name sum parsed")
|
|
eq(map["sha256sums.txt"], "bbbb2222", "* marker and ./ prefix stripped, lowered")
|
|
eq(Check.parseSums(sums, "gen1recomp-1.4.2.love"), "aaaa1111", "targeted lookup returns the hash")
|
|
eq(Check.parseSums(sums, "missing.love"), nil, "targeted lookup misses cleanly")
|
|
|
|
-- pickAsset guards a non-table assets field
|
|
eq(Check.pickAsset(nil, "x"), nil, "pickAsset tolerates a nil asset list")
|
|
|
|
T.finish("update_check")
|