Lots of content
This commit is contained in:
Executable
+239
@@ -0,0 +1,239 @@
|
||||
local target = assert(arg[1], "clock.lua path required")
|
||||
local total = 0
|
||||
local function test(name, fn)
|
||||
local old_core, old_hearth, old_time = core, hearth, os.time
|
||||
local ok, err = pcall(fn)
|
||||
core, hearth, os.time = old_core, old_hearth, old_time
|
||||
assert(ok, name .. ": " .. tostring(err)); total = total + 1
|
||||
end
|
||||
-- Contract doubles keep this suite independent of the engine, filesystem,
|
||||
-- schema implementation, and real calendar/timezone. model_spec tests the math;
|
||||
-- persistence_spec tests validation/migration of the stored records themselves.
|
||||
local function harness(settings, singleplayer, stored)
|
||||
local calls = {loads = 0, saves = 0, deferred = {}, solar = {}}
|
||||
local C = {settings = {get = function(_, key) return settings[key] end}}
|
||||
C.is_singleplayer = function() return singleplayer end
|
||||
C.after = function(delay, fn) calls.deferred[#calls.deferred + 1] = {delay = delay, fn = fn} end
|
||||
C.set_timeofday = function(value) calls.timeofday = value end
|
||||
local P = {}
|
||||
P.load_world = function(kind)
|
||||
assert(kind == "location"); calls.loads = calls.loads + 1
|
||||
if calls.load_error then error("Last Hearth: persistence failed; save retained for recovery", 0) end
|
||||
return stored
|
||||
end
|
||||
P.save_world = function(kind, value)
|
||||
assert(kind == "location"); calls.saves = calls.saves + 1
|
||||
if calls.save_error then error("Last Hearth: persistence failed; save retained for recovery", 0) end
|
||||
calls.saved = value
|
||||
end
|
||||
local M = {}
|
||||
M.location = function(lat, lon)
|
||||
lat, lon = tonumber(lat), tonumber(lon)
|
||||
if not lat or not lon or lat ~= lat or lon ~= lon or math.abs(lat) > 90 or math.abs(lon) > 180 then return end
|
||||
return {latitude = lat, longitude = lon}
|
||||
end
|
||||
M.solar = function(now, lat, lon)
|
||||
local value = {now = now, latitude = lat, longitude = lon, timeofday = 0.5}
|
||||
calls.solar[#calls.solar + 1] = value
|
||||
return value
|
||||
end
|
||||
M.weather = function(now, lat, lon)
|
||||
calls.weather = {now = now, latitude = lat, longitude = lon}
|
||||
return "clear"
|
||||
end
|
||||
core, hearth = C, {model = M, persistence = P}
|
||||
os.time = function() return calls.now or 1800000000 end
|
||||
return calls
|
||||
end
|
||||
local function rejects(settings, singleplayer, text, stored)
|
||||
local calls = harness(settings, singleplayer, stored)
|
||||
local ok, message = pcall(dofile, target)
|
||||
assert(not ok and type(message) == "string" and message:find(text, 1, true), "Expected configuration rejection")
|
||||
assert(#calls.deferred == 0 and calls.saves == 0, "Rejected configuration must not start clock or save")
|
||||
return calls
|
||||
end
|
||||
local function configured(deployment)
|
||||
return {hearth_environment = deployment, hearth_latitude = "45", hearth_longitude = "0"}
|
||||
end
|
||||
test("singleplayer defaults local with disclosed neutral preview and deferred initialization", function()
|
||||
local calls = harness({}, true)
|
||||
dofile(target)
|
||||
assert(hearth.has_location == false and hearth.clock == nil and hearth.weather == nil)
|
||||
assert(calls.loads == 1 and calls.saves == 0 and #calls.deferred == 1)
|
||||
assert(calls.deferred[1].delay == 0 and calls.deferred[1].fn == hearth.clock_update)
|
||||
calls.deferred[1].fn()
|
||||
assert(hearth.clock.latitude == 0 and hearth.clock.longitude == 0)
|
||||
assert(hearth.clock.now == 1800000000 and hearth.weather == "clear" and calls.timeofday == 0.5)
|
||||
assert(hearth.has_location == false)
|
||||
end)
|
||||
test("dedicated defaults production and requires explicit coordinates", function()
|
||||
rejects({}, false, "requires explicit")
|
||||
local calls = harness(configured(nil), false)
|
||||
dofile(target); calls.deferred[1].fn()
|
||||
assert(hearth.has_location and hearth.clock.latitude == 45)
|
||||
end)
|
||||
test("only the three environment enum values are accepted", function()
|
||||
for _, value in ipairs({"", "dev", "LOCAL", "Production", " staging", "PRIVATE\nfixture"}) do
|
||||
rejects({hearth_environment = value}, true, "must be local, staging, or production")
|
||||
end
|
||||
end)
|
||||
test("dedicated local is forbidden even with coordinates", function()
|
||||
rejects(configured("local"), false, "dedicated servers cannot")
|
||||
end)
|
||||
test("production rejects every nonempty date before parsing or persistence", function()
|
||||
for _, value in ipairs({"1782043200", "bad", " ", "0", "nan"}) do
|
||||
local settings = configured("production"); settings.hearth_preview_date = value
|
||||
assert(rejects(settings, true, "production rejects").loads == 0)
|
||||
end
|
||||
end)
|
||||
test("production rejects all weather settings except auto", function()
|
||||
for _, value in ipairs({"rain", "clear", "", "bad", "AUTO"}) do
|
||||
local settings = configured("production"); settings.hearth_preview_weather = value
|
||||
rejects(settings, false, "production rejects")
|
||||
end
|
||||
end)
|
||||
test("production rejects launcher mode and smoke token independently", function()
|
||||
for _, key in ipairs({"hearth_launcher_mode", "hearth_smoke_token"}) do
|
||||
for _, value in ipairs({"", "PRIVATE\nfixture"}) do
|
||||
local settings = configured("production"); settings[key] = value
|
||||
local calls = harness(settings, false)
|
||||
local ok, message = pcall(dofile, target)
|
||||
assert(not ok and message == "Last Hearth: production rejects fixture and launcher test settings.")
|
||||
assert(calls.loads == 0 and calls.saves == 0 and #calls.deferred == 0)
|
||||
end
|
||||
end
|
||||
end)
|
||||
test("production rejects every course setting including empty and disabled strings before persistence", function()
|
||||
for _, singleplayer in ipairs({true, false}) do
|
||||
for _, key in ipairs({"hearth_m1_course", "hearth_m1_course_nonce",
|
||||
"hearth_m1_course_snag", "hearth_course_token"}) do
|
||||
for _, value in ipairs({"", "false", "0", "true", "PRIVATE\nfixture"}) do
|
||||
local settings = configured("production"); settings[key] = value
|
||||
local calls = harness(settings, singleplayer, {latitude = 45, longitude = 0})
|
||||
local ok, message = pcall(dofile, target)
|
||||
assert(not ok and message == "Last Hearth: production rejects fixture and launcher test settings.", key)
|
||||
assert(calls.loads == 0 and calls.saves == 0 and #calls.deferred == 0)
|
||||
assert(#calls.solar == 0 and calls.weather == nil and calls.timeofday == nil)
|
||||
assert(hearth.clock_update == nil and hearth.set_location == nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
test("implicit dedicated production also rejects course flags before loading stored location", function()
|
||||
for _, key in ipairs({"hearth_m1_course", "hearth_m1_course_nonce",
|
||||
"hearth_m1_course_snag", "hearth_course_token"}) do
|
||||
local settings = configured(nil); settings[key] = "false"
|
||||
assert(rejects(settings, false, "production rejects", {latitude = 45, longitude = 0}).loads == 0)
|
||||
end
|
||||
end)
|
||||
test("local and staging clocks accept course fixtures without granting enrollment or starting a run", function()
|
||||
for _, singleplayer in ipairs({true, false}) do
|
||||
local settings = configured(singleplayer and "local" or "staging")
|
||||
settings.hearth_m1_course, settings.hearth_m1_course_nonce = "true", "public-clock-fixture"
|
||||
settings.hearth_m1_course_snag, settings.hearth_course_token = "false", "public-clock-fixture"
|
||||
settings.hearth_preview_date, settings.hearth_preview_weather = "1782043200", "clear"
|
||||
settings.hearth_launcher_mode = "course"
|
||||
local calls = harness(settings, singleplayer)
|
||||
dofile(target); calls.deferred[1].fn()
|
||||
assert(calls.loads == 1 and calls.saves == 0 and #calls.deferred == 1)
|
||||
assert(hearth.clock.now == 1782043200 and hearth.clock.latitude == 45 and hearth.clock.longitude == 0)
|
||||
assert(hearth.weather == "clear" and hearth.course == nil and hearth.sessions == nil)
|
||||
end
|
||||
end)
|
||||
test("production allows empty date and automatic weather and advances real UTC", function()
|
||||
local settings = configured("production")
|
||||
settings.hearth_preview_date, settings.hearth_preview_weather = "", "auto"
|
||||
local calls = harness(settings, false)
|
||||
dofile(target); hearth.clock_update()
|
||||
assert(hearth.clock.now == 1800000000)
|
||||
calls.now = 1800000010; hearth.clock_update()
|
||||
assert(hearth.clock.now == 1800000010 and calls.weather.now == 1800000010)
|
||||
end)
|
||||
test("production requires configured valid coordinates even in singleplayer with a stored location", function()
|
||||
for _, singleplayer in ipairs({true, false}) do
|
||||
for _, coords in ipairs({{}, {"45"}, {"bad", "0"}, {"91", "0"}, {"0", "181"}}) do
|
||||
rejects({hearth_environment = "production", hearth_latitude = coords[1], hearth_longitude = coords[2]},
|
||||
singleplayer, "requires explicit", {latitude = 45, longitude = 0})
|
||||
end
|
||||
end
|
||||
end)
|
||||
test("dedicated staging preserves the explicit coordinate requirement", function()
|
||||
rejects({hearth_environment = "staging"}, false, "requires explicit", {latitude = 45, longitude = 0})
|
||||
end)
|
||||
test("staging accepts integer fixture dates including bounds on dedicated engines", function()
|
||||
for _, date in ipairs({"946684800", "1782043200", "1797854400", "4102444800"}) do
|
||||
local settings = configured("staging")
|
||||
settings.hearth_preview_date, settings.hearth_preview_weather = date, "rain"
|
||||
settings.hearth_launcher_mode, settings.hearth_smoke_token = "smoke", "fixture"
|
||||
local calls = harness(settings, false)
|
||||
dofile(target); hearth.clock_update()
|
||||
assert(hearth.clock.now == tonumber(date) and calls.weather.now == tonumber(date))
|
||||
assert(hearth.weather == "rain" and calls.timeofday == 0.5)
|
||||
end
|
||||
end)
|
||||
test("local fixtures and all supported weather choices work", function()
|
||||
for _, weather in ipairs({"auto", "clear", "rain"}) do
|
||||
local calls = harness({hearth_preview_date = "1782043200", hearth_preview_weather = weather}, true)
|
||||
dofile(target); hearth.clock_update()
|
||||
assert(hearth.clock.now == 1782043200 and calls.weather.now == 1782043200)
|
||||
assert(hearth.weather == (weather == "auto" and "clear" or weather))
|
||||
end
|
||||
end)
|
||||
test("nonproduction rejects fractional out of range and nonfinite dates", function()
|
||||
for _, deployment in ipairs({"local", "staging"}) do
|
||||
for _, date in ipairs({"946684799", "4102444801", "1782043200.5", "bad", "nan", "inf", "1e309", " "}) do
|
||||
local settings = configured(deployment); settings.hearth_preview_date = date
|
||||
rejects(settings, deployment == "local", "must be an integer UTC timestamp")
|
||||
end
|
||||
end
|
||||
end)
|
||||
test("nonproduction rejects invalid weather instead of ignoring it", function()
|
||||
for _, weather in ipairs({"", "snow", "Rain"}) do
|
||||
rejects({hearth_preview_weather = weather}, true, "must be auto, clear, or rain")
|
||||
end
|
||||
end)
|
||||
test("singleplayer stored location is loaded exclusively through persistence", function()
|
||||
local calls = harness({}, true, {latitude = -30, longitude = 120})
|
||||
dofile(target); hearth.clock_update()
|
||||
assert(calls.loads == 1 and calls.saves == 0 and hearth.has_location)
|
||||
assert(hearth.clock.latitude == -30 and hearth.clock.longitude == 120)
|
||||
end)
|
||||
test("configured location takes precedence but still validates stored location", function()
|
||||
local calls = harness(configured("local"), true, {latitude = -30, longitude = 120})
|
||||
dofile(target); hearth.clock_update()
|
||||
assert(calls.loads == 1 and hearth.clock.latitude == 45 and hearth.clock.longitude == 0)
|
||||
calls = harness(configured("production"), false); calls.load_error = true
|
||||
local ok = pcall(dofile, target)
|
||||
assert(not ok and calls.loads == 1 and calls.saves == 0 and #calls.deferred == 0)
|
||||
end)
|
||||
test("stored location errors never silently become neutral previews", function()
|
||||
local calls = harness({}, true); calls.load_error = true
|
||||
assert(not pcall(dofile, target))
|
||||
assert(calls.saves == 0 and #calls.deferred == 0)
|
||||
end)
|
||||
test("set_location saves before publishing and next update uses the new coordinates", function()
|
||||
local calls = harness({}, true)
|
||||
dofile(target)
|
||||
hearth.set_location(-45, 70)
|
||||
assert(calls.saves == 1 and calls.saved.latitude == -45 and calls.saved.longitude == 70)
|
||||
assert(hearth.has_location)
|
||||
hearth.clock_update(); assert(hearth.clock.latitude == -45 and hearth.clock.longitude == 70)
|
||||
end)
|
||||
test("failed location save retains unchosen and previously selected state", function()
|
||||
local calls = harness({}, true)
|
||||
dofile(target); calls.save_error = true
|
||||
assert(not pcall(hearth.set_location, 45, 10) and not hearth.has_location)
|
||||
hearth.clock_update(); assert(hearth.clock.latitude == 0 and hearth.clock.longitude == 0)
|
||||
calls.save_error = false; hearth.set_location(20, 30); calls.save_error = true
|
||||
assert(not pcall(hearth.set_location, 45, 10) and hearth.has_location)
|
||||
hearth.clock_update(); assert(hearth.clock.latitude == 20 and hearth.clock.longitude == 30)
|
||||
end)
|
||||
test("invalid location input and dedicated selection never write", function()
|
||||
local calls = harness({}, true)
|
||||
dofile(target)
|
||||
assert(not pcall(hearth.set_location, 91, 0) and calls.saves == 0)
|
||||
calls = harness(configured("staging"), false)
|
||||
dofile(target)
|
||||
assert(not pcall(hearth.set_location, 0, 0) and calls.saves == 0)
|
||||
end)
|
||||
print("PASS " .. total .. " clock tests")
|
||||
Reference in New Issue
Block a user