Files
last-hearth/tests/course_spec.lua
T
2026-11-03 12:41:09 -05:00

274 lines
15 KiB
Lua
Executable File

-- Mock-only security/geometry/telemetry checks; never starts an engine.
local target = assert(arg[1], "course.lua path required")
local total = 0
local NONCE = "12345678-1234-4234-8234-123456789abc" -- Public test fixture, not a credential.
local function test(name, fn)
local old_core, old_hearth, old_io = core, hearth, io
local ok, err = pcall(fn)
core, hearth, io = old_core, old_hearth, old_io
assert(ok, name .. ": " .. tostring(err))
total = total + 1; print("ok " .. total .. " - " .. name)
end
local function key(p) return p.x .. ":" .. p.y .. ":" .. p.z end
local function harness(options)
local h = {settings = {hearth_environment = "local", hearth_m1_course = true,
hearth_m1_course_nonce = NONCE}, storage = {}, marker = NONCE .. "\n", worldpath = "C:/isolated/course",
single = true, time = 10, commands = {}, steps = {}, players = {}, messages = {},
registered = {}, nodes = {}, writes = 0, reads = 0, emerges = 0, storage_writes = 0,
priv_checks = 0, io_reads = 0}
for k, v in pairs(options or {}) do h[k] = v end
local C = {
settings = {get = function(_, k) return h.settings[k] end,
get_bool = function(_, k) return h.settings[k] == true end},
get_us_time = function() return h.time * 1000000 end,
get_worldpath = function() return h.worldpath end,
is_singleplayer = function() return h.single end,
check_player_privs = function(name, privs)
assert(name and privs.server == true); h.priv_checks = h.priv_checks + 1
return h.privileged == true
end,
register_chatcommand = function(name, def) h.commands[name] = def end,
register_globalstep = function(fn) h.steps[#h.steps + 1] = fn end,
register_on_leaveplayer = function(fn) h.leave = fn end,
register_on_dieplayer = function(fn) h.die = fn end,
register_node = function(name, def) h.registered[name] = def end,
get_player_by_name = function(name) return h.players[name] end,
chat_send_player = function(name, msg) h.messages[#h.messages + 1] = {name, msg} end,
get_node_or_nil = function(p)
h.reads = h.reads + 1
if h.unloaded and p.x == 90 and p.y == 58 and p.z == 40 then return nil end
if h.ignore and p.x == 90 and p.y == 58 and p.z == 40 then return {name = "ignore"} end
return {name = h.nodes[key(p)] or "air"}
end,
set_node = function(p, n)
assert(p.x >= 0 and p.x <= 90 and p.y >= 48 and p.y <= 58 and p.z >= 0 and p.z <= 40)
h.writes = h.writes + 1; assert(h.writes < 45000, "Unbounded generation")
h.nodes[key(p)] = n.name
end,
emerge_area = function(a, b, cb)
assert(a.x == 0 and a.y == 48 and a.z == 0 and b.x == 90 and b.y == 58 and b.z == 40)
h.emerges = h.emerges + 1; h.callback = cb
end,
}
local H = {players = {}, sessions = {}, texture = function(color) return "texture:" .. color end,
storage = {get_string = function(_, k) return h.storage[k] or "" end,
set_string = function(_, k, v) h.storage_writes = h.storage_writes + 1; h.storage[k] = v end},
feedback = function(_, msg) h.feedback = msg end}
local fakeio = {open = function(path, mode)
assert(path == h.worldpath:gsub("\\", "/"):gsub("/+$", "") .. "/.hearth-m1-course-nonce" and mode == "rb")
if h.marker == false then return nil end
return {read = function(_, size) assert(size == 65); h.io_reads = h.io_reads + 1; return h.marker:sub(1, size) end,
close = function() h.closed = true end}
end}
function h.load()
core, hearth, io = C, H, fakeio
h.module = dofile(target); return h.module
end
function h.player(name)
local p = {name = name or "alice", hp = 20, pos = {x = 0, y = 0, z = 0}, grants = 0, teleports = 0}
function p:is_player() return true end
function p:get_player_name() return self.name end
function p:get_hp() return self.hp end
function p:get_pos() return self.pos end
function p:set_hp(n) self.hp = n end
function p:set_pos(pos) self.pos = pos; self.teleports = self.teleports + 1 end
function p:set_velocity(v) assert(v.x == 0 and v.y == 0 and v.z == 0) end
function p:get_inventory()
return {set_list = function(_, list, items)
assert(list == "main"); self.grants = self.grants + 1; self.kit = items
end}
end
h.players[p.name] = p; H.players[p.name] = {food = 13, water = 12}; H.sessions[p.name] = {}
return p, H.sessions[p.name]
end
function h.command(name, param) return h.commands.course.func(name or "alice", param or "") end
function h.complete() h.callback({x = 0, y = 0, z = 0}, 4, 0) end
function h.tick(dt)
h.time = h.time + dt; for _, step in ipairs(h.steps) do step(dt) end
end
function h.node(x, y, z) return h.nodes[key({x = x, y = y, z = z})] end
h.C, h.H = C, H
return h
end
test("default boot is inert before join and generation requires opt-in", function()
local h = harness(); h.load()
assert(h.storage.m1_course_world_nonce == NONCE and h.closed and h.io_reads == 1)
assert(h.emerges == 0 and h.writes == 0)
assert(not h.command()); assert(h.emerges == 0)
h.player(); h.tick(1); assert(h.emerges == 0 and h.writes == 0)
end)
test("disabled setting cannot enroll or generate even with matching marker", function()
local h = harness(); h.settings.hearth_m1_course = false; h.load(); h.player()
assert(not h.command() and h.storage_writes == 0 and h.io_reads == 0 and h.emerges == 0)
end)
test("production hard-denies server admins and never touches marker or storage", function()
local h = harness({privileged = true}); h.settings.hearth_environment = "production"
h.load(); h.player(); assert(not h.command())
assert(h.writes == 0 and h.storage_writes == 0 and h.io_reads == 0 and h.emerges == 0)
end)
test("invalid and dedicated-local deployments fail closed", function()
for _, env in ipairs({"typo", "local"}) do
local h = harness({single = false, privileged = true}); h.settings.hearth_environment = env
h.load(); h.player(); assert(not h.command() and h.storage_writes == 0)
end
end)
test("staging explicitly checks server privilege at command and completion", function()
local h = harness({single = false}); h.settings.hearth_environment = "staging"
h.load(); local p = h.player(); assert(not h.command() and h.priv_checks > 0)
h.privileged = true; assert(h.command()); assert(h.emerges == 1 and p.grants == 0)
h.privileged = false; h.complete(); assert(h.writes == 0 and p.grants == 0)
end)
test("missing environment defaults to production for a dedicated server", function()
local h = harness({single = false, privileged = true}); h.settings.hearth_environment = nil
h.load(); h.player(); assert(not h.command() and h.storage_writes == 0)
end)
test("settings alone cannot bless normal world names", function()
for _, path in ipairs({"C:/worlds/last_hearth", "C:/worlds/course-not-really"}) do
local h = harness({worldpath = path}); h.load(); h.player()
assert(not h.command() and h.io_reads == 0 and h.storage_writes == 0)
end
end)
test("missing, mismatched and oversized world marker files deny enrollment", function()
for _, marker in ipairs({false, "wrong", NONCE .. string.rep("x", 100)}) do
local h = harness({marker = marker}); h.load(); h.player()
assert(not h.command() and h.storage_writes == 0 and h.emerges == 0)
end
end)
test("malformed nonce fails without file access", function()
local h = harness(); h.settings.hearth_m1_course_nonce = "../course"
h.load(); h.player(); assert(not h.command() and h.io_reads == 0)
end)
test("saved spawn, claims, location or migration backups prevent first enrollment", function()
for _, k in ipairs({"spawn", "claims", "location", "m0_backup:spawn", "m0_backup:claims", "m0_backup:location"}) do
local h = harness(); h.storage[k] = "{}"; h.load(); h.player()
assert(not h.command() and h.storage_writes == 0 and h.emerges == 0)
end
end)
test("matching enrolled world may restart after normal spawn save, mismatched storage denies", function()
local h = harness(); h.storage.m1_course_world_nonce = NONCE; h.storage.spawn = "saved"
h.load(); h.player(); assert(h.command() and h.storage_writes == 0)
local other = harness(); other.storage.m1_course_world_nonce = "other"
other.load(); other.player(); assert(not other.command() and other.storage_writes == 0)
end)
test("loading after a player session exists cannot enroll an old world", function()
local h = harness(); h.player(); h.load(); assert(not h.command() and h.storage_writes == 0)
end)
test("no join kit or teleport before all emergence callbacks finish", function()
local h = harness(); h.load(); local p = h.player(); assert(h.command())
h.callback({}, 4, 1); assert(h.writes == 0 and p.grants == 0 and p.teleports == 0)
h.complete(); assert(h.writes > 41041 and p.grants == 1 and p.teleports == 1)
assert(p.pos.x == 5 and p.pos.y == 50.5 and p.pos.z == 5 and h.H.players.alice.food == 100)
end)
test("generation is single-instance across opt-ins, retries and duplicate callbacks", function()
local h = harness(); h.load(); local a, b = h.player("alice"), h.player("bob")
assert(h.command("alice") and h.command("bob") and h.emerges == 1)
h.complete(); local writes = h.writes; h.complete()
assert(a.grants == 1 and b.grants == 1 and h.writes == writes)
assert(h.command("alice", "retry")); assert(h.emerges == 1 and h.writes == writes and a.grants == 2)
assert(dofile(target) == h.module and #h.steps == 1)
end)
test("loaded preflight fails before the first write", function()
for _, mode in ipairs({"unloaded", "ignore"}) do
local h = harness(); h[mode] = true; h.load(); local p = h.player()
assert(h.command()); h.complete(); assert(h.writes == 0 and p.grants == 0)
assert(not h.command())
end
end)
test("cancelled or errored emergence does not generate or grant", function()
for action = 0, 1 do
local h = harness(); h.load(); local p = h.player(); h.command()
h.callback({}, action, 1); h.complete(); assert(h.writes == 0 and p.grants == 0)
end
end)
test("revoked settings and storage identity prevent deferred writes", function()
for _, mode in ipairs({"production", "disabled", "nonce", "storage"}) do
local h = harness(); h.load(); local p = h.player(); h.command()
if mode == "production" then h.settings.hearth_environment = "production"
elseif mode == "disabled" then h.settings.hearth_m1_course = false
elseif mode == "nonce" then h.settings.hearth_m1_course_nonce = "changed"
else h.storage.m1_course_world_nonce = "changed" end
h.complete(); assert(h.writes == 0 and p.grants == 0)
end
end)
test("disconnect/rejoin cannot inherit deferred consent or a kit", function()
local h = harness(); h.load(); local old = h.player(); h.command()
local stale_callback = h.callback
h.leave(old); local new = h.player(); h.complete()
assert(h.writes == 0 and old.grants == 0 and new.grants == 0)
assert(h.command()); stale_callback({}, 4, 0)
assert(h.writes == 0 and new.grants == 0)
h.complete(); assert(h.writes > 0 and new.grants == 1)
end)
test("death, stale sessions and pending arrival invalidate requests", function()
local h = harness(); h.load(); local p, session = h.player()
session.waiting = true; assert(not h.command()); session.waiting = false
p.hp = 0; assert(not h.command()); p.hp = 20
h.command(); h.H.sessions.alice = {}; h.complete(); assert(h.writes == 0 and p.grants == 0)
h.command(); h.die(p); h.complete(); assert(h.writes == 0)
end)
test("terrain geometry has bounded floor, half steps, doorway, pool, falls and resources", function()
local h = harness(); h.load(); h.player(); h.command(); h.complete()
assert(h.reads == 41041 and h.writes < 45000)
local half = h.registered["hearth:m1_course_half_step"]
assert(half.node_box.fixed[2] == -0.5 and half.node_box.fixed[5] == 0)
assert(h.node(11,51,5) == "hearth:m1_course_half_step" and h.node(12,51,5) == "hearth:planks")
assert(h.node(26,51,5) == "air" and h.node(26,52,5) == "air" and h.node(26,53,5) == "hearth:wall")
assert(h.node(26,51,4) == "hearth:wall" and h.node(26,51,6) == "hearth:wall")
assert(h.node(39,48,10) == "hearth:stone" and h.node(39,49,10) == "hearth:water_source")
assert(h.node(34,49,10) == "hearth:stone" and h.node(34,50,10) == "hearth:water_source")
assert(h.node(48,52,7) == "hearth:planks" and h.node(49,51,7) == "air")
assert(h.node(60,54,7) == "hearth:planks" and h.node(61,51,7) == "air")
assert(h.node(70,51,10) == "hearth:log" and h.node(70,51,12) == "hearth:loose_stone")
assert(h.node(82,50,33) == "hearth:planks" and h.node(82,51,33) == "air")
assert(h.node(29,52,9) == "air")
end)
test("optional snag fixture is explicit and bounded", function()
local h = harness(); h.settings.hearth_m1_course_snag = true
h.load(); h.player(); h.command(); h.complete(); assert(h.node(29,52,9) == "hearth:roof")
end)
test("staged kit is limited to disposable course and known content names", function()
local h = harness(); h.load(); local p = h.player(); h.command(); h.complete()
assert(#p.kit == 8 and p.kit[1] == "hearth:axe" and p.kit[2] == "hearth:knife" and p.kit[3] == "hearth:spear")
assert(p.kit[4] == "hearth:planks 64" and p.kit[5] == "hearth:wall 64" and p.kit[6] == "hearth:roof 64")
end)
test("checkpoints progress in order within five nodes, never from mere elapsed time", function()
local h = harness(); h.load(); local p = h.player(); h.command(); h.complete()
p.pos = {x = 26, y = 50.5, z = 5}; h.tick(1)
assert(h.module.report("alice"):find("checkpoints 0/10", 1, true))
p.pos = {x = 20.01, y = 51, z = 5}; h.tick(0.1)
assert(h.module.report("alice"):find("checkpoints 0/10", 1, true))
p.pos = {x = 20, y = 51, z = 5}; h.tick(0.1)
assert(h.module.report("alice"):find("checkpoints 1/10", 1, true))
local points = {{26,50.5,5},{39,49.5,10},{50,50.5,7},{62,50.5,7},{71,50.5,10},
{79,50.5,20},{82,50.5,33},{60,50.5,33},{8,50.5,33}}
for _, v in ipairs(points) do p.pos = {x=v[1],y=v[2],z=v[3]}; h.tick(0.2) end
local result = h.module.report("alice"); assert(result:find("complete",1,true) and result:find("checkpoints 10/10",1,true))
h.tick(10); assert(h.module.report("alice") == result)
end)
test("telemetry uses bounded real step samples with honest p95 labels and retry reset", function()
local h = harness(); h.load(); h.player(); h.command(); h.complete()
for _ = 1, 650 do h.tick(0.02) end
local ok, report = h.command("alice", "report")
assert(ok and report:find("p95 20.00ms (last 600 samples)",1,true))
assert(report:find("Not client FPS or CPU profiling",1,true) and report:find("p95 0.00us",1,true))
h.command("alice", "retry"); report = h.module.report("alice")
assert(report:find("0.0s; checkpoints 0/10",1,true) and report:find("last 0 samples",1,true))
for _, dt in ipairs({0/0, math.huge, -1, 0}) do h.steps[1](dt) end
assert(h.module.report("alice") == report)
end)
test("leave/death discard telemetry; inactive players get no sampling or grants", function()
local h = harness(); h.load(); local a, b = h.player("alice"), h.player("bob")
h.command(); h.complete(); h.tick(1)
assert(b.grants == 0 and h.module.report("bob"):find("No course run",1,true))
h.die(a); assert(h.module.report("alice"):find("No course run",1,true))
h.command(); h.leave(a); assert(h.module.report("alice"):find("No course run",1,true))
end)
test("session count and run duration are bounded", function()
local h = harness(); h.load()
for i = 1, 16 do h.player("p"..i); assert(h.command("p"..i)) end
h.player("overflow"); assert(not h.command("overflow")); h.complete()
h.tick(1801); assert(h.module.report("p1"):find("No course run",1,true))
assert(h.command("overflow"))
end)
print("PASS " .. total .. " course checks")