diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index 6ed76c6..52ff1b0 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -853,7 +853,7 @@ func (p *AdventurePlugin) resolvePendingInteraction(ctx MessageContext, interact case "pet_type": return p.resolvePetType(ctx) case "pet_name": - return p.resolvePetName(ctx) + return p.resolvePetName(ctx, interaction) } return nil } diff --git a/internal/plugin/adventure_pets.go b/internal/plugin/adventure_pets.go index efad499..e1bf26b 100644 --- a/internal/plugin/adventure_pets.go +++ b/internal/plugin/adventure_pets.go @@ -329,8 +329,11 @@ func (p *AdventurePlugin) resolvePetType(ctx MessageContext) error { article, titleCase(petType))) } -// resolvePetName handles naming the pet. -func (p *AdventurePlugin) resolvePetName(ctx MessageContext) error { +// resolvePetName handles naming the pet. The dispatcher (handlePendingReply) +// has already deleted the pending interaction and passes it in, so this must +// NOT re-load from p.pending — doing so previously made every adoption fail +// silently (the carried PetType was lost and the save never ran). +func (p *AdventurePlugin) resolvePetName(ctx MessageContext, interaction *advPendingInteraction) error { userMu := p.advUserLock(ctx.Sender) userMu.Lock() defer userMu.Unlock() @@ -340,20 +343,15 @@ func (p *AdventurePlugin) resolvePetName(ctx MessageContext) error { return p.SendDM(ctx.Sender, "Failed to load your character.") } - val, ok := p.pending.LoadAndDelete(string(ctx.Sender)) - if !ok { - return nil - } - pi := val.(*advPendingInteraction) - data := pi.Data.(*advPendingPetName) + data := interaction.Data.(*advPendingPetName) name := strings.TrimSpace(ctx.Body) if len(name) == 0 || len(name) > 30 { - p.pending.Store(string(ctx.Sender), pi) + p.pending.Store(string(ctx.Sender), interaction) return p.SendDM(ctx.Sender, "Name must be 1-30 characters. Try again.") } if !petNameValid.MatchString(name) { - p.pending.Store(string(ctx.Sender), pi) + p.pending.Store(string(ctx.Sender), interaction) return p.SendDM(ctx.Sender, "Name can only contain letters, numbers, spaces, hyphens, and apostrophes. Try again.") } diff --git a/internal/plugin/adventure_pets_dispatch_test.go b/internal/plugin/adventure_pets_dispatch_test.go new file mode 100644 index 0000000..6e3b206 --- /dev/null +++ b/internal/plugin/adventure_pets_dispatch_test.go @@ -0,0 +1,51 @@ +package plugin + +import ( + "testing" + "time" + + "maunium.net/go/mautrix/id" +) + +// TestResolvePetName_ThroughDispatcher is a regression test for the bug where +// resolvePendingInteraction deleted the pending entry before dispatch, while +// resolvePetName then tried to LoadAndDelete it again — always missing, so the +// adoption silently no-op'd and no pet was ever persisted. The fix passes the +// already-loaded interaction into resolvePetName. This test drives the real +// dispatcher path (resolvePendingInteraction) to lock that in. +func TestResolvePetName_ThroughDispatcher(t *testing.T) { + setupAuditTestDB(t) + uid := id.UserID("@pet-name-dispatch:example") + if err := createAdvCharacter(uid, "petnamer"); err != nil { + t.Fatal(err) + } + + p := &AdventurePlugin{} + interaction := &advPendingInteraction{ + Type: "pet_name", + Data: &advPendingPetName{PetType: "dog"}, + ExpiresAt: time.Now().Add(time.Hour), + } + p.pending.Store(string(uid), interaction) + + if err := p.resolvePendingInteraction(MessageContext{Sender: uid, Body: "Pepper"}, interaction); err != nil { + t.Fatal(err) + } + + pet, err := loadPetState(uid) + if err != nil { + t.Fatal(err) + } + if !pet.HasPet() { + t.Fatalf("expected pet to be adopted; got %+v", pet) + } + if pet.Name != "Pepper" { + t.Errorf("pet name = %q, want Pepper", pet.Name) + } + if pet.Type != "dog" { + t.Errorf("pet type = %q, want dog", pet.Type) + } + if !pet.Arrived || pet.Level != 1 { + t.Errorf("pet arrived=%v level=%d, want true/1", pet.Arrived, pet.Level) + } +}