From a9bfca1ffe9805f3bf68ee17a5606beb4dd9292f Mon Sep 17 00:00:00 2001 From: Calic Date: Sun, 14 Jun 2026 12:20:53 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20lib-core.crafting-test=20v0.1.0=20?= =?UTF-8?q?=E2=80=94=2012=20TAP=20asserts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test suite for lib-core.crafting v0.1.0. Covers: 1. define_recipe + list_recipes round-trip; shallow-copy isolation (mutating the returned list does not affect the registry). 2. define_recipe missing id → Loud-Error. 3. define_recipe duplicate id → Loud-Error. 4. define_recipe empty inputs → Loud-Error. 5. get_recipe known/unknown. 6. is_known propagates ctx-identity to recipe.is_known(ctx). 7. can_craft happy-path → ok=true. 8. can_craft missing inputs → error='missing_inputs' + missing-array with {template, needed, have}. 9. craft happy-path: consumes 2, crafts 1, container has only output. 10. craft unknown recipe → ok=false, error='unknown_recipe', container untouched. 11. craft is_known=false → ok=false, error='unknown_recipe', container untouched. 12. count-form: 2-of-3 reports missing={template, needed=3, have=2}. Uses engine.test.assert with string.find for pattern matching loud-error messages (engine.test has no assert_match helper). Per-test isolation via crafting._test_clear_all; composition templates are defined once at module-scope (composition has no clear-all hook). Depends on lib-core.crafting 0.1.0, lib-core.inventory-list 0.1.0, lib-core.composition 0.3.0 (all three direct, per non-transitive engine resolver). Co-Authored-By: Claude Opus 4.7 (1M context) --- LICENSE | 24 ++++ README.md | 11 ++ init.lua | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.core | 6 + manifest.lib | 11 ++ 5 files changed, 391 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 init.lua create mode 100644 manifest.core create mode 100644 manifest.lib diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81c9d65 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e045624 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# lib-core.crafting-test + +Test suite for `lib-core.crafting` v0.1.0. + +**12 assertions.** + +**Run:** +```bash +SPOREL_LIBS_DIR=~/Projects/Sporel/sporel-libs SPOREL_CI=1 SPOREL_LOG_STDERR=1 \ + ./build/Sporel.exe --test-libs=lib-core.crafting-test 2>&1 | tail -30 +``` diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..df3e62d --- /dev/null +++ b/init.lua @@ -0,0 +1,339 @@ +-- lib-core.crafting-test — Phase C.1 +-- 12 assertions: 5 registry (define_recipe + list + get) + 7 discovery/match/craft. +-- 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 + +-- --------------------------------------------------------------------- +-- 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}") +end + +return M diff --git a/manifest.core b/manifest.core new file mode 100644 index 0000000..95f2726 --- /dev/null +++ b/manifest.core @@ -0,0 +1,6 @@ +{ + "id": "lib-core.crafting-test.core", + "kind": "lib", + "version": "0.1.0", + "license": "Proprietary" +} diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..4dbe2ed --- /dev/null +++ b/manifest.lib @@ -0,0 +1,11 @@ +{ + "id": "lib-core.crafting-test", + "role": "test", + "version": "0.1.0", + "api_min": "0.1", + "deps": [ + {"id": "lib-core.crafting", "version": "0.1.0"}, + {"id": "lib-core.inventory-list", "version": "0.1.0"}, + {"id": "lib-core.composition", "version": "0.3.0"} + ] +}