-- lib-core.inventory-list-test — v0.1.0 -- 12 assertions: 1 engine spike + 11 spec assertions (B.2 test-spec). -- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.* local composition = require("lib-core.composition") local inventory = require("lib-core.inventory-list") local T = engine.test local M = {} -- entity.get_children() returns fresh userdata wrappers on each call; -- there is no __eq on entity userdata, so == is Lua-pointer equality -- (always false for different wrappers of the same engine entity). -- Compare by composition.reg_id (stable property on all composition -- entities) instead. local function same_entity(a, b) local ra = a and a:get_property("composition.reg_id") local rb = b and b:get_property("composition.reg_id") if ra and ra ~= 0 and rb and rb ~= 0 then return ra == rb end -- Fallback for raw entities: pointer equality (may be unreliable). return a == b end -- ---------------------------------------------------------------- -- Template definitions (shared across tests) -- All template names are unique within this test run. -- ---------------------------------------------------------------- -- Container template: kind="list", no constraints. composition.define_template{ id = "inv_test.container", properties = { sprite_path = "sprites/bag.png", position = {x = 0, y = 0}, }, tags = {"renderable"}, container = { kind = "list" }, } -- Item template: stack_mode="individual", has "item" tag + "renderable" tag. composition.define_template{ id = "inv_test.sword", properties = { name = "Iron Sword", stack_mode = "individual", sprite_path = "sprites/sword.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } -- A second item template for multi-item tests. composition.define_template{ id = "inv_test.potion", properties = { name = "Health Potion", stack_mode = "individual", sprite_path = "sprites/potion.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } -- Non-item template (no stack_mode property). composition.define_template{ id = "inv_test.sign", properties = { text = "a sign", sprite_path = "sprites/sign.png", position = {x = 0, y = 0}, }, tags = {"renderable"}, } -- Stack-item template (stack_mode="stack" — not supported in v0.1). composition.define_template{ id = "inv_test.gold_coin", properties = { name = "Gold Coin", stack_mode = "stack", sprite_path = "sprites/coin.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } -- Non-container template (no container block). composition.define_template{ id = "inv_test.plain_entity", properties = { name = "plain", }, } function M.run_tests(ctx) -- ================================================================ -- Spike: engine.entity supports multiple children on distinct slots -- (locks the invariant that synthetic item. naming is safe) -- ================================================================ local spike_parent = entity.create() local spike_a = entity.create() local spike_b = entity.create() local spike_c = entity.create() entity.attach(spike_parent, "a", spike_a) entity.attach(spike_parent, "b", spike_b) entity.attach(spike_parent, "c", spike_c) local spike_children = spike_parent:get_children() local spike_n = 0 for _ in pairs(spike_children) do spike_n = spike_n + 1 end T.assert(spike_n == 3 and spike_children.a ~= nil and spike_children.b ~= nil and spike_children.c ~= nil, "spike: engine.entity supports 3 children on distinct slot names") -- ================================================================ -- Assertion 1: add(container, item) → contains returns true -- ================================================================ local bag1 = composition.create{ template = "inv_test.container" } local sword1 = composition.create{ template = "inv_test.sword" } inventory.add(bag1, sword1) T.assert(inventory.contains(bag1, sword1), "1. add(container, item) → contains(container, item) is true") -- ================================================================ -- Assertion 2: after add, count == 1 and contents contains item -- ================================================================ T.assert(inventory.count(bag1) == 1, "2a. after add: count(container) == 1") local contents2 = inventory.contents(bag1) local found_sword1 = false for _, it in ipairs(contents2) do if same_entity(it, sword1) then found_sword1 = true; break end end T.assert(found_sword1, "2b. after add: contents(container) contains item") -- ================================================================ -- Assertion 3: two adds of different items → count == 2, -- both in contents, different engine slot names -- ================================================================ local bag3 = composition.create{ template = "inv_test.container" } local sword3 = composition.create{ template = "inv_test.sword" } local potion3 = composition.create{ template = "inv_test.potion" } inventory.add(bag3, sword3) inventory.add(bag3, potion3) T.assert(inventory.count(bag3) == 2, "3a. two adds → count == 2") local contents3 = inventory.contents(bag3) local found3_s = false local found3_p = false for _, it in ipairs(contents3) do if same_entity(it, sword3) then found3_s = true end if same_entity(it, potion3) then found3_p = true end end T.assert(found3_s and found3_p, "3b. two adds → both items in contents") -- Verify no slot collision (different slot names). local children3 = bag3:get_children() local slots3 = {} for slot, _ in pairs(children3) do table.insert(slots3, slot) end T.assert(#slots3 == 2 and slots3[1] ~= slots3[2], "3c. two adds → different engine slot names (no collision)") -- ================================================================ -- Assertion 4: remove → contains false, count 0 -- ================================================================ local bag4 = composition.create{ template = "inv_test.container" } local sword4 = composition.create{ template = "inv_test.sword" } inventory.add(bag4, sword4) inventory.remove(bag4, sword4) T.assert(not inventory.contains(bag4, sword4), "4a. remove → contains(container, item) is false") T.assert(inventory.count(bag4) == 0, "4b. remove → count == 0") -- ================================================================ -- Assertion 5: add to entity without container-block → Loud-Error -- ================================================================ local plain5 = composition.create{ template = "inv_test.plain_entity" } local sword5 = composition.create{ template = "inv_test.sword" } local ok5 = pcall(inventory.add, plain5, sword5) T.assert(not ok5, "5. add to entity without container block → Loud-Error") -- ================================================================ -- Assertion 6: add of entity without stack_mode → Loud-Error -- ================================================================ local bag6 = composition.create{ template = "inv_test.container" } local sign6 = composition.create{ template = "inv_test.sign" } local ok6 = pcall(inventory.add, bag6, sign6) T.assert(not ok6, "6. add entity without stack_mode (not an item) → Loud-Error") -- ================================================================ -- Assertion 7: add item with stack_mode="stack" → Loud-Error -- ================================================================ local bag7 = composition.create{ template = "inv_test.container" } local coin7 = composition.create{ template = "inv_test.gold_coin" } local ok7 = pcall(inventory.add, bag7, coin7) T.assert(not ok7, "7. add item with stack_mode='stack' → Loud-Error") -- ================================================================ -- Assertion 8: add on non-composition entity (no reg_id) → Loud-Error -- Uses raw entity.create() (not composition.create) so the entity -- has no composition.reg_id. composition.get_container returns nil, -- which propagates through validate_container as a Loud-Error. -- Distinct from assertion 5 (composition entity, no container block). -- ================================================================ local raw8 = entity.create() local sword8 = composition.create{ template = "inv_test.sword" } local ok8 = pcall(inventory.add, raw8, sword8) T.assert(not ok8, "8. add on non-composition entity (no reg_id) → Loud-Error") -- ================================================================ -- Assertion 9: add sets renderable tag off on item -- ================================================================ local bag9 = composition.create{ template = "inv_test.container" } local sword9 = composition.create{ template = "inv_test.sword" } -- Confirm sword is renderable before add. local renderable_before9 = composition.list_by_tag("renderable") local in_renderable_before9 = false for _, e in ipairs(renderable_before9) do if same_entity(e, sword9) then in_renderable_before9 = true; break end end T.assert(in_renderable_before9, "9a. item is in renderable index before add") inventory.add(bag9, sword9) local renderable_after9 = composition.list_by_tag("renderable") local in_renderable_after9 = false for _, e in ipairs(renderable_after9) do if same_entity(e, sword9) then in_renderable_after9 = true; break end end T.assert(not in_renderable_after9, "9b. add: item removed from renderable index via composition.set_tag") -- ================================================================ -- Assertion 10: remove sets renderable tag back on item -- ================================================================ inventory.remove(bag9, sword9) local renderable_after_remove10 = composition.list_by_tag("renderable") local restored10 = false for _, e in ipairs(renderable_after_remove10) do if same_entity(e, sword9) then restored10 = true; break end end T.assert(restored10, "10. remove: item restored to renderable index via composition.set_tag") -- ================================================================ -- Assertion 11: slot-naming reconstruction -- A container already has 2 children from a prior session (simulated -- by directly attaching raw entities under item.3 and item.7 slots), -- then inventory.contents and inventory.count derive correct results -- from get_children, and next add lands on item.8 (max+1). -- ================================================================ local bag11 = composition.create{ template = "inv_test.container" } -- Simulate pre-existing items (as if loaded from persistence). -- We create composition items so set_tag works, then directly attach -- them under specific slot names to mimic a reloaded state. local item11a = composition.create{ template = "inv_test.sword" } local item11b = composition.create{ template = "inv_test.potion" } -- Attach directly at known slot indices (simulating a reload state). entity.attach(bag11, "item.3", item11a) entity.attach(bag11, "item.7", item11b) -- contents/count must work purely from get_children. T.assert(inventory.count(bag11) == 2, "11a. reconstruction: count derives 2 from existing item. children") local contents11 = inventory.contents(bag11) T.assert(#contents11 == 2 and same_entity(contents11[1], item11a) and same_entity(contents11[2], item11b), "11b. reconstruction: contents ordered by slot-n, items correct") -- Next add must land on item.8 (max=7, so 7+1=8), not item.1. local item11c = composition.create{ template = "inv_test.sword" } -- item11c has no prior parent; set_tag is invoked by add. inventory.add(bag11, item11c) local children11 = bag11:get_children() T.assert(children11["item.8"] ~= nil, "11c. reconstruction: next synthetic slot after item.7 is item.8") end return M