mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
test: cover title checkpoint resume recovery
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
-- A tool can persist a checkpoint before the first normal Pokémon save. After
|
-- A tool can persist a checkpoint before the first normal Pokémon save. After
|
||||||
-- a restart the title runtime is deliberately a fresh save skeleton, so it
|
-- a restart the title runtime is deliberately a fresh save skeleton, so it
|
||||||
-- needs a read-only binding to the already-selected playthrough -- not a call
|
-- needs a non-allocating binding to the already-selected playthrough -- not a
|
||||||
-- to the normal active-playthrough storage methods, which would mint an id.
|
-- call to the normal active-playthrough storage methods, which would mint an id.
|
||||||
--
|
--
|
||||||
-- This is a public SDK contract test. The fixture never reaches into storage
|
-- This is a public SDK contract test. The fixture never reaches into storage
|
||||||
-- paths or launcher slot internals.
|
-- paths or launcher slot internals.
|
||||||
@@ -59,6 +59,10 @@ local files = {
|
|||||||
return function(mod)
|
return function(mod)
|
||||||
_G.MOD_TITLE_STORAGE = mod.storage
|
_G.MOD_TITLE_STORAGE = mod.storage
|
||||||
_G.MOD_TITLE_CHECKPOINTS = mod.checkpoints
|
_G.MOD_TITLE_CHECKPOINTS = mod.checkpoints
|
||||||
|
_G.MOD_TITLE_RESTORE_COUNT = 0
|
||||||
|
mod.events:on("checkpoint.restored", function()
|
||||||
|
_G.MOD_TITLE_RESTORE_COUNT = _G.MOD_TITLE_RESTORE_COUNT + 1
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
]],
|
]],
|
||||||
}
|
}
|
||||||
@@ -87,6 +91,9 @@ if type(storage) == "table" then
|
|||||||
local originalId = active.save.meta and active.save.meta.playthroughId
|
local originalId = active.save.meta and active.save.meta.playthroughId
|
||||||
T.check(type(originalId) == "string" and originalId ~= "",
|
T.check(type(originalId) == "string" and originalId ~= "",
|
||||||
"first tool persistence allocates the opaque active playthrough identity")
|
"first tool persistence allocates the opaque active playthrough identity")
|
||||||
|
local nonTitleSelected, nonTitleCode = storage:selected(active)
|
||||||
|
T.check(nonTitleSelected == nil and nonTitleCode == "not_at_title",
|
||||||
|
"selected-playthrough storage cannot be used from active gameplay")
|
||||||
|
|
||||||
-- Simulate a fresh process/title session. The normal save was never written:
|
-- Simulate a fresh process/title session. The normal save was never written:
|
||||||
-- only the engine-owned slot/playthrough mapping and this mod's durable data
|
-- only the engine-owned slot/playthrough mapping and this mod's durable data
|
||||||
@@ -117,6 +124,10 @@ if type(storage) == "table" then
|
|||||||
}, "selected binding reports the durable playthrough without exposing a slot path")
|
}, "selected binding reports the durable playthrough without exposing a slot path")
|
||||||
T.same(selected:read("history/index"), { format = 1, newest = "q0001" },
|
T.same(selected:read("history/index"), { format = 1, newest = "q0001" },
|
||||||
"title reads only this mod's selected-playthrough durable history")
|
"title reads only this mod's selected-playthrough durable history")
|
||||||
|
T.check(selected:write("history/title-operation", { allowed = true }) == true,
|
||||||
|
"title binding supports safe same-namespace durable operations")
|
||||||
|
T.same(selected:read("history/title-operation"), { allowed = true },
|
||||||
|
"title durable operation remains scoped to the selected playthrough")
|
||||||
end
|
end
|
||||||
T.check(title.save.meta.playthroughId == nil,
|
T.check(title.save.meta.playthroughId == nil,
|
||||||
"opening title history never allocates or adopts a playthrough identity")
|
"opening title history never allocates or adopts a playthrough identity")
|
||||||
@@ -155,6 +166,11 @@ if type(storage) == "table" then
|
|||||||
},
|
},
|
||||||
}, { __index = GameMethods })
|
}, { __index = GameMethods })
|
||||||
stack.states[1] = title and { screenId = "TitleState" } or overworld
|
stack.states[1] = title and { screenId = "TitleState" } or overworld
|
||||||
|
if title then
|
||||||
|
-- The failure-injection path needs the same title recovery contract as a
|
||||||
|
-- real Game without constructing renderer-owned title content.
|
||||||
|
function game:makeTitleState() return { screenId = "TitleState" } end
|
||||||
|
end
|
||||||
return game
|
return game
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -181,6 +197,34 @@ if type(storage) == "table" then
|
|||||||
"title bootstrap never creates a normal Pokémon save as a side effect")
|
"title bootstrap never creates a normal Pokémon save as a side effect")
|
||||||
T.same(checkpoints:capture(titleRuntime), checkpoint,
|
T.same(checkpoints:capture(titleRuntime), checkpoint,
|
||||||
"bootstrapped overworld differentially recaptures the selected checkpoint")
|
"bootstrapped overworld differentially recaptures the selected checkpoint")
|
||||||
|
T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1,
|
||||||
|
"a successful title bootstrap emits checkpoint.restored exactly once")
|
||||||
|
|
||||||
|
-- Force a failure after restoreCheckpointSave has already installed the
|
||||||
|
-- checkpoint's canonical save and overworld. Title has no live checkpoint
|
||||||
|
-- rollback, so it must rebuild a clean title session and must not publish
|
||||||
|
-- the success-only lifecycle event.
|
||||||
|
SaveData.resetSlotState()
|
||||||
|
local failingTitle = makeRuntime(SaveData.newGame({ version = "red" }), true)
|
||||||
|
failingTitle.save.options = { volume = 7, bindings = {} }
|
||||||
|
local restoreCheckpointSave = failingTitle.restoreCheckpointSave
|
||||||
|
function failingTitle:restoreCheckpointSave(loaded)
|
||||||
|
restoreCheckpointSave(self, loaded)
|
||||||
|
error("forced title reconstruction failure")
|
||||||
|
end
|
||||||
|
local failed, failureCode = checkpoints:resume(failingTitle, checkpoint)
|
||||||
|
T.check(failed == false and failureCode == "resume_failed",
|
||||||
|
"failed title reconstruction reports a recoverable bootstrap failure")
|
||||||
|
T.eq(failingTitle.stack:top().screenId, "TitleState",
|
||||||
|
"failed title reconstruction returns to a usable title session")
|
||||||
|
T.check(failingTitle.save.meta.playthroughId == nil,
|
||||||
|
"failed title reconstruction restores the unbound title skeleton")
|
||||||
|
T.eq(failingTitle.save.options.volume, 7,
|
||||||
|
"failed title reconstruction retains current title options")
|
||||||
|
T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1,
|
||||||
|
"failed title reconstruction never emits checkpoint.restored")
|
||||||
|
T.check(files["save.lua"] == nil,
|
||||||
|
"failed title reconstruction never writes a normal Pokémon save")
|
||||||
end
|
end
|
||||||
|
|
||||||
local explicitNewGame = SaveData.newGame({ version = "red" })
|
local explicitNewGame = SaveData.newGame({ version = "red" })
|
||||||
@@ -193,6 +237,7 @@ Runtime.events, Runtime.hooks = savedEvents, savedHooks
|
|||||||
Runtime.currentMod = nil
|
Runtime.currentMod = nil
|
||||||
_G.MOD_TITLE_STORAGE = nil
|
_G.MOD_TITLE_STORAGE = nil
|
||||||
_G.MOD_TITLE_CHECKPOINTS = nil
|
_G.MOD_TITLE_CHECKPOINTS = nil
|
||||||
|
_G.MOD_TITLE_RESTORE_COUNT = nil
|
||||||
SaveData.resetSlotState()
|
SaveData.resetSlotState()
|
||||||
SaveData.loadOptions = originalLoadOptions
|
SaveData.loadOptions = originalLoadOptions
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user