test: lib-core.crafting-test v0.2.0 — Locale-API assertions + bw-compat regression

Adds 6 new TAP assertions (13-18) covering the new Multi-Source /
Single-Sink locale API and the bare-handle bw-compat shim:

 13. Bw-Compat: craft(recipe, entity_handle, ctx) still accepts a
     bare container-entity-handle; post-state matches the v0.1
     single-container behavior (inputs consumed, output added back
     to the same container).
 14. Form-2 happy-path: sources={c1,c2}, sink=c1, recipe needs one
     rock + one stick (c1 has rock, c2 has stick) → c1 ends with the
     rock_pick + zero rocks, c2 ends empty.
 15. Greedy-drain-order: source1 has 2 rocks, source2 has 5 rocks,
     recipe needs 3 rocks → source1 fully drained, source2 has 4 left
     (proves left-to-right priority is stable).
 16. Aggregate missing-count: source1 has 1 rock, source2 has 1 rock,
     recipe needs 3 → can_craft returns missing entry with have=2
     (proves cross-source counting in can_craft).
 17. Loud-Error empty sources: locale={sources={}, sink=c1} →
     error string mentions "crafting", "sources", "non-empty".
 18. Loud-Error nil sink: locale={sources={c1}, sink=nil} →
     error string mentions "crafting", "sink", "nil".

Adds a sibling fixture helper `extra_container(items)` that creates
a populated container WITHOUT resetting the recipe registry. Needed
for the multi-source cases (14-18): the existing `setup_container`
calls `crafting._test_clear_all()` on every invocation, so it can
only be used once per test; the second-and-later containers come
from `extra_container`. This keeps test isolation per-case while
allowing N>1 containers in a single case.

Manifest bumped 0.1.0 → 0.2.0; dep on lib-core.crafting bumped to
0.2.0 to match the producer. inventory-list 0.1.0 and composition
0.3.0 deps unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Calic
2026-06-14 18:48:18 +02:00
parent a9bfca1ffe
commit ccaed2d0de
2 changed files with 199 additions and 2 deletions

197
init.lua
View File

@@ -72,6 +72,20 @@ local function setup_container(items)
return container return container
end end
-- Sibling helper for multi-source tests: creates an additional container
-- without resetting the recipe registry. Use AFTER setup_container() to
-- avoid wiping recipes that were just defined for the test.
local function extra_container(items)
local container = composition.create{ template = "craft_test.container" }
for _, spec in ipairs(items) do
for _ = 1, spec.count do
local e = composition.create{ template = spec.template }
inv.add(container, e)
end
end
return container
end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
-- Tests -- Tests
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
@@ -334,6 +348,189 @@ function M.run_tests(ctx)
end end
T.assert(cc12.ok == false and cc12.error == "missing_inputs" and missing_match_12, T.assert(cc12.ok == false and cc12.error == "missing_inputs" and missing_match_12,
"12. count-form: 2-of-3 rocks reports missing={template, needed=3, have=2}") "12. count-form: 2-of-3 rocks reports missing={template, needed=3, have=2}")
-- ================================================================
-- §13: Bw-Compat — craft(recipe, entity_handle, ctx) still works
-- with bare-entity-handle locale (Form 1).
-- ================================================================
local container13 = setup_container{
{ template = "craft_test.rock", count = 1 },
{ template = "craft_test.stick", count = 1 },
}
crafting.define_recipe{
id = "rock_pick_r",
inputs = {
{ template = "craft_test.rock", count = 1 },
{ template = "craft_test.stick", count = 1 },
},
output = { template = "craft_test.rock_pick", count = 1 },
}
local res13 = crafting.craft("rock_pick_r", container13, {})
local pick_count_13 = 0
local rock_count_13 = 0
local stick_count_13 = 0
for _, e in ipairs(inv.contents(container13)) do
local tpl = composition.template_of(e)
if tpl == "craft_test.rock_pick" then pick_count_13 = pick_count_13 + 1 end
if tpl == "craft_test.rock" then rock_count_13 = rock_count_13 + 1 end
if tpl == "craft_test.stick" then stick_count_13 = stick_count_13 + 1 end
end
T.assert(res13.ok == true
and res13.crafted_items ~= nil and #res13.crafted_items == 1
and res13.consumed ~= nil and #res13.consumed == 2
and pick_count_13 == 1 and rock_count_13 == 0 and stick_count_13 == 0,
"13. Bw-Compat: craft(recipe, entity_handle, ctx) accepts bare handle, post-state matches v0.1 behavior")
-- ================================================================
-- §14: Form-2 happy-path — 2 sources, sink = source[1].
-- c1=1 rock, c2=1 stick, recipe needs both,
-- locale={sources={c1,c2}, sink=c1}.
-- Post: c1 has 1 rock_pick + 0 rock, c2 has 0 stick.
-- ================================================================
local c1_14 = setup_container{
{ template = "craft_test.rock", count = 1 },
}
local c2_14 = extra_container{
{ template = "craft_test.stick", count = 1 },
}
crafting.define_recipe{
id = "rock_pick_r",
inputs = {
{ template = "craft_test.rock", count = 1 },
{ template = "craft_test.stick", count = 1 },
},
output = { template = "craft_test.rock_pick", count = 1 },
}
local res14 = crafting.craft("rock_pick_r",
{ sources = {c1_14, c2_14}, sink = c1_14 }, {})
local c1_pick_14, c1_rock_14, c1_stick_14 = 0, 0, 0
for _, e in ipairs(inv.contents(c1_14)) do
local tpl = composition.template_of(e)
if tpl == "craft_test.rock_pick" then c1_pick_14 = c1_pick_14 + 1 end
if tpl == "craft_test.rock" then c1_rock_14 = c1_rock_14 + 1 end
if tpl == "craft_test.stick" then c1_stick_14 = c1_stick_14 + 1 end
end
local c2_pick_14, c2_rock_14, c2_stick_14 = 0, 0, 0
for _, e in ipairs(inv.contents(c2_14)) do
local tpl = composition.template_of(e)
if tpl == "craft_test.rock_pick" then c2_pick_14 = c2_pick_14 + 1 end
if tpl == "craft_test.rock" then c2_rock_14 = c2_rock_14 + 1 end
if tpl == "craft_test.stick" then c2_stick_14 = c2_stick_14 + 1 end
end
T.assert(res14.ok == true
and c1_pick_14 == 1 and c1_rock_14 == 0 and c1_stick_14 == 0
and c2_pick_14 == 0 and c2_rock_14 == 0 and c2_stick_14 == 0,
"14. Form-2 happy-path: sources={c1,c2}, sink=c1; c1 ends with rock_pick + no rock, c2 ends empty")
-- ================================================================
-- §15: Greedy-drain-order — source1=2 rocks, source2=5 rocks,
-- recipe needs 3 rocks → source1 fully drained, source2 has 4 left.
-- ================================================================
local c1_15 = setup_container{
{ template = "craft_test.rock", count = 2 },
}
local c2_15 = extra_container{
{ template = "craft_test.rock", count = 5 },
}
crafting.define_recipe{
id = "rock_pile_r",
inputs = {
{ template = "craft_test.rock", count = 3 },
},
output = { template = "craft_test.rock_pick", count = 1 },
}
local res15 = crafting.craft("rock_pile_r",
{ sources = {c1_15, c2_15}, sink = c1_15 }, {})
local c1_rock_15 = 0
for _, e in ipairs(inv.contents(c1_15)) do
if composition.template_of(e) == "craft_test.rock" then
c1_rock_15 = c1_rock_15 + 1
end
end
local c2_rock_15 = 0
for _, e in ipairs(inv.contents(c2_15)) do
if composition.template_of(e) == "craft_test.rock" then
c2_rock_15 = c2_rock_15 + 1
end
end
T.assert(res15.ok == true
and c1_rock_15 == 0 and c2_rock_15 == 4,
"15. Greedy-drain-order: source1 (2 rocks) fully drained, source2 (5 rocks) has 4 remaining after needing 3")
-- ================================================================
-- §16: Aggregate missing-count — source1=1 rock, source2=1 rock,
-- recipe needs 3 → missing.have=2.
-- ================================================================
local c1_16 = setup_container{
{ template = "craft_test.rock", count = 1 },
}
local c2_16 = extra_container{
{ template = "craft_test.rock", count = 1 },
}
crafting.define_recipe{
id = "rock_pile_r",
inputs = {
{ template = "craft_test.rock", count = 3 },
},
output = { template = "craft_test.rock_pick", count = 1 },
}
local cc16 = crafting.can_craft("rock_pile_r",
{ sources = {c1_16, c2_16}, sink = c1_16 }, {})
local missing_match_16 = false
if cc16.missing then
for _, m in ipairs(cc16.missing) do
if m.template == "craft_test.rock"
and m.needed == 3 and m.have == 2 then
missing_match_16 = true; break
end
end
end
T.assert(cc16.ok == false and cc16.error == "missing_inputs"
and missing_match_16,
"16. Aggregate missing-count across sources: 1+1 rocks vs need 3 reports have=2")
-- ================================================================
-- §17: Loud-Error empty sources — locale={sources={}, sink=c1}
-- → error matches "crafting" + "sources" + "non-empty".
-- ================================================================
local c1_17 = setup_container{
{ template = "craft_test.rock", count = 1 },
}
crafting.define_recipe{
id = "rock_pile_r",
inputs = { { template = "craft_test.rock", count = 1 } },
output = { template = "craft_test.rock_pick", count = 1 },
}
local ok17, err17 = pcall(crafting.can_craft, "rock_pile_r",
{ sources = {}, sink = c1_17 }, {})
T.assert(not ok17 and type(err17) == "string"
and string.find(err17, "crafting") ~= nil
and string.find(err17, "sources") ~= nil
and (string.find(err17, "non%-empty") ~= nil
or string.find(err17, "non empty") ~= nil),
"17. Loud-Error: locale.sources={} → error mentions 'crafting', 'sources', 'non-empty'")
-- ================================================================
-- §18: Loud-Error nil sink — locale={sources={c1}, sink=nil}
-- → error matches "crafting" + "sink" + "nil".
-- ================================================================
local c1_18 = setup_container{
{ template = "craft_test.rock", count = 1 },
}
crafting.define_recipe{
id = "rock_pile_r",
inputs = { { template = "craft_test.rock", count = 1 } },
output = { template = "craft_test.rock_pick", count = 1 },
}
local ok18, err18 = pcall(crafting.can_craft, "rock_pile_r",
{ sources = {c1_18}, sink = nil }, {})
T.assert(not ok18 and type(err18) == "string"
and string.find(err18, "crafting") ~= nil
and string.find(err18, "sink") ~= nil
and string.find(err18, "nil") ~= nil,
"18. Loud-Error: locale.sink=nil → error mentions 'crafting', 'sink', 'nil'")
end end
return M return M

View File

@@ -1,10 +1,10 @@
{ {
"id": "lib-core.crafting-test", "id": "lib-core.crafting-test",
"role": "test", "role": "test",
"version": "0.1.0", "version": "0.2.0",
"api_min": "0.1", "api_min": "0.1",
"deps": [ "deps": [
{"id": "lib-core.crafting", "version": "0.1.0"}, {"id": "lib-core.crafting", "version": "0.2.0"},
{"id": "lib-core.inventory-list", "version": "0.1.0"}, {"id": "lib-core.inventory-list", "version": "0.1.0"},
{"id": "lib-core.composition", "version": "0.3.0"} {"id": "lib-core.composition", "version": "0.3.0"}
] ]