§19 verifies sources={c, c} is de-duplicated (have=1, not 2).
§20 verifies {sink=c} without sources raises a loud-error
attributing crafting + sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
606 lines
26 KiB
Lua
606 lines
26 KiB
Lua
-- lib-core.crafting-test — Phase C.1 + D.0
|
|
-- 20 assertions: 12 base (registry + discovery/match/craft), 6 locale-API
|
|
-- (Form-2 happy + greedy-drain + aggregate + Loud-Errors), 2 silent-fail
|
|
-- regressions (duplicate-sources de-dup + malformed-locale loud-error).
|
|
-- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.*
|
|
|
|
local composition = require("lib-core.composition")
|
|
local inv = require("lib-core.inventory-list")
|
|
local crafting = require("lib-core.crafting")
|
|
local T = engine.test
|
|
|
|
local M = {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Template definitions (one-time, shared across tests).
|
|
-- composition does not expose a clear-all, so we never re-define.
|
|
-- All template-ids are unique within this test run.
|
|
-- ---------------------------------------------------------------------
|
|
|
|
composition.define_template{
|
|
id = "craft_test.container",
|
|
properties = {
|
|
sprite_path = "sprites/bag.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable"},
|
|
container = { kind = "list" },
|
|
}
|
|
|
|
composition.define_template{
|
|
id = "craft_test.rock",
|
|
properties = {
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/rock.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
composition.define_template{
|
|
id = "craft_test.stick",
|
|
properties = {
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/stick.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
composition.define_template{
|
|
id = "craft_test.rock_pick",
|
|
properties = {
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/pick.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Fixture helper: fresh container + populate with items.
|
|
-- crafting._test_clear_all() wipes recipe state between tests.
|
|
-- ---------------------------------------------------------------------
|
|
|
|
local function setup_container(items)
|
|
crafting._test_clear_all()
|
|
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
|
|
|
|
-- 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
|
|
-- ---------------------------------------------------------------------
|
|
|
|
function M.run_tests(ctx)
|
|
-- ================================================================
|
|
-- §1: define_recipe + list_recipes round-trip (with shallow-copy isolation)
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
|
|
crafting.define_recipe{
|
|
id = "recipe_round_trip",
|
|
inputs = {
|
|
{ template = "craft_test.rock", count = 1 },
|
|
{ template = "craft_test.stick", count = 1 },
|
|
},
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
name = "Round-Trip Recipe",
|
|
}
|
|
|
|
local listed1 = crafting.list_recipes()
|
|
local found_rt = false
|
|
for _, r in ipairs(listed1) do
|
|
if r.id == "recipe_round_trip" then found_rt = true; break end
|
|
end
|
|
-- Shallow-copy isolation: mutating the returned entry must not leak.
|
|
if #listed1 >= 1 then
|
|
listed1[1].id = "MUTATED"
|
|
end
|
|
local listed1b = crafting.list_recipes()
|
|
local still_rt = false
|
|
for _, r in ipairs(listed1b) do
|
|
if r.id == "recipe_round_trip" then still_rt = true; break end
|
|
end
|
|
T.assert(found_rt and still_rt,
|
|
"1. define_recipe + list_recipes round-trip; mutating returned list does not affect registry")
|
|
|
|
-- ================================================================
|
|
-- §2: define_recipe missing id → Loud-Error matching 'crafting.define_recipe.*id'
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
local ok2, err2 = pcall(crafting.define_recipe, {
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
})
|
|
T.assert(not ok2 and type(err2) == "string"
|
|
and string.find(err2, "crafting%.define_recipe") ~= nil
|
|
and string.find(err2, "id") ~= nil,
|
|
"2. define_recipe missing id → Loud-Error matching 'crafting.define_recipe.*id'")
|
|
|
|
-- ================================================================
|
|
-- §3: define_recipe duplicate id → Loud-Error matching 'crafting.define_recipe.*duplicate'
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
crafting.define_recipe{
|
|
id = "dup_r",
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
}
|
|
local ok3, err3 = pcall(crafting.define_recipe, {
|
|
id = "dup_r",
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
})
|
|
T.assert(not ok3 and type(err3) == "string"
|
|
and string.find(err3, "crafting%.define_recipe") ~= nil
|
|
and string.find(err3, "duplicate") ~= nil,
|
|
"3. define_recipe duplicate id → Loud-Error matching 'crafting.define_recipe.*duplicate'")
|
|
|
|
-- ================================================================
|
|
-- §4: define_recipe empty inputs → Loud-Error matching 'crafting.define_recipe.*inputs'
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
local ok4, err4 = pcall(crafting.define_recipe, {
|
|
id = "empty_inputs_r",
|
|
inputs = {},
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
})
|
|
T.assert(not ok4 and type(err4) == "string"
|
|
and string.find(err4, "crafting%.define_recipe") ~= nil
|
|
and string.find(err4, "inputs") ~= nil,
|
|
"4. define_recipe empty inputs → Loud-Error matching 'crafting.define_recipe.*inputs'")
|
|
|
|
-- ================================================================
|
|
-- §5: get_recipe known/unknown
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
crafting.define_recipe{
|
|
id = "known_r",
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
name = "Known Recipe",
|
|
}
|
|
local got_known = crafting.get_recipe("known_r")
|
|
local got_unknown = crafting.get_recipe("nonexistent_r")
|
|
T.assert(got_known ~= nil and got_known.id == "known_r"
|
|
and got_known.name == "Known Recipe"
|
|
and got_unknown == nil,
|
|
"5. get_recipe returns def for known id, nil for unknown id")
|
|
|
|
-- ================================================================
|
|
-- §6: is_known calls recipe.is_known with ctx (closure capture)
|
|
-- ================================================================
|
|
crafting._test_clear_all()
|
|
local captured_ctx = nil
|
|
crafting.define_recipe{
|
|
id = "is_known_r",
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
is_known = function(c)
|
|
captured_ctx = c
|
|
return true
|
|
end,
|
|
}
|
|
local test_ctx = { actor = "abc", marker = "test6" }
|
|
local result6 = crafting.is_known("is_known_r", test_ctx)
|
|
T.assert(result6 == true and captured_ctx == test_ctx,
|
|
"6. is_known calls recipe.is_known with the supplied ctx (identity preserved)")
|
|
|
|
-- ================================================================
|
|
-- §7: can_craft happy-path → ok=true
|
|
-- ================================================================
|
|
local container7 = 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 cc7 = crafting.can_craft("rock_pick_r", container7, {})
|
|
T.assert(cc7.ok == true,
|
|
"7. can_craft happy-path with all inputs present → ok=true")
|
|
|
|
-- ================================================================
|
|
-- §8: can_craft missing inputs → error='missing_inputs' + missing array
|
|
-- ================================================================
|
|
local container8 = setup_container{
|
|
{ template = "craft_test.rock", count = 1 },
|
|
-- no stick
|
|
}
|
|
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 cc8 = crafting.can_craft("rock_pick_r", container8, {})
|
|
local stick_missing_found = false
|
|
if cc8.missing then
|
|
for _, m in ipairs(cc8.missing) do
|
|
if m.template == "craft_test.stick"
|
|
and m.needed == 1 and m.have == 0 then
|
|
stick_missing_found = true; break
|
|
end
|
|
end
|
|
end
|
|
T.assert(cc8.ok == false and cc8.error == "missing_inputs"
|
|
and stick_missing_found,
|
|
"8. can_craft missing inputs → ok=false, error='missing_inputs', missing entry has correct fields")
|
|
|
|
-- ================================================================
|
|
-- §9: craft happy-path consumes inputs + creates output
|
|
-- ================================================================
|
|
local container9 = 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 res9 = crafting.craft("rock_pick_r", container9, {})
|
|
|
|
local contents9 = inv.contents(container9)
|
|
local pick_count = 0
|
|
local rock_count_9 = 0
|
|
local stick_count_9 = 0
|
|
for _, e in ipairs(contents9) do
|
|
local tpl = composition.template_of(e)
|
|
if tpl == "craft_test.rock_pick" then pick_count = pick_count + 1 end
|
|
if tpl == "craft_test.rock" then rock_count_9 = rock_count_9 + 1 end
|
|
if tpl == "craft_test.stick" then stick_count_9 = stick_count_9 + 1 end
|
|
end
|
|
T.assert(res9.ok == true
|
|
and res9.crafted_items ~= nil and #res9.crafted_items == 1
|
|
and res9.consumed ~= nil and #res9.consumed == 2
|
|
and pick_count == 1 and rock_count_9 == 0 and stick_count_9 == 0,
|
|
"9. craft happy-path: ok=true, consumed=2, crafted=1, container has only output")
|
|
|
|
-- ================================================================
|
|
-- §10: craft unknown recipe → ok=false, error='unknown_recipe', container untouched
|
|
-- ================================================================
|
|
local container10 = setup_container{
|
|
{ template = "craft_test.rock", count = 1 },
|
|
{ template = "craft_test.stick", count = 1 },
|
|
}
|
|
local count_before_10 = inv.count(container10)
|
|
local res10 = crafting.craft("does_not_exist_r", container10, {})
|
|
local count_after_10 = inv.count(container10)
|
|
T.assert(res10.ok == false and res10.error == "unknown_recipe"
|
|
and count_before_10 == count_after_10,
|
|
"10. craft unknown recipe → ok=false, error='unknown_recipe', container untouched")
|
|
|
|
-- ================================================================
|
|
-- §11: craft with is_known=false → ok=false, error='unknown_recipe', container untouched
|
|
-- ================================================================
|
|
local container11 = 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 },
|
|
is_known = function(_) return false end,
|
|
}
|
|
local count_before_11 = inv.count(container11)
|
|
local res11 = crafting.craft("rock_pick_r", container11, {})
|
|
local count_after_11 = inv.count(container11)
|
|
T.assert(res11.ok == false and res11.error == "unknown_recipe"
|
|
and count_before_11 == count_after_11,
|
|
"11. craft with is_known=false → ok=false, error='unknown_recipe', container untouched")
|
|
|
|
-- ================================================================
|
|
-- §12: count-form: 2-of-3 reports correct missing
|
|
-- ================================================================
|
|
local container12 = setup_container{
|
|
{ template = "craft_test.rock", count = 2 },
|
|
}
|
|
crafting.define_recipe{
|
|
id = "rock_pile_r",
|
|
inputs = {
|
|
{ template = "craft_test.rock", count = 3 },
|
|
},
|
|
output = { template = "craft_test.rock_pick", count = 1 },
|
|
}
|
|
local cc12 = crafting.can_craft("rock_pile_r", container12, {})
|
|
local missing_match_12 = false
|
|
if cc12.missing then
|
|
for _, m in ipairs(cc12.missing) do
|
|
if m.template == "craft_test.rock"
|
|
and m.needed == 3 and m.have == 2 then
|
|
missing_match_12 = true; break
|
|
end
|
|
end
|
|
end
|
|
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}")
|
|
|
|
-- ================================================================
|
|
-- §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'")
|
|
|
|
-- ================================================================
|
|
-- §19: Duplicate sources are de-duplicated.
|
|
-- Single container with 1 rock; pass {c, c} as sources.
|
|
-- count_by_template must NOT double-count → can_craft for a
|
|
-- recipe needing 2 rocks reports have=1 (not 2).
|
|
-- ================================================================
|
|
local c_19 = setup_container{
|
|
{ template = "craft_test.rock", count = 1 },
|
|
}
|
|
composition.define_template{
|
|
id = "craft_test.rock_pile_19",
|
|
properties = {
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/pile.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
crafting.define_recipe{
|
|
id = "r2rocks_19",
|
|
inputs = { { template = "craft_test.rock", count = 2 } },
|
|
output = { template = "craft_test.rock_pile_19", count = 1 },
|
|
}
|
|
local cc19 = crafting.can_craft("r2rocks_19",
|
|
{ sources = {c_19, c_19}, sink = c_19 }, {})
|
|
local missing_match_19 = false
|
|
if cc19.missing then
|
|
for _, m in ipairs(cc19.missing) do
|
|
if m.template == "craft_test.rock"
|
|
and m.needed == 2 and m.have == 1 then
|
|
missing_match_19 = true; break
|
|
end
|
|
end
|
|
end
|
|
T.assert(cc19.ok == false and cc19.error == "missing_inputs"
|
|
and missing_match_19,
|
|
"19. Duplicate sources de-duplicated: sources={c,c} with 1 rock reports have=1 (not 2)")
|
|
|
|
-- ================================================================
|
|
-- §20: Loud-Error — Form-2 table without sources field.
|
|
-- Caller typo passes {sink=c} without sources. resolve_locale
|
|
-- must loud-error attributing crafting + sources, not silently
|
|
-- fall through to the Form-1 bw-compat shim (which would then
|
|
-- crash deep inside inventory-list).
|
|
-- ================================================================
|
|
local c_20 = setup_container{}
|
|
composition.define_template{
|
|
id = "craft_test.x_20",
|
|
properties = {
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/x.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
crafting.define_recipe{
|
|
id = "rp_20",
|
|
inputs = { { template = "craft_test.rock", count = 1 } },
|
|
output = { template = "craft_test.x_20", count = 1 },
|
|
}
|
|
local malformed_20 = { sink = c_20 }
|
|
local ok20, err20 = pcall(crafting.craft, "rp_20", malformed_20, {})
|
|
T.assert(not ok20 and type(err20) == "string"
|
|
and string.find(err20, "crafting") ~= nil
|
|
and string.find(err20, "sources") ~= nil,
|
|
"20. Loud-Error: locale table without 'sources' field → error mentions 'crafting' and 'sources'")
|
|
end
|
|
|
|
return M
|