Files
gogobee/internal/plugin/dnd_r1_migration_test.go
prosolis e05da913c4 H3: retire manual harvest commands
Removes !forage / !mine / !scavenge / !fish / !essence / !commune from
the command surface. Typing one now returns a one-line deprecation DM
pointing at the auto-harvest flow ("Harvest is automatic now — just
walk."). The internal handleHarvestCmd / handleStandaloneHarvest paths
are left in place; operator deletes after soak per plan §H3.

Help text and the supplies-low autopilot pause line no longer mention
the manual verbs.
2026-05-17 16:20:10 -07:00

172 lines
4.8 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", "Harvest is automatic", "retired"} {
if !strings.Contains(out, want) {
t.Errorf("deprecation DM missing %q; got:\n%s", want, out)
}
}
}