Full codebase audit: 21 security/robustness fixes, 328 tests across 4 packages

Security & economy:
- Credit()/Debit() reject non-positive amounts (closes infinite-money exploit)
- Lottery ticket purchase uses in-transaction count check (closes TOCTOU race)
- Arena bail channel ownership prevents double-close panic
- Entry.ID validated before exec.Command in fetch-esteemed
- Internal errors no longer leaked to users (forex, esteemed)

Robustness:
- safeGo() panic recovery added to 17 goroutine launch sites across 14 plugins
- db.Close() added and called on shutdown
- Miniflux mutex pattern fixed (snapshot-under-lock)
- Silently ignored DB/JSON errors now logged (lottery_db)

Performance:
- Added indexes: idx_arena_runs_user(user_id, status), idx_euro_bal_user(user_id)

UX:
- Empty !buy args shows usage instead of "No item matching ''"
- "Failed to load character" now suggests !adventure

Test coverage:
- internal/util/parser_test.go (19 tests: XP, levels, progress bar, commands, parsing)
- internal/crypto/crypto_test.go (12 tests: encrypt/decrypt, HMAC, ParseKey)
- internal/plugin/helpers_test.go (30 tests: formatNumber, calc, lottery, chat perks)
- internal/plugin/audit_fixes_test.go (25 tests: safeGo, bail channels, error leaks, overflow)
- internal/db/db_test.go (4 tests: Init, Close, schema indexes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-08 17:04:40 -07:00
parent 68b2f8b7a5
commit e459b6e78d
34 changed files with 2443 additions and 486 deletions

View File

@@ -8,9 +8,13 @@ import (
// ── Arena Tier Menu ─────────────────────────────────────────────────────────
func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) string {
func renderArenaStreakEntry(char *AdventureCharacter, stats *ArenaPersonalStats, tier *ArenaTier, firstMonster *ArenaMonster) string {
var b strings.Builder
b.WriteString("⚔️ **THE ARENA**\n\n")
b.WriteString("The Arena is a streak. You start at Tier 1 and fight your way down.\n")
b.WriteString("After each tier, you have 30 seconds to bail or you auto-advance.\n")
b.WriteString("Death forfeits all accumulated rewards.\n\n")
b.WriteString(fmt.Sprintf("Combat Level: %d\n\n", char.CombatLevel))
for i := range arenaTiers {
@@ -18,12 +22,13 @@ func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) st
eligible := char.CombatLevel >= t.MinLevel
icon := "🔒"
if eligible {
icon = "⬚" // eligible but not cleared
icon = "⬚"
}
if stats != nil && stats.HighestTier >= t.Number {
icon = "✅" // cleared
icon = "✅"
}
b.WriteString(fmt.Sprintf("%s **Tier %d — %s** (Lv.%d+)\n", icon, t.Number, t.Name, t.MinLevel))
mult := arenaStreakEuroMultiplier[t.Number]
b.WriteString(fmt.Sprintf("%s **Tier %d — %s** (Lv.%d+) — %.1f× euros\n", icon, t.Number, t.Name, t.MinLevel, mult))
}
if stats != nil && stats.TotalRuns > 0 {
@@ -34,9 +39,11 @@ func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) st
b.WriteString("\n⛑ _Today's helmets are provided by Brim & Battle — celebrated makers of baseball hats — and proud sponsors of the Arena. ")
b.WriteString("Their new VeriFort line of field-evaluated combat headgear represents an exciting expansion beyond their area of expertise. ")
b.WriteString("Winners may receive a complimentary sample. Brim & Battle thanks you for your participation in their ongoing verification process._\n\n")
b.WriteString("`!arena tier <1-5>` — Enter a tier\n")
b.WriteString("`!arena stats` — Your arena stats\n")
b.WriteString("`!arena leaderboard` — Top arena players\n")
b.WriteString(fmt.Sprintf("**Round 1 opponent: %s**\n", firstMonster.Name))
b.WriteString(fmt.Sprintf("_%s_\n\n", firstMonster.Flavor))
b.WriteString("`!arena fight` — Enter and fight Round 1\n")
b.WriteString("`!arena cancel` — Back out")
return b.String()
}
@@ -48,60 +55,23 @@ func renderArenaRoundStart(tier *ArenaTier, round int, monster *ArenaMonster, ru
b.WriteString(fmt.Sprintf("**%s**\n", monster.Name))
b.WriteString(fmt.Sprintf("_%s_\n\n", monster.Flavor))
if run.Earnings > 0 {
b.WriteString(fmt.Sprintf("Run earnings: €%d (at risk)\n\n", run.Earnings))
sessionTotal := run.Earnings + run.TierEarnings
if sessionTotal > 0 {
b.WriteString(fmt.Sprintf("Session earnings: €%d (at risk)\n\n", sessionTotal))
}
b.WriteString("`!arena fight` — Face this opponent\n")
return b.String()
}
// ── Survival ────────────────────────────────────────────────────────────────
func renderArenaSurvival(tier *ArenaTier, round int, monster *ArenaMonster, reward int64, xp int, totalEarnings int64) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("✅ **%s defeated.**\n\n", monster.Name))
b.WriteString(fmt.Sprintf("Round reward: €%d\n", reward))
b.WriteString(fmt.Sprintf("Battle XP: +%d\n", xp))
b.WriteString(fmt.Sprintf("Run total: €%d\n", totalEarnings))
return b.String()
}
// renderArenaSurvival removed — survival text is built by renderArenaCombatLog + inline formatting.
// ── Tier Complete (Transition Prompt) ───────────────────────────────────────
func renderArenaTierComplete(tier *ArenaTier, completionBonus int64, totalEarnings int64) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("🏆 **Tier %d — %s cleared!**\n\n", tier.Number, tier.Name))
b.WriteString(fmt.Sprintf("Completion bonus: €%d\n", completionBonus))
b.WriteString(fmt.Sprintf("Run total: €%d\n\n", totalEarnings))
// renderArenaTierComplete is no longer used — tier complete text is built inline
// in resolveArenaSurvival and the countdown DM handles the opt-out flow.
if tier.Number < 5 {
nextTier := arenaGetTier(tier.Number + 1)
b.WriteString(fmt.Sprintf("Descend to Tier %d — %s? Your earnings are at risk.\n\n", nextTier.Number, nextTier.Name))
b.WriteString(fmt.Sprintf("`!arena descend` — Enter Tier %d (earnings carry over, still at risk)\n", nextTier.Number))
b.WriteString(fmt.Sprintf("`!arena cashout` — Take your €%d and leave\n\n", totalEarnings))
b.WriteString("_You have 10 minutes to decide. After that, GogoBee collects on your behalf._")
}
return b.String()
}
// ── Tier 5 Complete ─────────────────────────────────────────────────────────
func renderArenaTier5Complete(totalEarnings int64, startTier int) string {
var b strings.Builder
b.WriteString("🏆🏆🏆 **THE ARENA HAS BEEN CONQUERED.**\n\n")
b.WriteString("That Which Has Always Been has fallen. The machine has logged a loss.\n")
b.WriteString("The crowd is silent. Not out of respect — out of disbelief.\n\n")
b.WriteString(fmt.Sprintf("**Total earnings: €%d**\n\n", totalEarnings))
b.WriteString("Your euros have been credited. Your name has been etched. The Arena remembers.")
if startTier == 1 {
b.WriteString("\n\n_All the way down. From Tier 1 to Tier 5 in a single run. Statistically impossible. Empirically: you._")
}
return b.String()
}
// renderArenaTier5Complete removed — T5 completion is part of arenaCompleteSession payout summary.
// ── Death ────────────────────────────────────────────────────────────────────
@@ -117,25 +87,7 @@ func renderArenaDeath(tier *ArenaTier, round int, monster *ArenaMonster, lostEar
return b.String()
}
// ── Cashout ─────────────────────────────────────────────────────────────────
func renderArenaCashout(totalEarnings int64, lastTier int) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("💰 **Cashed out: €%d**\n\n", totalEarnings))
b.WriteString(fmt.Sprintf("You cleared through Tier %d and lived to spend it. ", lastTier))
b.WriteString("Wisdom or cowardice — the euros don't care which.")
return b.String()
}
// ── Auto-Cashout ────────────────────────────────────────────────────────────
func renderArenaAutoCashout(totalEarnings int64) string {
return fmt.Sprintf(
"⏰ **Auto-cashout: €%d**\n\n"+
"You took too long to decide. GogoBee collected your winnings on your behalf "+
"and is annoyed about it. The money is in your account. You're welcome.",
totalEarnings)
}
// renderArenaCashout and renderArenaAutoCashout removed — payout handled by arenaCompleteSession.
// ── Status ──────────────────────────────────────────────────────────────────
@@ -146,9 +98,11 @@ func renderArenaStatus(run *ArenaRun, char *AdventureCharacter) string {
}
var b strings.Builder
b.WriteString("⚔️ **Arena Run Status**\n\n")
b.WriteString("⚔️ **Arena Streak Status**\n\n")
b.WriteString(fmt.Sprintf("Tier: %d — %s\n", tier.Number, tier.Name))
sessionTotal := run.Earnings + run.TierEarnings
switch run.Status {
case "active":
monster := arenaGetMonster(run.Tier, run.Round)
@@ -156,14 +110,14 @@ func renderArenaStatus(run *ArenaRun, char *AdventureCharacter) string {
if monster != nil {
b.WriteString(fmt.Sprintf("Opponent: %s\n", monster.Name))
}
b.WriteString(fmt.Sprintf("Earnings: €%d (at risk)\n", run.Earnings))
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
b.WriteString("\n`!arena fight` to continue")
case "awaiting":
b.WriteString(fmt.Sprintf("Tier %d cleared — awaiting decision\n", run.Tier))
b.WriteString(fmt.Sprintf("Earnings: €%d (at risk)\n", run.Earnings))
b.WriteString(fmt.Sprintf("Tier %d cleared — advancing shortly\n", run.Tier))
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
b.WriteString("\n`!arena descend` or `!arena cashout`")
b.WriteString("\n`!bail` to collect your rewards")
}
return b.String()
@@ -207,19 +161,6 @@ func renderArenaLeaderboard(entries []ArenaLeaderboardEntry) string {
return b.String()
}
// ── Tier Entry Confirmation ──────────────────────────────────────────────────
func renderArenaTierConfirm(tier *ArenaTier, firstMonster *ArenaMonster) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("⚔️ **Tier %d — %s**\n\n", tier.Number, tier.Name))
b.WriteString(fmt.Sprintf("4 rounds. You cannot leave mid-tier. Death forfeits all earnings.\n\n"))
b.WriteString(fmt.Sprintf("Round 1 opponent: **%s**\n", firstMonster.Name))
b.WriteString(fmt.Sprintf("_%s_\n\n", firstMonster.Flavor))
b.WriteString("`!arena fight` — Enter and fight Round 1\n")
b.WriteString("`!arena cancel` — Back out")
return b.String()
}
// ── Personal Stats ──────────────────────────────────────────────────────────
type ArenaPersonalStats struct {
@@ -305,12 +246,11 @@ func renderArenaAlreadyInRun(run *ArenaRun) string {
switch run.Status {
case "awaiting":
return fmt.Sprintf(
"You have a pending arena decision. Tier %d cleared, €%d at risk.\n\n"+
"`!arena descend` or `!arena cashout`",
"Tier %d cleared, €%d accumulated. Waiting for next tier or `!bail` to collect.\n",
run.Tier, run.Earnings)
default:
return fmt.Sprintf(
"You're already in an arena run. Tier %d, Round %d.\n\n"+
"You're in an arena streak. Tier %d, Round %d.\n\n"+
"`!arena fight` to continue.",
run.Tier, run.Round)
}