initial: inventory-list-test v0.1.0 — 12 assertions
Covers engine spike (multi-slot children), add/remove/contains/count round-trips, Loud-Error paths (no container block, no stack_mode, stack_mode=stack), renderable-tag toggle via set_tag, and slot-naming reconstruction from get_children (simulated post-reload state).
This commit is contained in:
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2026 Calic. All rights reserved.
|
||||
|
||||
This software is part of the Sporel platform — **Tier 1 (Official /
|
||||
Proprietary)** content per the Three-Tier Licensing Model documented in
|
||||
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
|
||||
migration to `meta/docs/architecture/licensing-model.md` pending).
|
||||
|
||||
⚠ **WIP — Legal review required before public launch.** The terms below
|
||||
reflect design intent only; the formalized license framework will be
|
||||
finalized through legal counsel before the first public release. Until
|
||||
then, this notice serves as a placeholder defending the platform owner's
|
||||
rights against unintentional re-licensing.
|
||||
|
||||
No license is granted to copy, modify, distribute, sublicense, or otherwise
|
||||
use this software in any form without prior written permission from the
|
||||
copyright holder.
|
||||
|
||||
References:
|
||||
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
|
||||
via official channels (Steam, etc.)
|
||||
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
|
||||
share — see vision.md §Licensing Model
|
||||
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
|
||||
to community-uploaded libs/modules/assets, not this repo
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# lib-core.inventory-list-test
|
||||
|
||||
Test suite for `lib-core.inventory-list` v0.1.0.
|
||||
|
||||
**12 assertions:** 1 engine spike + 11 spec assertions.
|
||||
|
||||
| # | Description |
|
||||
|---|---|
|
||||
| spike | `engine.entity` supports 3 children on distinct slot names |
|
||||
| 1 | `add` → `contains` true |
|
||||
| 2 | after `add`: `count == 1`, `contents` contains item |
|
||||
| 3 | two `add`s → `count == 2`, both in `contents`, distinct slot names |
|
||||
| 4 | `remove` → `contains` false, `count == 0` |
|
||||
| 5 | `add` to entity without container block → Loud-Error |
|
||||
| 6 | `add` entity without `stack_mode` → Loud-Error |
|
||||
| 7 | `add` item with `stack_mode="stack"` → Loud-Error |
|
||||
| 8 | `add` to non-container-template entity → Loud-Error |
|
||||
| 9 | `add` removes item from renderable index |
|
||||
| 10 | `remove` restores item to renderable index |
|
||||
| 11 | slot-naming reconstruction from `get_children` (simulated reload) |
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
SPOREL_LIBS_DIR=~/Projects/Sporel/sporel-libs SPOREL_CI=1 SPOREL_LOG_STDERR=1 \
|
||||
./build/Sporel.exe --test-libs=lib-core.inventory-list-test 2>&1 | tail -30
|
||||
```
|
||||
304
init.lua
Normal file
304
init.lua
Normal file
@@ -0,0 +1,304 @@
|
||||
-- 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.<n> 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.<n> 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
|
||||
6
manifest.core
Normal file
6
manifest.core
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "lib-core.inventory-list-test.core",
|
||||
"kind": "lib",
|
||||
"version": "0.1.0",
|
||||
"license": "Proprietary"
|
||||
}
|
||||
10
manifest.lib
Normal file
10
manifest.lib
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "lib-core.inventory-list-test",
|
||||
"role": "test",
|
||||
"version": "0.1.0",
|
||||
"api_min": "0.1",
|
||||
"deps": [
|
||||
{"id": "lib-core.inventory-list", "version": "0.1.0"},
|
||||
{"id": "lib-core.composition", "version": "0.2.0"}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user