Pet adoption: stop double-deleting pending, pass interaction to resolvePetName

resolvePendingInteraction deletes the pending entry before dispatch, but
resolvePetName then re-ran LoadAndDelete to recover the carried PetType.
That second lookup always missed, so the if !ok branch returned nil: the
save never ran and no pet was ever persisted (adoption was 100% broken).

Pass the already-loaded interaction in, matching every other handler.
Add a regression test driving the real dispatcher path.
This commit is contained in:
prosolis
2026-05-22 08:42:47 -07:00
parent b167882e3e
commit 7dbfa0b56f
3 changed files with 60 additions and 11 deletions

View File

@@ -853,7 +853,7 @@ func (p *AdventurePlugin) resolvePendingInteraction(ctx MessageContext, interact
case "pet_type": case "pet_type":
return p.resolvePetType(ctx) return p.resolvePetType(ctx)
case "pet_name": case "pet_name":
return p.resolvePetName(ctx) return p.resolvePetName(ctx, interaction)
} }
return nil return nil
} }

View File

@@ -329,8 +329,11 @@ func (p *AdventurePlugin) resolvePetType(ctx MessageContext) error {
article, titleCase(petType))) article, titleCase(petType)))
} }
// resolvePetName handles naming the pet. // resolvePetName handles naming the pet. The dispatcher (handlePendingReply)
func (p *AdventurePlugin) resolvePetName(ctx MessageContext) error { // 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 := p.advUserLock(ctx.Sender)
userMu.Lock() userMu.Lock()
defer userMu.Unlock() defer userMu.Unlock()
@@ -340,20 +343,15 @@ func (p *AdventurePlugin) resolvePetName(ctx MessageContext) error {
return p.SendDM(ctx.Sender, "Failed to load your character.") return p.SendDM(ctx.Sender, "Failed to load your character.")
} }
val, ok := p.pending.LoadAndDelete(string(ctx.Sender)) data := interaction.Data.(*advPendingPetName)
if !ok {
return nil
}
pi := val.(*advPendingInteraction)
data := pi.Data.(*advPendingPetName)
name := strings.TrimSpace(ctx.Body) name := strings.TrimSpace(ctx.Body)
if len(name) == 0 || len(name) > 30 { 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.") return p.SendDM(ctx.Sender, "Name must be 1-30 characters. Try again.")
} }
if !petNameValid.MatchString(name) { 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.") return p.SendDM(ctx.Sender, "Name can only contain letters, numbers, spaces, hyphens, and apostrophes. Try again.")
} }

View File

@@ -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)
}
}