Files
gogobee/internal/plugin/dnd_r1_migration_test.go
prosolis 8a1d9a16ce Adv 2.0 D&D Phase R R1: Legacy activity-loop deprecation
Retires the standalone !adventure dungeon/mine/forage/fish daily loop in
favor of !expedition. Gate intercepts legacy 1/2/3/4 + word-form input
and DMs a deprecation notice; town services (shop/blacksmith/rest/thom)
stay on !adventure.

- Delete dead resolveActivity (269 lines) + parseActivityLocation; add
  isLegacyActivityInput + renderLegacyActivityDeprecation.
- Morning-DM menu rewritten to point at !expedition and the !forage etc.
  harvest commands; writeAdvLocationLines removed (sole caller).
- New dnd_r1_migration.go runs archiveOrphanZoneRuns at Init: archives
  any active dnd_zone_run not linked to an active expedition (via
  exp.run_id or region_state.region_runs). Idempotent.
- Pre-existing flake in TestPickEveningRecap_BossOverridesAll fixed
  (E6c pool added entries without literal "boss"; widen substring set).
- Internal helpers (resolveAdvAction, advEligibleLocations, AdvLocation
  tier data, consumable drops) preserved — babysit/scheduler still use
  them passively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

172 lines
4.7 KiB
Go

package plugin
import (
"math/rand/v2"
"strings"
"testing"
"gogobee/internal/db"
"maunium.net/go/mautrix/id"
)
// TestArchiveOrphanZoneRuns_LeavesLinkedRunsAlone verifies that a run
// pointed at by an active expedition.run_id survives the migration.
func TestArchiveOrphanZoneRuns_LeavesLinkedRunsAlone(t *testing.T) {
setupZoneRunTestDB(t)
uid := id.UserID("@r1-link-test:example.org")
defer cleanupZoneRuns(uid)
defer cleanupExpeditions(uid)
rng := rand.New(rand.NewPCG(11, 7))
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
if err != nil {
t.Fatalf("startZoneRun: %v", err)
}
exp, err := startExpedition(uid, ZoneGoblinWarrens, run.RunID, ExpeditionSupplies{Max: 5, Current: 5, DailyBurn: 1})
if err != nil {
t.Fatalf("startExpedition: %v", err)
}
_ = exp
if _, err := archiveOrphanZoneRuns(); err != nil {
t.Fatalf("archive: %v", err)
}
got, err := getZoneRun(run.RunID)
if err != nil {
t.Fatalf("getZoneRun: %v", err)
}
if got == nil || !got.IsActive() {
t.Fatalf("linked run got archived; abandoned=%v", got != nil && !got.IsActive())
}
}
// TestArchiveOrphanZoneRuns_RegionRunsPreserved verifies that runs only
// referenced via RegionState["region_runs"] (not exp.run_id) are kept.
func TestArchiveOrphanZoneRuns_RegionRunsPreserved(t *testing.T) {
setupZoneRunTestDB(t)
uid := id.UserID("@r1-region-test:example.org")
defer cleanupZoneRuns(uid)
defer cleanupExpeditions(uid)
rng := rand.New(rand.NewPCG(13, 8))
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
if err != nil {
t.Fatalf("startZoneRun: %v", err)
}
exp, err := startExpedition(uid, ZoneGoblinWarrens, "", ExpeditionSupplies{Max: 5, Current: 5, DailyBurn: 1})
if err != nil {
t.Fatalf("startExpedition: %v", err)
}
// Pin via region_runs only; exp.run_id stays empty.
regionJSON := `{"region_runs":{"r1":"` + run.RunID + `"}}`
if _, err := db.Get().Exec(`UPDATE dnd_expedition SET region_state = ? WHERE expedition_id = ?`, regionJSON, exp.ID); err != nil {
t.Fatalf("set region_state: %v", err)
}
if _, err := archiveOrphanZoneRuns(); err != nil {
t.Fatalf("archive: %v", err)
}
got, err := getZoneRun(run.RunID)
if err != nil {
t.Fatalf("getZoneRun: %v", err)
}
if got == nil || !got.IsActive() {
t.Fatalf("region-linked run got archived")
}
}
// TestArchiveOrphanZoneRuns_OrphanArchived verifies that an active zone
// run with no expedition pointing at it gets flagged abandoned.
func TestArchiveOrphanZoneRuns_OrphanArchived(t *testing.T) {
setupZoneRunTestDB(t)
uid := id.UserID("@r1-orphan-test:example.org")
defer cleanupZoneRuns(uid)
rng := rand.New(rand.NewPCG(17, 9))
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
if err != nil {
t.Fatalf("startZoneRun: %v", err)
}
n, err := archiveOrphanZoneRuns()
if err != nil {
t.Fatalf("archive: %v", err)
}
if n < 1 {
t.Fatalf("expected ≥1 archived row, got %d", n)
}
got, err := getZoneRun(run.RunID)
if err != nil {
t.Fatalf("getZoneRun: %v", err)
}
if got == nil {
t.Fatal("run vanished")
}
if got.IsActive() {
t.Fatal("orphan run still active after archive")
}
}
// TestArchiveOrphanZoneRuns_Idempotent: a second call is a no-op.
func TestArchiveOrphanZoneRuns_Idempotent(t *testing.T) {
setupZoneRunTestDB(t)
uid := id.UserID("@r1-idempotent-test:example.org")
defer cleanupZoneRuns(uid)
rng := rand.New(rand.NewPCG(19, 11))
if _, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng); err != nil {
t.Fatalf("startZoneRun: %v", err)
}
if _, err := archiveOrphanZoneRuns(); err != nil {
t.Fatalf("archive #1: %v", err)
}
n, err := archiveOrphanZoneRuns()
if err != nil {
t.Fatalf("archive #2: %v", err)
}
if n != 0 {
t.Fatalf("expected 0 on second call, got %d", n)
}
}
// TestIsLegacyActivityInput covers the deprecation-gate detection: legacy
// keywords / numbers match, unrelated DM input doesn't.
func TestIsLegacyActivityInput(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"1", true},
{"2 deep mine", true},
{"dungeon", true},
{"forage", true},
{"fish", true},
{"f", true},
{"Soggy Cellar", true}, // location name match
{"", false},
{"hello there", false},
{"7", false},
{"!expedition", false},
}
for _, c := range cases {
got := isLegacyActivityInput(strings.ToLower(c.in), nil)
if got != c.want {
t.Errorf("isLegacyActivityInput(%q) = %v, want %v", c.in, got, c.want)
}
}
}
// TestRenderLegacyActivityDeprecation: deprecation DM points at !expedition
// and the harvest entry-points so the user has somewhere to go.
func TestRenderLegacyActivityDeprecation(t *testing.T) {
out := renderLegacyActivityDeprecation(nil)
for _, want := range []string{"expedition", "!forage", "!mine", "retired"} {
if !strings.Contains(out, want) {
t.Errorf("deprecation DM missing %q; got:\n%s", want, out)
}
}
}