-- Focused injected reader tests; no engine, network or metadata required. local target=assert(arg[1],"species_reader.lua path required") local path=target:match("^(.*[/\\])") or "" local factory=dofile(target) local total=0 local function test(name,fn) local ok,err=pcall(fn); assert(ok,name..": "..tostring(err)) total=total+1; print("PASS "..name) end local function forbidden() error("Reader attempted a side effect") end local screens, hits, node, reads, rays={}, {}, {name="hearth:oak_log"}, 0, 0 local H={sessions={alice={}},players={alice={food=73,water=61}},clock={season="Autumn"}} H.botany=dofile(path.."botany.lua")(H,{}) H.wiki_articles=dofile(path.."wiki_articles.lua") H.species_effects=dofile(path.."species_effects.lua")(H,{}) local api={set_node=forbidden,swap_node=forbidden,get_meta=forbidden, emerge_area=forbidden,forceload_block=forbidden,request_http_api=forbidden, show_url=forbidden,register_chatcommand=forbidden,register_on_player_receive_fields=forbidden} function api.formspec_escape(s) return (s:gsub("\\","\\\\"):gsub("%[","\\["):gsub("%]","\\]"):gsub(";","\\;"):gsub(",","\\,")) end function api.show_formspec(name,form,spec) screens[#screens+1]={name=name,form=form,spec=spec} end function api.get_node_or_nil() reads=reads+1; return node end local eye_height=1.6 local player={get_player_name=function() return "alice" end, get_pos=function() return {x=0,y=0,z=0} end, get_properties=function() return {eye_height=eye_height} end, get_look_dir=function() return {x=0,y=0,z=2} end, get_meta=forbidden,set_pos=forbidden,get_inventory=forbidden} local function hit(z,name) return {type="node",under={x=0,y=2,z=z},intersection_point={x=0,y=1.6,z=z},name=name} end function api.raycast(start,finish,objects,liquids) rays=rays+1 assert(start.x==0 and start.y==1.6 and start.z==0,"not eye origin") assert(finish.y==start.y and finish.z==6,"reach not normalized to six") assert(objects==false and liquids==false) local i=0 return function() i=i+1; assert(i<=3,"unbounded scan"); return hits[i] end end local R=factory(H,api) local function latest() return screens[#screens].spec end test("factory installs no global callbacks or API mutations",function() assert(type(R.show)=="function" and type(R.look)=="function" and type(R.resolve)=="function" and type(R.handle)=="function") assert(api.set_node==forbidden and H.species_reader==nil) end) test("exact IDs and canonical Latin resolve without fuzzy URL fallback",function() assert(R.resolve("oak").latin=="Quercus robur") assert(R.resolve(" Quercus robur ").id=="oak") for _,v in ipairs({"Quercus","quercus robur","Pedunculate oak","hearth:oak_log","https://en.wikipedia.org/wiki/Oak",string.rep("x",121),42}) do assert(not R.resolve(v)) end assert(not R.resolve(nil)) end) test("actual botanical seasonal stages logs leaves and copy API resolve",function() local S=dofile(path.."seasons.lua")(H,{}) H.seasons=S -- The real diversity factory installs the existing seasonal tree mappings. H.diversity=dofile(path.."diversity.lua")(H,{}) for _,v in ipairs({"hearth:oak_seedling","hearth:oak_young","hearth:oak_mature","hearth:oak_log","hearth:oak_leaves","hearth:oak_leaves_winter","hearth:clover_winter"}) do local r=H.botany.get(v); assert(r,v) assert(R.show(player,r.id)) end local r=R.resolve("oak"); r.common="changed"; assert(R.resolve("oak").common~="changed") end) test("ray uses normalized six-block eye reach and loaded-node read only",function() hits={hit(6)}; node={name="hearth:oak_log"}; reads=0 assert(R.look(player).id=="oak" and reads==1) end) test("ray rejects hits beyond reach without node reads",function() hits={hit(6.01)}; reads=0; assert(not R.look(player) and reads==0) end) test("unknown and unloaded first surfaces cannot be inspected through",function() hits={hit(2),hit(3)} for _,v in ipairs({false,{name="ignore"},{name="hearth:stone"},{name="foreign:oak_log"}}) do node=v or nil; reads=0; assert(not R.look(player) and reads==1) end end) test("empty ray and invalid hit positions do not read map",function() hits={}; reads=0; assert(not R.look(player) and reads==0) hits={{type="node",under={x=0,y=0,z=0}}}; assert(not R.look(player) and reads==0) hits={hit(3)}; hits[1].under.x=0/0; assert(not R.look(player) and reads==0) eye_height=math.huge; local n=rays; assert(not R.look(player) and rays==n); eye_height=1.6 end) test("all hundred entries render with honest ninety-eight excerpt and two note headings",function() local excerpts,notes=0,0 for _,r in ipairs(H.botany.list()) do assert(R.show(player,r.id)); local a=H.wiki_articles[r.id] assert(latest():find(api.formspec_escape(r.latin),1,true)) if a.kind=="wikipedia_intro" then excerpts=excerpts+1; assert(latest():find("Wikipedia excerpt (offline)",1,true)) else notes=notes+1 assert(r.id=="old_mans_beard" or r.id=="cup_lichen") assert(latest():find("No Wikipedia article cached (offline)",1,true)) assert(not latest():find("Wikipedia excerpt (offline)",1,true)) assert(latest():find("CC0",1,true) and not latest():find("CC BY-SA 4.0",1,true)) assert(not a.revision and latest():find(api.formspec_escape(a.text),1,true)) end end assert(excerpts==98 and notes==2) end) test("excerpt retains full shipped introduction and attribution revision history license",function() assert(R.show(player,"oak")); local a=H.wiki_articles.oak for _,key in ipairs({"title","text","attribution","license","license_url","url","revision_url","history_url","revision_timestamp","retrieved","changes"}) do assert(latest():find(api.formspec_escape(a[key]),1,true),key) end assert(latest():find("not the full wiki",1,true) and latest():find("not",1,true)) assert(latest():find("medical advice",1,true)) end) test("every shipped introduction fits and is displayed in full",function() for _,r in ipairs(H.botany.list()) do assert(R.show(player,r.id)) assert(latest():find(api.formspec_escape(H.wiki_articles[r.id].text),1,true),r.id) end end) test("current effects use actual describe records with simulated descriptive separation",function() assert(R.show(player,"honey_fungus",2)) assert(latest():find("SIMULATED",1,true) and latest():find("DESCRIPTIVE (not simulated)",1,true)) assert(latest():find("no infection or attack",1,true)) assert(R.show(player,"alder",2)) assert(latest():find("90 real calendar days",1,true) and latest():find("Frankia",1,true)) assert(latest():find("not measured Wikipedia constants",1,true)) end) test("article textarea and provenance are unnamed read-only copyable fields",function() assert(R.show(player,"oak")) local count=0 for body in latest():gmatch("textarea%[([^\n]-);;;") do count=count+1 end assert(count==3 and not latest():find("field[",1,true)) assert(latest():find("URLs are copyable",1,true)) end) test("forged IDs source URLs text and pages never replace session article",function() assert(R.show(player,"oak")) local before=#screens assert(R.handle(player,{id="pine",url="javascript:boom",text="replacement",page="999",reader_tab="99"})) assert(#screens==before and H.sessions.alice.species_reader.id=="oak") R.handle(player,{reader_tab="2",id="pine"}); assert(H.sessions.alice.species_reader.id=="oak") R.handle(player,{reader_tab="1",text="replacement"}); assert(latest():find(api.formspec_escape(H.wiki_articles.oak.text),1,true)) end) test("malicious cached text source and URLs are plain escaped inert data",function() local a=H.wiki_articles.oak local payload="];button[0,0;1,1;attack;Injected]" H.wiki_articles.oak={kind="wikipedia_intro",text=payload,url=payload,attribution=payload,license=payload} assert(R.show(player,"oak")) assert(latest():find(api.formspec_escape(payload),1,true) and not latest():find(";attack;Injected]",1,true)) assert(not latest():find("hypertext[",1,true) and not latest():find("button_url[",1,true)) H.wiki_articles.oak=a end) test("Unicode body caps at eight thousand with four-thousand character pages",function() local a=H.wiki_articles.oak local char=string.char(195,169) H.wiki_articles.oak={kind="wikipedia_intro",text=string.rep(char,9000)} assert(R.show(player,"oak")) assert(latest():find(string.rep(char,4000),1,true) and not latest():find(string.rep(char,4001),1,true)) assert(latest():find("display limited to 8000",1,true)) R.handle(player,{reader_next=true}); assert(H.sessions.alice.species_reader.page==2) assert(latest():find(string.rep(char,4000),1,true)) R.handle(player,{reader_next=true}); assert(H.sessions.alice.species_reader.page==2) R.handle(player,{reader_previous=true}); assert(H.sessions.alice.species_reader.page==1) assert(#latest()<20000) H.wiki_articles.oak=a end) test("absent optional catalogue sources and effects are explicit not counterfeit",function() local B,A,E=H.botany,H.wiki_articles,H.species_effects H.wiki_articles=nil; assert(R.show(player,"oak")); assert(latest():find("No article is available",1,true)) H.species_effects=nil; assert(R.show(player,"oak",2)); assert(latest():find("Effect descriptions unavailable",1,true)) H.botany=nil; assert(not R.resolve("oak") and not R.show(player,"oak")) H.botany,H.wiki_articles,H.species_effects=B,A,E end) test("player close confirmation clears reader but changes no gameplay or persistent data",function() local needs=H.players.alice assert(R.show(player,"oak")); local old=H.sessions.alice.species_reader R.handle(player,{text="modified"}); assert(H.sessions.alice.species_reader==old) R.handle(player,{quit=true}); assert(not H.sessions.alice.species_reader) assert(H.players.alice==needs and needs.food==73 and needs.water==61) assert(not R.handle(player,{reader_next=true})) end) test("missing session rejects forms and forged follow-up packets",function() local s=H.sessions.alice; H.sessions.alice=nil; local n=#screens assert(not R.show(player,"oak") and not R.handle(player,{reader_tab="2"})) assert(#screens==n); H.sessions.alice=s end) test("actual feedback HUD hints known species with hand or building and fails closed unloaded",function() local saved_core,saved_hearth=core,hearth local ok,err=pcall(function() local t,wield_name,message=1,"","" local s={} hearth={sessions={alice=s},botany=H.botany,movement={delta=function(d) return d end}, construction={preview=function() return true,"ok" end}} core={get_us_time=function() return t*1000000 end, register_chatcommand=function() end,raycast=function(start,finish) assert(start.y==1.6 and finish.z==5) local i=0; return function() i=i+1; return hits[i] end end,get_node_or_nil=api.get_node_or_nil, get_meta=forbidden,set_node=forbidden,swap_node=forbidden,sound_play=forbidden, get_dig_params=function() return {diggable=true} end, registered_nodes={["hearth:oak_log"]={description="Oak log",groups={tree=1}}, ["hearth:stone"]={description="Stone",groups={stone=1}}, ["hearth:wall"]={description="Wall",groups={wood=1}}}} local p={get_player_name=player.get_player_name,get_pos=player.get_pos, get_properties=player.get_properties,get_look_dir=function() return {x=0,y=0,z=1} end, get_hp=function() return 20 end,get_velocity=function() return {x=0,y=0,z=0} end, get_player_control=function() return {} end,set_animation=function() end, hud_add=function(_,def) message=def.text; return 1 end, hud_change=function(_,_,key,value) if key=="text" then message=value end end} function p:get_wielded_item() return {get_name=function() return wield_name end,get_wear=function() return 0 end, get_tool_capabilities=function() return {} end} end dofile(path.."feedback.lua") hits={hit(3)}; node={name="hearth:oak_log"} hearth.feedback_step(p,0); assert(message:find("/inspect species",1,true)) wield_name="hearth:wall"; t=2 hearth.feedback_step(p,0) assert(message:find("Ready to place",1,true) and message:find("/inspect species",1,true)) wield_name=""; node={name="hearth:stone"}; t=3 hearth.feedback_step(p,0); assert(not message:find("/inspect",1,true)) node=nil; t=4; hearth.feedback_step(p,0); assert(message=="") end) core,hearth=saved_core,saved_hearth assert(ok,err) end) print("PASS "..total.." species reader tests")