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