Files
gen1recomp/tests/engine/sync_state_test.lua
T
2026-09-09 09:46:27 -04:00

137 lines
5.1 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = love or require("tests.love_stub")
local SaveData = require("src.core.SaveData")
local SyncState = require("src.sync.SyncState")
local realFS = love.filesystem
local function memfs(files)
return {
files = files,
write = function(path, content) files[path] = content return true end,
read = function(path) return files[path] end,
remove = function(path) files[path] = nil return true end,
getInfo = function(path)
if files[path] then return { type = "file" } end
return nil
end,
}
end
local function fresh()
local files = {}
love.filesystem = memfs(files)
SaveData.resetSlotState()
return files
end
do
local opts = SaveData.defaultOptions()
T.check(type(opts.saveSync) == "table", "defaultOptions carries saveSync")
T.eq(opts.saveSync.enabled, false, "sync is off until the player sets it up")
T.check(type(opts.saveSync.revs) == "table", "and starts with no synced revs")
T.eq(opts.saveSync.account, nil, "and no account")
local state = SyncState.defaults()
T.eq(SyncState.linked(state), false, "a default state is not linked")
T.eq(state.lastSyncAt, 0, "and has never synced")
end
do
local dirty = SyncState.sanitize({
enabled = "yes",
account = "aa11bb22cc33dd44",
deviceToken = "tok",
deviceLabel = "",
lastSyncAt = 0 / 0,
code1 = "12345678",
code2 = "87654321",
revs = { ["red/aaa"] = 4, [7] = 9, ["red/bad"] = "no" },
stamps = { ["red/aaa"] = 1700 },
pendingConflicts = { { key = "red/aaa", version = "red", overlap = true },
{ nope = true } },
})
T.eq(dirty.enabled, false, "a non-boolean enabled reads as off")
T.eq(dirty.account, "aa11bb22cc33dd44", "the account id survives")
T.eq(dirty.deviceLabel, nil, "an empty device label is dropped")
T.eq(dirty.lastSyncAt, 0, "a NaN lastSyncAt is refused")
T.eq(dirty.code1, "12345678", "the first account code is kept")
T.eq(dirty.code2, "87654321", "and the second")
T.eq(SyncState.sanitize({ code1 = "1234", code2 = "8765-4321" }).code1, nil,
"a code that is not 8 digits is dropped")
T.eq(SyncState.sanitize({ code1 = "1234", code2 = "8765-4321" }).code2,
"87654321", "and a grouped code is stored as bare digits")
T.eq(dirty.revs["red/aaa"], 4, "numeric revs survive")
T.eq(dirty.revs["red/bad"], nil, "a non-numeric rev is dropped")
T.eq(dirty.revs[7], nil, "a non-string rev key is dropped")
T.eq(dirty.stamps["red/aaa"], 1700, "the savedAt stamp survives")
T.eq(#dirty.pendingConflicts, 1, "only well-formed conflicts are kept")
T.eq(dirty.pendingConflicts[1].overlap, true, "with their overlap flag")
end
do
local files = fresh()
local state = SyncState.load()
T.eq(SyncState.linked(state), false, "a first boot has no linked account")
state.account = "aa11bb22cc33dd44"
state.deviceToken = "feedface"
state.deviceId = "0a1b2c3d"
state.deviceLabel = "laptop"
state.enabled = true
state.code1 = "12345678"
SyncState.setRev(state, SyncState.key("red", "abc"), 3, 1700000000)
SyncState.save(state)
T.check(files["options.lua"] ~= nil, "the state lands in options.lua")
T.check(files["options.lua"]:find("12345678", 1, true) ~= nil,
"the account codes are written with the state so the popup can show them")
local back = SyncState.load()
T.eq(SyncState.linked(back), true, "the linked account survives a reload")
T.eq(back.deviceLabel, "laptop", "and the device label")
T.eq(back.deviceId, "0a1b2c3d",
"and the device id the server revokes tokens by")
T.eq(SyncState.rev(back, "red/abc"), 3, "and the last synced rev")
T.eq(SyncState.stamp(back, "red/abc"), 1700000000, "and the savedAt stamp")
T.eq(back.code1, "12345678", "the code survives the reload")
local opts = SaveData.loadOptions()
T.eq(opts.textSpeed, 3, "writing sync state leaves other options alone")
SyncState.forget(back, "red/abc")
T.eq(SyncState.rev(back, "red/abc"), nil, "forget drops the rev")
T.eq(SyncState.stamp(back, "red/abc"), nil, "and the stamp")
SyncState.clear()
T.eq(SyncState.linked(SyncState.load()), false, "clear unlinks the device")
end
do
T.eq(SyncState.key("red", "abc"), "red/abc", "keys join version and id")
T.eq(SyncState.key("red", ""), nil, "an empty playthrough id has no key")
T.eq(SyncState.key(nil, "abc"), nil, "and neither does a missing version")
local version, id = SyncState.splitKey("gold/deadbeef")
T.eq(version, "gold", "splitKey reads the version back")
T.eq(id, "deadbeef", "and the playthrough id")
end
love.filesystem = realFS
do
local state = SyncState.defaults()
SyncState.markDeleted(state, "red/abc", 3, 900)
local back = SyncState.sanitize(state)
T.eq(back.pendingDeletes["red/abc"].rev, 3, "a pending delete survives sanitize")
T.eq(back.pendingDeletes["red/abc"].deletedAt, 900, "with its time")
SyncState.clearDeleted(back, "red/abc")
T.eq(next(back.pendingDeletes), nil, "and can be cleared")
T.eq(next(SyncState.sanitize({ pendingDeletes = { [7] = {}, x = "no" } })
.pendingDeletes), nil, "malformed pending deletes are dropped")
end
T.finish("sync_state")