mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Hands a consumable or non-magic gear item to another adventurer. The sender pays a 5% handling fee (of item value) to the community pot — the same civic tax every payout pays — and is capped at 3 gifts/day (a persisted log doubling as an anti-twink-funnel guard and audit trail). Recipient must have run !setup. Magic items are deliberately non-giftable: attunement and the BiS economy stay personal. Built on the vault's inventory plumbing: a gift is transferInventoryItem flipping the row's owner (owner-scoped, forces in_vault=0 so it lands in the recipient's active pack). pickGiftableItem prefers a giftable match so a non-giftable item can't shadow a giftable one and block the command. go test ./... green; golden byte-identical. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
146 lines
4.6 KiB
Go
146 lines
4.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
func TestIsGiftableItem(t *testing.T) {
|
|
cases := []struct {
|
|
typ string
|
|
want bool
|
|
}{
|
|
{"consumable", true},
|
|
{"MasterworkGear", true},
|
|
{"ArenaGear", true},
|
|
{"magic_item", false},
|
|
{"treasure", false},
|
|
{"ore", false},
|
|
{"card", false},
|
|
}
|
|
for _, c := range cases {
|
|
got, reason := isGiftableItem(AdvItem{Type: c.typ, Name: "X"})
|
|
if got != c.want {
|
|
t.Errorf("isGiftableItem(%q) = %v, want %v (reason %q)", c.typ, got, c.want, reason)
|
|
}
|
|
if !got && reason == "" {
|
|
t.Errorf("isGiftableItem(%q) rejected with no reason", c.typ)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPickGiftableItem: a non-giftable item must not shadow a giftable one that
|
|
// also matches the query.
|
|
func TestPickGiftableItem(t *testing.T) {
|
|
inv := []AdvItem{
|
|
{ID: 1, Name: "Dragon Scale", Type: "material", Tier: 5, Value: 9000},
|
|
{ID: 2, Name: "Dragon Draught", Type: "consumable", Tier: 1, Value: 200},
|
|
}
|
|
// "dragon" matches the Scale first (higher tier), but the Draught is the
|
|
// giftable one — pick it, don't refuse.
|
|
got, reason := pickGiftableItem(inv, "dragon")
|
|
if got == nil {
|
|
t.Fatalf("pickGiftableItem returned nil (reason %q), want the giftable Draught", reason)
|
|
}
|
|
if got.Name != "Dragon Draught" {
|
|
t.Errorf("picked %q, want Dragon Draught", got.Name)
|
|
}
|
|
|
|
// Only a non-giftable match → refuse with a reason.
|
|
onlyMagic := []AdvItem{{ID: 3, Name: "Ring of Power", Type: "magic_item", Value: 5000}}
|
|
got, reason = pickGiftableItem(onlyMagic, "ring")
|
|
if got != nil || reason == "" {
|
|
t.Errorf("magic-only match: got=%v reason=%q, want nil with a refusal reason", got, reason)
|
|
}
|
|
|
|
// No match at all → nil, empty reason.
|
|
got, reason = pickGiftableItem(inv, "nonexistent")
|
|
if got != nil || reason != "" {
|
|
t.Errorf("no match: got=%v reason=%q, want nil/empty", got, reason)
|
|
}
|
|
}
|
|
|
|
// TestTransferInventoryItem: an item moves owners exactly, and a wrong-owner
|
|
// transfer is a no-op.
|
|
func TestTransferInventoryItem(t *testing.T) {
|
|
vaultTestDB(t)
|
|
alice := id.UserID("@alice:test.invalid")
|
|
bob := id.UserID("@bob:test.invalid")
|
|
if err := addAdvInventoryItem(alice, AdvItem{Name: "Health Potion", Type: "consumable", Tier: 1, Value: 200}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inv, _ := loadAdvInventory(alice)
|
|
if len(inv) != 1 {
|
|
t.Fatalf("setup: alice has %d items, want 1", len(inv))
|
|
}
|
|
itemID := inv[0].ID
|
|
|
|
// Wrong owner can't move it.
|
|
if moved, err := transferInventoryItem(itemID, bob, alice); err != nil || moved {
|
|
t.Fatalf("wrong-owner transfer moved=%v err=%v, want no-op", moved, err)
|
|
}
|
|
|
|
// Real owner moves it to bob.
|
|
moved, err := transferInventoryItem(itemID, alice, bob)
|
|
if err != nil || !moved {
|
|
t.Fatalf("transfer moved=%v err=%v, want success", moved, err)
|
|
}
|
|
if a, _ := loadAdvInventory(alice); len(a) != 0 {
|
|
t.Errorf("alice still has %d items after gifting", len(a))
|
|
}
|
|
b, _ := loadAdvInventory(bob)
|
|
if len(b) != 1 || b[0].Name != "Health Potion" {
|
|
t.Errorf("bob's inventory = %+v, want the potion", b)
|
|
}
|
|
}
|
|
|
|
// TestTransferUnvaultsItem: gifting a currently-vaulted item lands it in the
|
|
// recipient's active pack, not a phantom vault slot.
|
|
func TestTransferUnvaultsItem(t *testing.T) {
|
|
vaultTestDB(t)
|
|
alice := id.UserID("@alice2:test.invalid")
|
|
bob := id.UserID("@bob2:test.invalid")
|
|
if err := addAdvInventoryItem(alice, AdvItem{Name: "Elixir", Type: "consumable", Tier: 1, Value: 100}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inv, _ := loadAdvInventory(alice)
|
|
if _, err := setItemVaulted(alice, inv[0].ID, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := transferInventoryItem(inv[0].ID, alice, bob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Bob sees it in active inventory (not vault).
|
|
if b, _ := loadAdvInventory(bob); len(b) != 1 {
|
|
t.Errorf("bob active inventory = %d, want 1 (item un-vaulted on gift)", len(b))
|
|
}
|
|
if bv, _ := loadAdvVault(bob); len(bv) != 0 {
|
|
t.Errorf("bob vault = %d, want 0", len(bv))
|
|
}
|
|
}
|
|
|
|
// TestGiftDailyCapCounting: logGift increments the day's count and the count is
|
|
// day-scoped.
|
|
func TestGiftDailyCapCounting(t *testing.T) {
|
|
vaultTestDB(t)
|
|
sender := id.UserID("@giver:test.invalid")
|
|
recip := id.UserID("@taker:test.invalid")
|
|
|
|
if n := giftCountToday(sender, "2026-07-10"); n != 0 {
|
|
t.Fatalf("initial count = %d, want 0", n)
|
|
}
|
|
for i := 0; i < giftDailyCap; i++ {
|
|
if err := logGift(sender, recip, "Potion", 100, "2026-07-10"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if n := giftCountToday(sender, "2026-07-10"); n != giftDailyCap {
|
|
t.Errorf("count = %d, want %d", n, giftDailyCap)
|
|
}
|
|
// A different day is a fresh allowance.
|
|
if n := giftCountToday(sender, "2026-07-11"); n != 0 {
|
|
t.Errorf("next-day count = %d, want 0", n)
|
|
}
|
|
}
|