Bump composition dep to 0.3.0 and add 3 assertions covering the new getter: round-trip template-id on composition.create, loud-error on nil arg with message-pattern match, nil on raw entity.create handle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
9.6 KiB
Lua
222 lines
9.6 KiB
Lua
-- lib-core.composition-test — Phase A.1 + B.1 + C.0
|
|
-- 26 assertions: 4 define_template + 4 create + 3 list_by_* + 2 destroy + 1 list_templates
|
|
-- + 8 B.1 (set_tag round-trip + container-block validation) + 1 slots regression
|
|
-- + 3 C.0 (template_of getter: round-trip, nil-arg loud-error, raw-entity nil).
|
|
-- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.*
|
|
|
|
local composition = require("lib-core.composition")
|
|
local T = engine.test
|
|
|
|
local M = {}
|
|
|
|
function M.run_tests(ctx)
|
|
-- ----------------------------------------------------------------
|
|
-- define_template (4)
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- Happy path: define + retrieve via list_templates
|
|
composition.define_template{
|
|
id = "sign",
|
|
properties = {
|
|
sprite_path = "sprites/sign.png",
|
|
text = "default text",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable"},
|
|
}
|
|
local templates_after_define = composition.list_templates()
|
|
local found_sign = false
|
|
for _, id in ipairs(templates_after_define) do
|
|
if id == "sign" then found_sign = true; break end
|
|
end
|
|
T.assert(found_sign, "define_template 'sign' registers in list_templates")
|
|
|
|
-- Loud-error on missing id
|
|
local ok_missing = pcall(composition.define_template, { properties = {} })
|
|
T.assert(not ok_missing, "define_template loud-errors on missing id")
|
|
|
|
-- Loud-error on duplicate id (sign already defined above)
|
|
local ok_dup = pcall(composition.define_template, { id = "sign", properties = {} })
|
|
T.assert(not ok_dup, "define_template loud-errors on duplicate id")
|
|
|
|
-- Loud-error on deferred slots-block (Phase D)
|
|
local ok_slots = pcall(composition.define_template, {
|
|
id = "sword",
|
|
slots = { blade = {} },
|
|
})
|
|
T.assert(not ok_slots, "define_template loud-errors on 'slots' block (Phase D trigger)")
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- create (4)
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- Happy path: create returns a non-nil entity that carries template
|
|
-- properties + per-instance overrides
|
|
local sign1 = composition.create{
|
|
template = "sign",
|
|
properties = {
|
|
position = {x = 144, y = 200},
|
|
text = "Hello",
|
|
},
|
|
}
|
|
T.assert(sign1 ~= nil, "create returns a non-nil entity handle")
|
|
T.equals(sign1:get_property("text"), "Hello",
|
|
"create override sets text-property")
|
|
T.equals(sign1:get_property("position.x"), 144,
|
|
"create flattens nested position into position.x")
|
|
|
|
-- Template-default-property survives when override doesn't touch it
|
|
T.equals(sign1:get_property("sprite_path"), "sprites/sign.png",
|
|
"create preserves template default for sprite_path")
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- list_by_template + list_by_tag (3)
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- Single instance
|
|
local by_tpl = composition.list_by_template("sign")
|
|
T.assert(#by_tpl == 1, "list_by_template returns 1 entity after 1 create")
|
|
|
|
-- Tag-Index
|
|
local by_tag = composition.list_by_tag("renderable")
|
|
T.assert(#by_tag == 1, "list_by_tag returns 1 entity for 'renderable'")
|
|
|
|
-- Unknown template / tag → empty
|
|
local empty_tpl = composition.list_by_template("nonexistent")
|
|
T.assert(#empty_tpl == 0, "list_by_template empty for unknown template")
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- destroy (2)
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- Create a second instance, then destroy it, indexes shrink
|
|
local sign2 = composition.create{
|
|
template = "sign",
|
|
properties = { position = {x = 50, y = 50}, text = "T2" },
|
|
}
|
|
T.assert(#composition.list_by_template("sign") == 2,
|
|
"list_by_template grows to 2 after second create")
|
|
|
|
composition.destroy(sign2)
|
|
T.assert(#composition.list_by_template("sign") == 1,
|
|
"destroy shrinks template-index back to 1")
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- list_templates (1)
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- list_templates is non-empty after define
|
|
T.assert(#composition.list_templates() >= 1,
|
|
"list_templates returns at least 1 entry after define_template")
|
|
|
|
-- ================================================================
|
|
-- B.1 — set_tag + container-block validation (8)
|
|
-- ================================================================
|
|
|
|
-- Need a live entity to test set_tag against.
|
|
local tagtest_entity = composition.create{ template = "sign" }
|
|
|
|
-- 1. set_tag(e, "foo", true) → list_by_tag("foo") contains e.
|
|
composition.set_tag(tagtest_entity, "foo", true)
|
|
local foo_list_after_add = composition.list_by_tag("foo")
|
|
local found_in_foo = false
|
|
for _, e in ipairs(foo_list_after_add) do
|
|
if e == tagtest_entity then found_in_foo = true; break end
|
|
end
|
|
T.assert(found_in_foo, "set_tag(e,'foo',true) adds e to list_by_tag('foo')")
|
|
|
|
-- 2. set_tag(e, "foo", false) → list_by_tag("foo") does not contain e.
|
|
composition.set_tag(tagtest_entity, "foo", false)
|
|
local foo_list_after_remove = composition.list_by_tag("foo")
|
|
local still_in_foo = false
|
|
for _, e in ipairs(foo_list_after_remove) do
|
|
if e == tagtest_entity then still_in_foo = true; break end
|
|
end
|
|
T.assert(not still_in_foo, "set_tag(e,'foo',false) removes e from list_by_tag('foo')")
|
|
|
|
-- 3. set_tag idempotent — double true and double false must not error
|
|
-- and must not duplicate / extra-remove entries.
|
|
composition.set_tag(tagtest_entity, "bar", true)
|
|
composition.set_tag(tagtest_entity, "bar", true) -- second call: idempotent
|
|
local bar_list = composition.list_by_tag("bar")
|
|
local bar_count = 0
|
|
for _, e in ipairs(bar_list) do
|
|
if e == tagtest_entity then bar_count = bar_count + 1 end
|
|
end
|
|
T.equals(bar_count, 1, "set_tag idempotent: double-true does not duplicate in index")
|
|
|
|
composition.set_tag(tagtest_entity, "bar", false)
|
|
composition.set_tag(tagtest_entity, "bar", false) -- second call: idempotent
|
|
local bar_list_after = composition.list_by_tag("bar")
|
|
local bar_count_after = 0
|
|
for _, e in ipairs(bar_list_after) do
|
|
if e == tagtest_entity then bar_count_after = bar_count_after + 1 end
|
|
end
|
|
T.equals(bar_count_after, 0, "set_tag idempotent: double-false does not error or leave stale entry")
|
|
|
|
-- 4. set_tag on a foreign (non-composition) entity → Loud-Error.
|
|
local alien = entity.create()
|
|
local ok_alien = pcall(composition.set_tag, alien, "any", true)
|
|
T.assert(not ok_alien, "set_tag loud-errors on entity not created by composition")
|
|
|
|
-- 5. define_template with container={kind="list"} → accepted (no error).
|
|
local ok_container_list = pcall(composition.define_template, {
|
|
id = "backpack",
|
|
properties = {
|
|
sprite_atlas = "sprites/items.png",
|
|
sprite_uv = "0 0 32 32",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable"},
|
|
container = { kind = "list" },
|
|
})
|
|
T.assert(ok_container_list, "define_template accepts container={kind='list'}")
|
|
|
|
-- 6. define_template with container={kind="grid"} → Loud-Error.
|
|
local ok_container_grid = pcall(composition.define_template, {
|
|
id = "grid_bag",
|
|
container = { kind = "grid" },
|
|
})
|
|
T.assert(not ok_container_grid, "define_template loud-errors on container={kind='grid'}")
|
|
|
|
-- 7. define_template with container={kind="list", weight_max=10} → Loud-Error
|
|
-- (unimplemented constraint field declared).
|
|
local ok_weight_max = pcall(composition.define_template, {
|
|
id = "heavy_bag",
|
|
container = { kind = "list", weight_max = 10 },
|
|
})
|
|
T.assert(not ok_weight_max,
|
|
"define_template loud-errors on container with unimplemented constraint weight_max")
|
|
|
|
-- 8. define_template with slots={...} → still Loud-Error (Phase D, regression).
|
|
local ok_slots_regression = pcall(composition.define_template, {
|
|
id = "composite_sword",
|
|
slots = { blade = {} },
|
|
})
|
|
T.assert(not ok_slots_regression,
|
|
"define_template still loud-errors on 'slots' block (Phase D regression)")
|
|
|
|
-- ================================================================
|
|
-- C.0 — template_of getter (3)
|
|
-- ================================================================
|
|
|
|
-- 1. composition.create returns entity whose template_of returns its template-id.
|
|
local template_of_target = composition.create{ template = "sign" }
|
|
T.equals(composition.template_of(template_of_target), "sign",
|
|
"template_of returns template-id for composition-created entity")
|
|
|
|
-- 2. template_of(nil) loud-errors with message matching "composition.template_of.*entity".
|
|
local ok_nil, err_nil = pcall(composition.template_of, nil)
|
|
T.assert(not ok_nil and type(err_nil) == "string"
|
|
and string.find(err_nil, "composition%.template_of") ~= nil
|
|
and string.find(err_nil, "entity") ~= nil,
|
|
"template_of(nil) loud-errors with message matching 'composition.template_of.*entity'")
|
|
|
|
-- 3. Raw engine.entity.create() entity (no composition) → template_of returns nil (no error).
|
|
local raw_alien = entity.create()
|
|
T.equals(composition.template_of(raw_alien), nil,
|
|
"template_of returns nil for raw entity.create() handle (not composition-managed)")
|
|
end
|
|
|
|
return M
|