Files
last-hearth/tests/species_effects_spec.lua
T
2026-11-05 09:57:11 -05:00

106 lines
6.2 KiB
Lua
Executable File

-- Standalone Lua 5.1+; policy/source checks, no engine and no network.
local target=assert(arg[1],"species_effects.lua path required")
local root=target:gsub("[^/\\]+$","")
local hearth={}
hearth.lifecycle=dofile(root.."lifecycle.lua")
hearth.botany=dofile(root.."botany.lua")(hearth,{})
hearth.wiki_articles=dofile(root.."wiki_articles.lua")
local E=dofile(target)(hearth,setmetatable({}, {__index=function(_,k) error("unexpected API access: "..k) end}))
local passed=0
local function test(label,fn) fn(); passed=passed+1; print("ok "..passed.." - "..label) end
local function near(a,b) assert(math.abs(a-b)<0.000001,tostring(a).." != "..tostring(b)) end
local function entry(value,title)
for _,row in ipairs(E.describe(value)) do if row.title==title then return row end end
error("missing effect "..title)
end
test("all100 identity sources are mapped without claiming unreviewed trait evidence",function()
local sources=E.sources(); local total=0
for _,r in ipairs(hearth.botany.list()) do
local s=assert(sources[r.id]); total=total+1
assert(s.source_species==r.latin and s.source_url:match("^https://en%.wikipedia%.org/wiki/"))
local article=hearth.wiki_articles[r.id]
assert(s.identity_revision==(article and article.revision))
assert(s.identity_revision_url==(article and article.revision_url))
if not s.reviewed then assert(s.note:find("no species%-effect evidence reviewed")) end
for _,row in ipairs(E.describe(r)) do
assert(row.status=="simulated" or row.status=="descriptive")
assert(row.game~="" and row.source_species==r.latin and row.source_url==s.source_url)
end
end
assert(total==100)
assert(E.source("unknown")==nil and #E.describe({id="unknown",tags={engineer=true}})==0)
end)
test("reviewed mechanisms cite species and explicitly identify broader evidence",function()
for _,id in ipairs({"alder","clover","dog_lichen","sphagnum","duckweed","birch_polypore","fly_agaric"}) do
local s=E.source(id); assert(s.reviewed and s.reviewed_on=="2026-11-03" and s.evidence_url)
end
assert(E.source("sphagnum").evidence_url:match("/Sphagnum$"))
assert(E.source("duckweed").evidence_url:match("/Lemnoideae$"))
assert(E.source("dog_lichen").evidence_url:match("/Peltigera$"))
assert(entry("fly_agaric","ectomycorrhiza").game:find("debits at most 1 existing soil nutrient"))
end)
test("descriptors do not silently become engineer, parasite, resin or poison mechanics",function()
for _,pair in ipairs({{"willow","engineer"},{"bracken","allelopathy"},{"honey_fungus","parasite"},{"pine","resin"},{"foxglove","toxicity"}}) do
assert(entry(pair[1],pair[2]).status=="descriptive")
end
for _,id in ipairs({"alder","clover","dog_lichen"}) do assert(entry(id,"nitrogenfixer").status=="simulated") end
assert(entry("pine","canopy").game:find("15%%"))
assert(entry("birch_polypore","saprotroph").status=="simulated")
assert(entry("yarrow","pollinator").status=="simulated")
end)
test("sphagnum changes only a fresh local land estimate with two-mat quantitative ceilings",function()
local h={loaded=true,water=0,aquatic="bank",moisture=0.65,ph=6.5,light=10,nutrients=3,nested={value=1}}
local out=E.habitat(h,{sphagnum=10000,duckweed=10000,willow=10000})
near(out.moisture,0.85); near(out.ph,6.1); assert(out.light==10 and out.nutrients==3)
assert(h.ph==6.5 and h.moisture==0.65 and out.source_ph==6.5 and out.source_moisture==0.65)
out.nested.value=2; assert(h.nested.value==1)
for _=1,20 do near(E.habitat(h,{sphagnum=2}).ph,6.1) end
out=E.habitat({loaded=true,water=0,moisture=0.99,ph=3.6},{sphagnum=2})
assert(out.moisture==1 and out.ph==3.5)
local cowslip={loaded=true,ground="grass",substrate="grass",water=0,aquatic="land",light=12,moisture=0.3,ph=6}
assert(hearth.botany.suitable("cowslip",cowslip,"Spring"))
assert(not hearth.botany.suitable("cowslip",E.habitat(cowslip,{sphagnum=2}),"Spring"),"acidification changes real niche eligibility")
end)
test("floating mats shade only submerged freshwater, with a six-light ceiling and no brightening",function()
for _,base in ipairs({15,7,3,1,0}) do
local h={loaded=true,water=2,aquatic="submerged",freshwater=true,light=base,moisture=1,ph=6.5}
local out=E.habitat(h,{duckweed=10000,sphagnum=10000})
assert(out.light==base-math.min(6,math.max(0,base-2)) and h.light==base)
assert(out.water==2 and out.ph==6.5 and out.moisture==1)
end
for _,mode in ipairs({"surface","land","salt"}) do
local h={loaded=true,water=mode=="land" and 0 or 2,aquatic=mode=="salt" and "submerged" or mode,freshwater=mode~="salt",light=10}
assert(E.habitat(h,{duckweed=4}).light==10)
end
end)
test("unknown, malformed and unloaded observations grant no effects",function()
local h={loaded=true,water=0,moisture=0.5,ph=6.5,light=10}
for _,n in ipairs({-1,math.huge,0/0,"2",false}) do
local out=E.habitat(h,{sphagnum=n,duckweed=n}); assert(out.ph==h.ph and out.moisture==h.moisture)
end
assert(E.habitat(h,{unknown=999,alder=999}).ph==6.5)
h.loaded=false; assert(E.habitat(h,{sphagnum=2}).ph==6.5)
assert(E.habitat(nil)==nil)
end)
test("source and description results cannot mutate the reviewed policy",function()
local row=entry("sphagnum","engineer"); row.status="descriptive"; row.source_url="wrong"
assert(entry("sphagnum","engineer").status=="simulated")
local sources=E.sources(); sources.alder.note="wrong"
assert(E.source("alder").note~="wrong")
end)
test("effect text matches calendar policy without laundering identity references into timing evidence",function()
assert(entry("clover","nitrogenfixer").game:find("90 real calendar days",1,true))
local fungus=entry("fly_agaric","ectomycorrhiza").game
assert(fungus:find("No accelerated growth",1,true) and not fungus:find("16200",1,true))
assert(entry("birch_polypore","saprotroph").game:find("90 real days",1,true))
assert(entry("bilberry","pollinator").game:find("at most one extra berry",1,true))
for _,r in ipairs(hearth.botany.list()) do
local text=entry(r.id,"lifecycle").game
assert(text:find("source_reported, policy_fallback or UNKNOWN",1,true))
assert(text:find("unloaded time",1,true) and text:find("never authorize automatic death",1,true))
assert(not text:find("15 minutes",1,true))
end
end)
assert(passed==8)
print("LAST_HEARTH_SPECIES_EFFECTS_PASS "..passed)