-- 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 = {} -- ---------------------------------------------------------------- -- 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 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 it == sword3 then found3_s = true end if 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 to container with kind != "list" → Loud-Error -- (Template-level catch already fires, but runtime check is the -- double-belt tested here via a raw entity.create() bypass.) -- We test the double-belt by attempting to add to a composition -- entity whose template has a deferred constraint — this cannot -- be constructed via define_template (it loud-errors), so we test -- the "no container block" path which is the closest available -- runtime-only path not gated by template-load. -- The template-level gate for kind!="list" is covered in -- composition-test (assertion 6 there). Here we verify inventory -- itself also loud-errors on a non-container-template entity. -- ================================================================ local plain8 = composition.create{ template = "inv_test.plain_entity" } local sword8 = composition.create{ template = "inv_test.sword" } local ok8 = pcall(inventory.add, plain8, sword8) T.assert(not ok8, "8. add to entity with no container block (non-container) → 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 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 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 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 contents11[1] == item11a and 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 starts renderable; set_tag will be called by add. -- But item11a and item11b were attached directly (bypass add), -- so we must declare stack_mode; they have it via template. -- Detach item11c from any parent first (it has none here). 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