Files
gogobee/internal/plugin/wordle_game_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

149 lines
4.8 KiB
Go

package plugin
import (
"testing"
)
func TestScoreGuess_ExactMatch(t *testing.T) {
results := scoreGuess("HELLO", "HELLO")
for i, r := range results {
if r != LetterCorrect {
t.Errorf("position %d: got %d, want LetterCorrect", i, r)
}
}
}
func TestScoreGuess_AllAbsent(t *testing.T) {
results := scoreGuess("XXXYZ", "HELLO")
for i, r := range results {
if r != LetterAbsent {
t.Errorf("position %d: got %d, want LetterAbsent", i, r)
}
}
}
func TestScoreGuess_MixedResults(t *testing.T) {
// WORLD vs HELLO:
// W=absent, O=present, R=absent, L=correct(pos3 matches), D=absent
results := scoreGuess("WORLD", "HELLO")
expected := []LetterResult{LetterAbsent, LetterPresent, LetterAbsent, LetterCorrect, LetterAbsent}
for i, r := range results {
if r != expected[i] {
t.Errorf("position %d: got %d, want %d", i, r, expected[i])
}
}
}
func TestScoreGuess_DuplicateLetters(t *testing.T) {
// SPEED vs ABIDE:
// S=absent, P=absent, E=present(E is in ABIDE pos 4), E=present? no, only one E in answer
// Actually: ABIDE has E at position 4 (0-indexed: A=0,B=1,I=2,D=3,E=4)
// SPEED: S=absent, P=absent, E=absent(no more E after first match), E=present, D=present
results := scoreGuess("SPEED", "ABIDE")
// S not in ABIDE -> absent
// P not in ABIDE -> absent
// E in ABIDE pos4 -> present (consumes the E)
// E again but no more E in pool -> absent
// D in ABIDE pos3 -> present
expected := []LetterResult{LetterAbsent, LetterAbsent, LetterPresent, LetterAbsent, LetterPresent}
for i, r := range results {
if r != expected[i] {
t.Errorf("position %d: got %d, want %d", i, r, expected[i])
}
}
}
func TestScoreGuess_DuplicateWithCorrect(t *testing.T) {
// LLAMA vs HELLO:
// L at 0: not correct (H), L is in HELLO -> present (consumes one L)
// L at 1: not correct (E), L is in HELLO -> present (consumes second L)
// A at 2: absent
// M at 3: absent
// A at 4: absent
// Wait, HELLO has L at positions 2,3. So pool after first pass has H,E,L,L,O
// Actually no first pass: no exact matches for LLAMA vs HELLO
// Pool = H,E,L,L,O
// L at 0: found L in pool, present (pool = H,E,L,O)
// L at 1: found L in pool, present (pool = H,E,O)
// A at 2: not found, absent
// M at 3: not found, absent
// A at 4: not found, absent
results := scoreGuess("LLAMA", "HELLO")
expected := []LetterResult{LetterPresent, LetterPresent, LetterAbsent, LetterAbsent, LetterAbsent}
for i, r := range results {
if r != expected[i] {
t.Errorf("position %d: got %d, want %d", i, r, expected[i])
}
}
}
func TestScoreGuess_CorrectTakesPriority(t *testing.T) {
// HELLO vs LLAMA: H=absent, E=absent, L=correct(pos2? no)
// HELLO vs LEMON: H=absent, E=present, L=present, L=absent, O=present
// Let's use a clearer case:
// ALLAY vs LLAMA:
// A at 0: not L -> but A is in LLAMA. First pass: check exact matches
// pos0: A vs L -> no
// pos1: L vs L -> CORRECT
// pos2: L vs A -> no
// pos3: A vs M -> no
// pos4: Y vs A -> no
// Pool (unmatched answer letters): L(0), A(2), M(3), A(4)
// Second pass:
// pos0: A -> found A in pool, present (pool: L, M, A)
// pos2: L -> found L in pool, present (pool: M, A)
// pos3: A -> found A in pool, present (pool: M)
// pos4: Y -> not found, absent
results := scoreGuess("ALLAY", "LLAMA")
expected := []LetterResult{LetterPresent, LetterCorrect, LetterPresent, LetterPresent, LetterAbsent}
for i, r := range results {
if r != expected[i] {
t.Errorf("position %d: got %d, want %d", i, r, expected[i])
}
}
}
func TestUpdateLetterStates_Upgrades(t *testing.T) {
states := map[rune]LetterResult{}
// First guess: A is absent
updateLetterStates(states, "A", []LetterResult{LetterAbsent})
if states['A'] != LetterAbsent {
t.Errorf("A should be Absent, got %d", states['A'])
}
// Second guess: A is present — should upgrade
updateLetterStates(states, "A", []LetterResult{LetterPresent})
if states['A'] != LetterPresent {
t.Errorf("A should be Present after upgrade, got %d", states['A'])
}
// Third guess: A is correct — should upgrade again
updateLetterStates(states, "A", []LetterResult{LetterCorrect})
if states['A'] != LetterCorrect {
t.Errorf("A should be Correct after upgrade, got %d", states['A'])
}
}
func TestUpdateLetterStates_NoDowngrade(t *testing.T) {
states := map[rune]LetterResult{
'A': LetterCorrect,
}
// A was correct, now appears absent in a different position — should NOT downgrade
updateLetterStates(states, "A", []LetterResult{LetterAbsent})
if states['A'] != LetterCorrect {
t.Errorf("A should remain Correct, got %d", states['A'])
}
}
func TestWordleBasePots(t *testing.T) {
// Verify reward structure: fewer guesses = more money
for i := 1; i < len(wordleBasePots)-1; i++ {
if wordleBasePots[i] <= wordleBasePots[i+1] {
t.Errorf("pot for %d guesses (%d) should be > pot for %d guesses (%d)",
i, wordleBasePots[i], i+1, wordleBasePots[i+1])
}
}
}