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) } }) } }