aboutsummaryrefslogtreecommitdiff
path: root/internal/validate/hardening_craft_validation_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/validate/hardening_craft_validation_test.go')
-rw-r--r--internal/validate/hardening_craft_validation_test.go130
1 files changed, 130 insertions, 0 deletions
diff --git a/internal/validate/hardening_craft_validation_test.go b/internal/validate/hardening_craft_validation_test.go
new file mode 100644
index 0000000..5e5d83f
--- /dev/null
+++ b/internal/validate/hardening_craft_validation_test.go
@@ -0,0 +1,130 @@
+package validate
+
+import (
+ "path/filepath"
+ "testing"
+
+ "thehouseoficarus/internal/item"
+)
+
+// Regression: validateCraftBlocks must validate byproducts, the required tool,
+// and success/fail coherence. A typo in a byproduct previously passed startup
+// validation and the runtime wrote a phantom inventory slot with no item
+// definition; a tool that names no item was also unchecked; and a fail product
+// or fail_message on a recipe with no success formula can never be reached
+// (the engine hardcodes a 100% success chance).
+
+func writeCraftItem(t *testing.T, dir, name, body string) {
+ t.Helper()
+ writeFile(t, filepath.Join(dir, "items", name+".yaml"), body)
+}
+
+func TestCraftByproductMustExist(t *testing.T) {
+ dir := t.TempDir()
+ writeCraftItem(t, dir, "test_in", "id: test_in\nname: test in\n")
+ writeCraftItem(t, dir, "test_out", `id: test_out
+name: test out
+craft:
+ - type: crafting
+ ingredients:
+ - items: [test_in]
+ quantity: 1
+ byproducts: [missing_byproduct]
+`)
+ issues := validateCraftBlocks(newSource(dir))
+ if !containsMsg(issues, "nonexistent byproduct") {
+ t.Fatalf("expected nonexistent-byproduct error, got: %+v", issues)
+ }
+}
+
+func TestCraftToolMustExist(t *testing.T) {
+ dir := t.TempDir()
+ writeCraftItem(t, dir, "test_in", "id: test_in\nname: test in\n")
+ writeCraftItem(t, dir, "test_out", `id: test_out
+name: test out
+craft:
+ - type: crafting
+ tool: missing_tool
+ ingredients:
+ - items: [test_in]
+ quantity: 1
+`)
+ issues := validateCraftBlocks(newSource(dir))
+ if !containsMsg(issues, "nonexistent tool") {
+ t.Fatalf("expected nonexistent-tool error, got: %+v", issues)
+ }
+}
+
+func TestCraftFailWithoutSuccessFormulaIsWarned(t *testing.T) {
+ dir := t.TempDir()
+ writeCraftItem(t, dir, "test_in", "id: test_in\nname: test in\n")
+ writeCraftItem(t, dir, "burnt", "id: burnt\nname: burnt\n")
+ writeCraftItem(t, dir, "test_out", `id: test_out
+name: test out
+craft:
+ - type: cooking
+ fail: burnt
+ fail_message: You burn it.
+ ingredients:
+ - items: [test_in]
+ quantity: 1
+`)
+ issues := validateCraftBlocks(newSource(dir))
+ if !containsMsg(issues, "unreachable") {
+ t.Fatalf("expected unreachable fail warning, got: %+v", issues)
+ }
+ for _, issue := range issues {
+ if issue.Level == "ERROR" {
+ t.Fatalf("dead fail data must not fail startup validation: %+v", issue)
+ }
+ }
+}
+
+func TestCraftReachableFailProducesNoIssue(t *testing.T) {
+ dir := t.TempDir()
+ writeCraftItem(t, dir, "test_in", "id: test_in\nname: test in\n")
+ writeCraftItem(t, dir, "burnt", "id: burnt\nname: burnt\n")
+ writeCraftItem(t, dir, "test_out", `id: test_out
+name: test out
+craft:
+ - type: cooking
+ fail: burnt
+ fail_message: You burn it.
+ success:
+ base: 0.5
+ cap: 0.95
+ per_level: 0.02
+ ingredients:
+ - items: [test_in]
+ quantity: 1
+`)
+ if issues := validateCraftBlocks(newSource(dir)); len(issues) != 0 {
+ t.Fatalf("reachable fail branch must validate cleanly, got: %+v", issues)
+ }
+}
+
+func TestCraftFailureReachableFormula(t *testing.T) {
+ cases := []struct {
+ name string
+ base float64
+ perLvl float64
+ cap float64
+ nilF bool
+ want bool
+ }{
+ {name: "no formula", nilF: true, want: false},
+ {name: "50% base", base: 0.5, perLvl: 0.02, cap: 0.95, want: true},
+ {name: "always succeeds", base: 1.0, perLvl: 0, cap: 1.0, want: false},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ craft := &item.CraftDef{Level: 1}
+ if !c.nilF {
+ craft.Success = &item.SuccessFormula{Base: c.base, PerLevel: c.perLvl, Cap: c.cap}
+ }
+ if got := craftFailureReachable(craft); got != c.want {
+ t.Errorf("craftFailureReachable = %v, want %v", got, c.want)
+ }
+ })
+ }
+}