Files
gogobee/internal/plugin/uno_test.go
prosolis 9c6ded13fa Add forex plugin, stability fixes, and async HTTP dispatch
- Add forex plugin (Frankfurter v2 API) with rate lookups, analysis,
  DM-based alerts, and daily cron poll. Backfills 1 year of history
  on startup for moving averages and buy signal scoring.

- Fix bot hang caused by SQLite lock contention in reminder polling:
  rows cursor was held open while writing to the same DB. Collect
  results first, close cursor, then process. Same fix in milkcarton.

- Add sync retry loop so the bot reconnects after network drops
  instead of silently exiting. StopSync() for clean Ctrl+C shutdown.

- Add panic recovery to all dispatch, syncer, and cron paths.

- Make all HTTP-calling plugin commands async (goroutines) so a slow
  or dead external API cannot block the message dispatch pipeline.
  Affects: lookup, stocks, forex, anime, movies, concerts, gaming,
  retro, wotd, urls, howami.

- Extract DisplayName to Base, add db.Exec helper, convert silent
  error discards across the codebase.

- Fix UNO mercy-kill bug (eliminated bot continues playing), adventure
  DM nag spam, stats column mismatch, per-call regex/replacer allocs.

- Update README: forex commands, Finance section, 47 plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 09:22:02 -07:00

291 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import "testing"
// ---------------------------------------------------------------------------
// Card properties
// ---------------------------------------------------------------------------
func TestUnoCard_IsWild(t *testing.T) {
tests := []struct {
name string
card unoCard
want bool
}{
{"wild", unoCard{unoWild, unoWildCard}, true},
{"wild draw four", unoCard{unoWild, unoWildDrawFour}, true},
{"wild draw six", unoCard{unoWild, unoWildDrawSix}, true},
{"wild draw ten", unoCard{unoWild, unoWildDrawTen}, true},
{"wild reverse draw 4", unoCard{unoWild, unoWildReverseDraw4}, true},
{"wild color roulette", unoCard{unoWild, unoWildColorRoulette}, true},
{"red skip", unoCard{unoRed, unoSkip}, false},
{"blue 5", unoCard{unoBlue, unoFive}, false},
{"green draw two", unoCard{unoGreen, unoDrawTwo}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.isWild(); got != tt.want {
t.Errorf("isWild() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnoValue_IsAction(t *testing.T) {
actions := []unoValue{unoSkip, unoReverse, unoDrawTwo, unoWildCard, unoWildDrawFour,
unoSkipEveryone, unoDrawFour, unoDiscardAll, unoWildReverseDraw4,
unoWildDrawSix, unoWildDrawTen, unoWildColorRoulette}
for _, v := range actions {
if !v.isAction() {
t.Errorf("%s should be an action card", v)
}
}
numbers := []unoValue{unoZero, unoOne, unoTwo, unoThree, unoFour, unoFive, unoSix, unoSeven, unoEight, unoNine}
for _, v := range numbers {
if v.isAction() {
t.Errorf("%s should NOT be an action card", v)
}
}
}
// ---------------------------------------------------------------------------
// canPlayOn
// ---------------------------------------------------------------------------
func TestCanPlayOn(t *testing.T) {
tests := []struct {
name string
card unoCard
top unoCard
topColor unoColor
want bool
}{
{"wild always playable", unoCard{unoWild, unoWildCard}, unoCard{unoRed, unoFive}, unoRed, true},
{"wd4 always playable", unoCard{unoWild, unoWildDrawFour}, unoCard{unoBlue, unoThree}, unoBlue, true},
{"same color", unoCard{unoRed, unoThree}, unoCard{unoRed, unoSeven}, unoRed, true},
{"same value diff color", unoCard{unoBlue, unoFive}, unoCard{unoRed, unoFive}, unoRed, true},
{"diff color diff value", unoCard{unoBlue, unoThree}, unoCard{unoRed, unoSeven}, unoRed, false},
{"matches chosen color not card color", unoCard{unoGreen, unoTwo}, unoCard{unoWild, unoWildCard}, unoGreen, true},
{"doesnt match chosen color", unoCard{unoRed, unoTwo}, unoCard{unoWild, unoWildCard}, unoGreen, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.canPlayOn(tt.top, tt.topColor); got != tt.want {
t.Errorf("canPlayOn() = %v, want %v", got, tt.want)
}
})
}
}
// ---------------------------------------------------------------------------
// Deck sizes
// ---------------------------------------------------------------------------
func TestNewUnoDeck_Size(t *testing.T) {
deck := newUnoDeck()
// Standard UNO: 4 colors × (1 zero + 2×12 other) + 8 wilds = 4×25 + 8 = 108
if len(deck) != 108 {
t.Errorf("standard deck size = %d, want 108", len(deck))
}
}
func TestNewNoMercyDeck_Size(t *testing.T) {
deck := newNoMercyDeck()
if len(deck) < 108 {
t.Errorf("no mercy deck should be larger than standard, got %d", len(deck))
}
}
// ---------------------------------------------------------------------------
// Bot AI
// ---------------------------------------------------------------------------
func TestBotPickCard_NoPlayable(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoRed, unoTwo},
}
top := unoCard{unoBlue, unoSeven}
_, idx := botPickCard(hand, top, unoBlue, false, 5)
if idx != -1 {
t.Errorf("should return -1 when nothing playable, got %d", idx)
}
}
func TestBotPickCard_PlaysPlayable(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoBlue, unoSeven}, // matches top
}
top := unoCard{unoBlue, unoFive}
card, idx := botPickCard(hand, top, unoBlue, false, 5)
if idx == -1 {
t.Fatal("should find a playable card")
}
if !card.canPlayOn(top, unoBlue) {
t.Error("picked card should be playable on top")
}
}
func TestBotPickCard_AggressiveUsesWD4(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoWild, unoWildDrawFour},
{unoBlue, unoSeven},
}
top := unoCard{unoBlue, unoFive}
// bookDown=true triggers aggressive mode
card, idx := botPickCard(hand, top, unoBlue, true, 5)
if idx == -1 {
t.Fatal("should find a playable card")
}
if card.Value != unoWildDrawFour {
t.Errorf("aggressive mode should prefer WD4, got %s", card.Value)
}
}
func TestBotPickColor_MostCommon(t *testing.T) {
hand := []unoCard{
{unoRed, unoOne},
{unoRed, unoTwo},
{unoRed, unoThree},
{unoBlue, unoFive},
{unoWild, unoWildCard}, // wilds don't count
}
color := botPickColor(hand)
if color != unoRed {
t.Errorf("should pick red (3 cards), got %s", color)
}
}
func TestBotPickColor_EmptyHand(t *testing.T) {
// Should default to red with no cards
color := botPickColor([]unoCard{})
if color != unoRed {
t.Errorf("empty hand should default to red, got %s", color)
}
}
// ---------------------------------------------------------------------------
// No Mercy helpers
// ---------------------------------------------------------------------------
func TestCardDrawValue(t *testing.T) {
tests := []struct {
value unoValue
want int
}{
{unoDrawTwo, 2},
{unoDrawFour, 4},
{unoWildReverseDraw4, 4},
{unoWildDrawSix, 6},
{unoWildDrawTen, 10},
{unoOne, 0},
{unoSkip, 0},
{unoWildCard, 0},
}
for _, tt := range tests {
if got := cardDrawValue(tt.value); got != tt.want {
t.Errorf("cardDrawValue(%s) = %d, want %d", tt.value, got, tt.want)
}
}
}
func TestIsDrawCard(t *testing.T) {
if !isDrawCard(unoDrawTwo) {
t.Error("Draw Two should be a draw card")
}
if !isDrawCard(unoWildDrawTen) {
t.Error("Wild Draw Ten should be a draw card")
}
if isDrawCard(unoSkip) {
t.Error("Skip should not be a draw card")
}
}
func TestCanPlayOnStacking(t *testing.T) {
tests := []struct {
name string
card unoCard
topColor unoColor
stackMinValue int
want bool
}{
{"wild draw six stacks on 2", unoCard{unoWild, unoWildDrawSix}, unoRed, 2, true},
{"draw two on draw two same color", unoCard{unoRed, unoDrawTwo}, unoRed, 2, true},
{"draw two wrong color", unoCard{unoBlue, unoDrawTwo}, unoRed, 2, false},
{"number cant stack", unoCard{unoRed, unoFive}, unoRed, 2, false},
{"draw two cant stack on 4", unoCard{unoRed, unoDrawTwo}, unoRed, 4, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.card.canPlayOnStacking(tt.topColor, tt.stackMinValue); got != tt.want {
t.Errorf("canPlayOnStacking() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasStackableCard(t *testing.T) {
hand := []unoCard{
{unoRed, unoFive},
{unoBlue, unoDrawTwo},
{unoGreen, unoSkip},
}
if hasStackableCard(hand, unoBlue, 2) != true {
t.Error("should find blue draw two as stackable")
}
if hasStackableCard(hand, unoRed, 2) != false {
t.Error("no red draw cards in hand")
}
}
func TestParseNoMercyFlags(t *testing.T) {
tests := []struct {
input string
noMercy bool
sevenZero bool
amount string
}{
{"nomercy", true, false, ""},
{"nomercy 50", true, false, "50"},
{"nomercy7-0", true, true, ""},
{"nomercy7-0 100", true, true, "100"},
{"50", false, false, "50"},
{"", false, false, ""},
{"NOMERCY", true, false, ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
nm, sz, amt := parseNoMercyFlags(tt.input)
if nm != tt.noMercy || sz != tt.sevenZero || amt != tt.amount {
t.Errorf("parseNoMercyFlags(%q) = (%v, %v, %q), want (%v, %v, %q)",
tt.input, nm, sz, amt, tt.noMercy, tt.sevenZero, tt.amount)
}
})
}
}
// ---------------------------------------------------------------------------
// Color/Value string representations
// ---------------------------------------------------------------------------
func TestUnoColor_String(t *testing.T) {
if unoRed.String() != "Red" {
t.Errorf("red: got %q", unoRed.String())
}
if unoWild.String() != "Wild" {
t.Errorf("wild: got %q", unoWild.String())
}
}
func TestUnoColor_Emoji(t *testing.T) {
if unoRed.Emoji() != "🟥" {
t.Errorf("red emoji: got %q", unoRed.Emoji())
}
if unoBlue.Emoji() != "🟦" {
t.Errorf("blue emoji: got %q", unoBlue.Emoji())
}
}