mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
The !setup confirm path seeds the canonical player_meta row (commit
667f87f), but the auto-migration path (ensureDnDCharacterForCombat) writes
a confirmed dnd_character without touching the legacy layer. Commands that
trigger it — !rest, !cast, !abilities, !skills — derive the adventure char
via a bare loadAdvCharacter that is nil when player_meta is absent, and
autoBuildCharacter tolerates a nil char. So a brand-new player whose
first-ever adventure action is one of those gets a confirmed character with
no player_meta, which then fails every legacy-layer command (expeditions,
arena, world boss, town, duels) with "sql: no rows" — the same state
@camcast was found in.
Fix: ensurePlayerMetaSeed guarantees the seed row (+ tier-0 equipment)
exists at the fresh auto-migration point. Conditional on player_meta being
absent, so it's idempotent and never duplicates a legacy player's gear
(createAdvCharacter's equipment insert has no conflict guard).
Regression tests cover both the straggler repro (first-ever !rest seeds
player_meta + loads cleanly) and idempotency (no equipment duplication).
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
82 lines
3.1 KiB
Go
82 lines
3.1 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gogobee/internal/db"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// TestAutoMigrationSeedsPlayerMeta guards the camcast straggler regression: a
|
|
// brand-new player whose first-ever adventure action auto-migrates a character
|
|
// (e.g. !rest before !setup) must come out with a canonical player_meta seed,
|
|
// or every legacy-layer command (expeditions, arena, world boss, town…) fails
|
|
// with "sql: no rows". Before the ensurePlayerMetaSeed guard this produced a
|
|
// confirmed dnd_character with zero player_meta rows.
|
|
func TestAutoMigrationSeedsPlayerMeta(t *testing.T) {
|
|
setupEmptyTestDB(t)
|
|
uid := id.UserID("@fresh-automigrant:example.org")
|
|
|
|
p := &AdventurePlugin{euro: &EuroPlugin{}}
|
|
p.Sink = &captureSink{}
|
|
|
|
// First-ever adventure action for a player with no character at all.
|
|
if err := p.handleDnDShortRest(MessageContext{Sender: uid}); err != nil {
|
|
t.Fatalf("handleDnDShortRest: %v", err)
|
|
}
|
|
|
|
d := db.Get()
|
|
var dndRows, pmRows, equipRows, pending int
|
|
d.QueryRow(`SELECT COUNT(*) FROM dnd_character WHERE user_id=?`, string(uid)).Scan(&dndRows)
|
|
d.QueryRow(`SELECT COUNT(*) FROM player_meta WHERE user_id=?`, string(uid)).Scan(&pmRows)
|
|
d.QueryRow(`SELECT COUNT(*) FROM adventure_equipment WHERE user_id=?`, string(uid)).Scan(&equipRows)
|
|
d.QueryRow(`SELECT COALESCE(MAX(pending_setup),-1) FROM dnd_character WHERE user_id=?`, string(uid)).Scan(&pending)
|
|
|
|
if dndRows != 1 {
|
|
t.Fatalf("expected 1 auto-migrated dnd_character, got %d", dndRows)
|
|
}
|
|
if pending != 0 {
|
|
t.Errorf("auto-migrated character should be confirmed (pending_setup=0), got %d", pending)
|
|
}
|
|
if pmRows != 1 {
|
|
t.Errorf("player_meta not seeded for auto-migrant: got %d rows, want 1 (camcast straggler regression)", pmRows)
|
|
}
|
|
// createAdvCharacter seeds tier-0 gear in all five slots.
|
|
if equipRows != len(allSlots) {
|
|
t.Errorf("tier-0 equipment not seeded: got %d rows, want %d", equipRows, len(allSlots))
|
|
}
|
|
|
|
// The character must now load through the legacy layer that camcast choked on.
|
|
if _, err := loadAdvCharacter(uid); err != nil {
|
|
t.Errorf("loadAdvCharacter after auto-migration: %v (this is the exact failure camcast hit)", err)
|
|
}
|
|
}
|
|
|
|
// TestEnsurePlayerMetaSeedIdempotent proves the guard never duplicates a legacy
|
|
// player's equipment — createAdvCharacter's tier-0 insert has no conflict guard,
|
|
// so the seed must be conditional on player_meta being absent.
|
|
func TestEnsurePlayerMetaSeedIdempotent(t *testing.T) {
|
|
setupEmptyTestDB(t)
|
|
uid := id.UserID("@legacy:example.org")
|
|
|
|
// Seed once (fresh), then again (should be a no-op).
|
|
if err := ensurePlayerMetaSeed(uid); err != nil {
|
|
t.Fatalf("first seed: %v", err)
|
|
}
|
|
if err := ensurePlayerMetaSeed(uid); err != nil {
|
|
t.Fatalf("second seed: %v", err)
|
|
}
|
|
|
|
d := db.Get()
|
|
var pmRows, equipRows int
|
|
d.QueryRow(`SELECT COUNT(*) FROM player_meta WHERE user_id=?`, string(uid)).Scan(&pmRows)
|
|
d.QueryRow(`SELECT COUNT(*) FROM adventure_equipment WHERE user_id=?`, string(uid)).Scan(&equipRows)
|
|
if pmRows != 1 {
|
|
t.Errorf("player_meta rows = %d, want 1 (no duplication)", pmRows)
|
|
}
|
|
if equipRows != len(allSlots) {
|
|
t.Errorf("equipment rows = %d, want %d (guard must not re-insert gear)", equipRows, len(allSlots))
|
|
}
|
|
}
|