mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
- St. Guildmore's Memorial Hospital: !hospital command with Nurse Joy, procedural itemized billing, same-day revival, 6-hour dead timer, hospital ad after death, 2-hour nudge follow-up - Robbie the Friendly Bandit: automated inventory cleaner, random daily visits (8-21 UTC, 40% chance), collects sub-tier gear at €50/item, donates to community pot, drops Get Out of Medical Debt Free card - Fix MW auto-equip: T1 MW no longer replaces better equipped gear - Fix shop MW block: allow purchasing upgrades over lower-tier MW - Float64 equipment scoring: advEffectiveTier and advEquipmentScore return float64, no mid-calculation truncation (MW 1.25x, Arena 1.5x) - Fix markdown rendering: convert *asterisk* to _underscore_ italics across all flavor text files (shop, blacksmith, rival, hospital) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
354 lines
11 KiB
Go
354 lines
11 KiB
Go
package plugin
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ── Display Tests ───────────────────────────────────────────────────────────
|
||
|
||
func TestLuigiShopGreeting_Basic(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 2, Condition: 100},
|
||
}
|
||
text := luigiShopGreeting("@user:test", equip, 5000, false)
|
||
|
||
if !strings.Contains(text, "Luigi's") {
|
||
t.Error("greeting should contain Luigi's")
|
||
}
|
||
if !strings.Contains(text, "€5000") {
|
||
t.Error("greeting should show balance")
|
||
}
|
||
if !strings.Contains(text, "Weapons") {
|
||
t.Error("greeting should show category grid")
|
||
}
|
||
if !strings.Contains(text, "Reply with a category") {
|
||
t.Error("greeting should prompt for category")
|
||
}
|
||
}
|
||
|
||
func TestLuigiShopGreeting_MaxedOut(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 5, Condition: 100},
|
||
SlotArmor: {Tier: 5, Condition: 100},
|
||
SlotHelmet: {Tier: 5, Condition: 100},
|
||
SlotBoots: {Tier: 5, Condition: 100},
|
||
SlotTool: {Tier: 5, Condition: 100},
|
||
}
|
||
text := luigiShopGreeting("@user:test", equip, 99999, false)
|
||
|
||
// Should NOT show category grid.
|
||
if strings.Contains(text, "Reply with a category") {
|
||
t.Error("maxed-out greeting should not show category prompt")
|
||
}
|
||
// Should use maxed-out flavor.
|
||
if !strings.Contains(text, "fully kitted") {
|
||
t.Error("maxed-out greeting should mention being fully kitted")
|
||
}
|
||
}
|
||
|
||
func TestLuigiShopGreeting_ShowAll(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{}
|
||
text := luigiShopGreeting("@user:test", equip, 1000, true)
|
||
|
||
// Should contain a show-all comment.
|
||
if !strings.Contains(text, "judgment") && !strings.Contains(text, "thoroughness") {
|
||
t.Error("show all greeting should contain a show-all comment")
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryView_UpgradeIndicators(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 2, Condition: 100, Name: "Dull Steel Sword of Mediocrity"},
|
||
}
|
||
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
|
||
|
||
// Should show current equipped.
|
||
if !strings.Contains(text, "🟢") {
|
||
t.Error("should show 🟢 for currently equipped item")
|
||
}
|
||
// Should show upgrade indicators.
|
||
if !strings.Contains(text, "⬆️") {
|
||
t.Error("should show ⬆️ for items above current tier")
|
||
}
|
||
// Should NOT show T1 (below current tier) by default.
|
||
if strings.Contains(text, "Sad Iron Sword") {
|
||
t.Error("should not show items below current tier when showAll=false")
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryView_ShowAll(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 3, Condition: 100, Name: "Sword (It's Fine)"},
|
||
}
|
||
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, true)
|
||
|
||
// Should show items below current tier.
|
||
if !strings.Contains(text, "Sad Iron Sword") {
|
||
t.Error("show all should include items below current tier")
|
||
}
|
||
// Below-tier items should show ⬇️.
|
||
if !strings.Contains(text, "⬇️") {
|
||
t.Error("show all should show ⬇️ for items below current tier")
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryView_Affordability(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 0, Condition: 100, Name: "Basic Ass Sword"},
|
||
}
|
||
// Can afford T1 (€100) but not T2 (€450).
|
||
text := luigiCategoryView("@user:test", SlotWeapon, equip, 200, false)
|
||
|
||
if !strings.Contains(text, "✅") {
|
||
t.Error("should show ✅ for affordable items")
|
||
}
|
||
if !strings.Contains(text, "💸") {
|
||
t.Error("should show 💸 for unaffordable items")
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryView_MasterworkAck(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 3, Condition: 100, Name: "MW Blade", Masterwork: true},
|
||
}
|
||
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
|
||
|
||
if !strings.Contains(text, "⭐") {
|
||
t.Error("should show ⭐ for masterwork gear")
|
||
}
|
||
// The masterwork acknowledgement pool has varied text — just check it's referenced.
|
||
hasMWAck := strings.Contains(text, "Masterwork") || strings.Contains(text, "Arena gear") || strings.Contains(text, "can't beat that")
|
||
if !hasMWAck {
|
||
t.Error("should acknowledge masterwork/arena gear")
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryView_MaxedSlot(t *testing.T) {
|
||
equip := map[EquipmentSlot]*AdvEquipment{
|
||
SlotWeapon: {Tier: 5, Condition: 100, Name: "Vorpal Sword"},
|
||
}
|
||
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
|
||
|
||
if !strings.Contains(text, "max tier") {
|
||
t.Error("should show max-tier message when nothing to buy")
|
||
}
|
||
}
|
||
|
||
// ── Item Confirm Tests ──────────────────────────────────────────────<E29480><E29480><EFBFBD>───────
|
||
|
||
func TestLuigiItemConfirm_Upgrade(t *testing.T) {
|
||
def := &EquipmentDef{Name: "Silver Blade", Tier: 3, Price: 1500}
|
||
current := &AdvEquipment{Tier: 2, Name: "Dull Steel Sword"}
|
||
text := luigiItemConfirm("@user:test", SlotWeapon, def, current, 5000)
|
||
|
||
if !strings.Contains(text, "⬆️") {
|
||
t.Error("should show upgrade indicator")
|
||
}
|
||
if !strings.Contains(text, "upgrade from") {
|
||
t.Error("should mention it's an upgrade")
|
||
}
|
||
if !strings.Contains(text, "€5000") {
|
||
t.Error("should show current balance")
|
||
}
|
||
if !strings.Contains(text, "€3500") {
|
||
t.Error("should show balance after purchase")
|
||
}
|
||
if !strings.Contains(text, "Confirm?") {
|
||
t.Error("should prompt for confirmation")
|
||
}
|
||
}
|
||
|
||
func TestLuigiItemConfirm_NoCurrent(t *testing.T) {
|
||
def := &EquipmentDef{Name: "Sad Iron Sword", Tier: 1, Price: 100}
|
||
text := luigiItemConfirm("@user:test", SlotWeapon, def, nil, 1000)
|
||
|
||
if !strings.Contains(text, "upgrade from") {
|
||
t.Error("should say upgrade from None")
|
||
}
|
||
}
|
||
|
||
// ── Item Lookup Tests ──────────────────────────────────────────────<E29480><E29480>────────
|
||
|
||
func TestAdvFindShopItemInSlot_ExactMatch(t *testing.T) {
|
||
slot, def, ok := advFindShopItemInSlot("Enchanted Plate", SlotArmor)
|
||
if !ok {
|
||
t.Fatal("should find Enchanted Plate in armor slot")
|
||
}
|
||
if slot != SlotArmor {
|
||
t.Errorf("slot should be armor, got %s", slot)
|
||
}
|
||
if def.Tier != 4 {
|
||
t.Errorf("Enchanted Plate should be tier 4, got %d", def.Tier)
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItemInSlot_WrongSlot(t *testing.T) {
|
||
_, _, ok := advFindShopItemInSlot("Dragonscale", SlotWeapon)
|
||
if ok {
|
||
t.Error("should not find armor item in weapon slot")
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItemInSlot_PartialMatch(t *testing.T) {
|
||
slot, def, ok := advFindShopItemInSlot("Enchanted Blade", SlotWeapon)
|
||
if !ok {
|
||
t.Fatal("should find Enchanted Blade by partial match")
|
||
}
|
||
if slot != SlotWeapon {
|
||
t.Errorf("slot should be weapon, got %s", slot)
|
||
}
|
||
if def.Tier != 4 {
|
||
t.Errorf("Enchanted Blade should be tier 4, got %d", def.Tier)
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItemInSlot_CaseInsensitive(t *testing.T) {
|
||
_, _, ok := advFindShopItemInSlot("enchanted blade", SlotWeapon)
|
||
if !ok {
|
||
t.Error("should find item case-insensitively")
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItemInSlot_NoTier0(t *testing.T) {
|
||
_, _, ok := advFindShopItemInSlot("Basic Ass Sword", SlotWeapon)
|
||
if ok {
|
||
t.Error("should not find tier 0 items (price 0)")
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItem_TierShorthand(t *testing.T) {
|
||
slot, def, ok := advFindShopItem("3 sword")
|
||
if !ok {
|
||
t.Fatal("should find via tier shorthand")
|
||
}
|
||
if slot != SlotWeapon {
|
||
t.Errorf("slot should be weapon, got %s", slot)
|
||
}
|
||
if def.Tier != 3 {
|
||
t.Errorf("tier should be 3, got %d", def.Tier)
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItem_TierPrefix(t *testing.T) {
|
||
_, def, ok := advFindShopItem("tier 5 boots")
|
||
if !ok {
|
||
t.Fatal("should find via 'tier 5 boots'")
|
||
}
|
||
if def.Tier != 5 {
|
||
t.Errorf("tier should be 5, got %d", def.Tier)
|
||
}
|
||
}
|
||
|
||
func TestAdvFindShopItem_TShorthand(t *testing.T) {
|
||
_, def, ok := advFindShopItem("t4 armor")
|
||
if !ok {
|
||
t.Fatal("should find via 't4 armor'")
|
||
}
|
||
if def.Tier != 4 {
|
||
t.Errorf("tier should be 4, got %d", def.Tier)
|
||
}
|
||
}
|
||
|
||
// ── Flavor Coverage Tests ───────────────────────────────────────────────────
|
||
|
||
func TestLuigiItemDescriptions_Coverage(t *testing.T) {
|
||
for _, slot := range allSlots {
|
||
defs := equipmentTiers[slot]
|
||
for _, def := range defs {
|
||
if def.Price == 0 {
|
||
continue // skip tier 0
|
||
}
|
||
key := luigiItemKey{slot, def.Tier}
|
||
if _, ok := luigiItemDescriptions[key]; !ok {
|
||
t.Errorf("missing Luigi item description for %s tier %d (%s)", slot, def.Tier, def.Name)
|
||
}
|
||
if _, ok := luigiItemOneLiners[key]; !ok {
|
||
t.Errorf("missing Luigi one-liner for %s tier %d (%s)", slot, def.Tier, def.Name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLuigiCategoryIntros_Coverage(t *testing.T) {
|
||
for _, slot := range allSlots {
|
||
intros, ok := luigiCategoryIntros[slot]
|
||
if !ok || len(intros) == 0 {
|
||
t.Errorf("missing Luigi category intros for slot %s", slot)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLuigiFlavorPools_NonEmpty(t *testing.T) {
|
||
pools := map[string][]string{
|
||
"luigiGreetings": luigiGreetings,
|
||
"luigiPurchaseConfirm": luigiPurchaseConfirm,
|
||
"luigiTier5Confirm": luigiTier5Confirm,
|
||
"luigiComboConfirm": luigiComboConfirm,
|
||
"luigiInsufficientFunds": luigiInsufficientFunds,
|
||
"luigiBrowseTimeout": luigiBrowseTimeout,
|
||
"luigiMaxedOut": luigiMaxedOut,
|
||
"luigiMasterworkAck": luigiMasterworkAck,
|
||
"luigiShowAllComment": luigiShowAllComment,
|
||
"luigiCommentary": luigiCommentary,
|
||
"luigiCancellation": luigiCancellation,
|
||
}
|
||
for name, pool := range pools {
|
||
if len(pool) == 0 {
|
||
t.Errorf("flavor pool %s is empty", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Format String Safety ────────────────────────────────────────────────────
|
||
|
||
func TestLuigiPurchaseConfirm_FormatStrings(t *testing.T) {
|
||
for i, tmpl := range luigiPurchaseConfirm {
|
||
// Should have %s then %d
|
||
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
|
||
if !result {
|
||
t.Errorf("luigiPurchaseConfirm[%d] missing %%s or %%d placeholder", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLuigiTier5Confirm_FormatStrings(t *testing.T) {
|
||
for i, tmpl := range luigiTier5Confirm {
|
||
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
|
||
if !result {
|
||
t.Errorf("luigiTier5Confirm[%d] missing %%s or %%d placeholder", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLuigiComboConfirm_FormatStrings(t *testing.T) {
|
||
for i, tmpl := range luigiComboConfirm {
|
||
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
|
||
if !result {
|
||
t.Errorf("luigiComboConfirm[%d] missing %%s or %%d placeholder", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Tier Bounds Safety ──────────────────────────────────────────────────────
|
||
|
||
func TestEquipmentTiers_ConsistentIndexing(t *testing.T) {
|
||
for _, slot := range allSlots {
|
||
defs := equipmentTiers[slot]
|
||
for i, def := range defs {
|
||
if def.Tier != i {
|
||
t.Errorf("slot %s: tier %d at index %d (should match for safe direct indexing)", slot, def.Tier, i)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestEquipmentTiers_SixTiersPerSlot(t *testing.T) {
|
||
for _, slot := range allSlots {
|
||
defs := equipmentTiers[slot]
|
||
if len(defs) != 6 {
|
||
t.Errorf("slot %s: expected 6 tiers (0-5), got %d", slot, len(defs))
|
||
}
|
||
}
|
||
}
|