Files
gogobee/internal/plugin/dnd_zone_harvest_test.go
prosolis 893d3dacad Branching zones G6: dependent surfaces re-keyed on graph nodes
- Harvest tables (expedition + standalone) now keyed by node_id; legacy
  room-idx entries auto-migrate via read-fallback + drop-on-save.
- Narration salts swapped from run.CurrentRoom to narrationCadence(run)
  (= len(visited_nodes)-1) so flavor pickers survive G9 column drop.
- !zone map renders the graph (BFS by PosX/PosY) when the gate is on:
  ✓/▶/· status, ╳ for locked-only edges, secrets hidden until visited.
- Region-boundary hook in graph advance/!zone go updates expedition
  CurrentRegion + visited list when from/to nodes differ — without
  burning supplies or retiring runs (the graph IS the run state).

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

94 lines
3.1 KiB
Go

package plugin
import (
"strings"
"testing"
"gogobee/internal/db"
"maunium.net/go/mautrix/id"
)
// Standalone-zone-run harvest path: with no active expedition but an
// active !zone enter run, !forage / !mine / !fish should land yields
// against per-run node storage. Mirrors the bug Rurina hit where harvest
// commands silently bounced because the handler hard-required an
// expedition.
func TestStandaloneHarvest_RoutesToZoneRun(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@stand-harvest-route:example")
zoneCmdTestCharacter(t, uid, 4)
t.Cleanup(func() { cleanupZoneRuns(uid); cleanupExpeditions(uid) })
run, err := startZoneRun(uid, ZoneForestShadows, 4, nil)
if err != nil {
t.Fatal(err)
}
// Mark room 0 (entry) as cleared so harvest is allowed there.
if !currentRoomCleared(run) {
t.Fatalf("entry room should auto-clear")
}
p := &AdventurePlugin{}
// No expedition exists for this user — handler must fall through to
// the standalone path and not bounce with the expedition-required
// error message.
if err := p.handleHarvestCmd(MessageContext{Sender: uid}, HarvestForage); err != nil {
t.Fatalf("handleHarvestCmd: %v", err)
}
// Side-effect check: the run row should now have non-empty
// harvest_nodes_json since saveStandaloneHarvestNodes ran.
var raw string
row := db.Get().QueryRow(
`SELECT harvest_nodes_json FROM dnd_zone_run WHERE run_id = ?`, run.RunID)
if err := row.Scan(&raw); err != nil {
t.Fatalf("scan harvest_nodes_json: %v", err)
}
if raw == "" || raw == "{}" {
t.Errorf("expected harvest_nodes_json populated after standalone harvest, got %q", raw)
}
wantKey := "\"" + deriveLegacyNodeID(run.ZoneID, 0) + "\":"
if !strings.Contains(raw, wantKey) {
t.Errorf("expected entry node %s in harvest table, got %q", wantKey, raw)
}
}
func TestStandaloneHarvest_BouncesWhenNoActiveAnything(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@stand-harvest-noctx:example")
zoneCmdTestCharacter(t, uid, 1)
p := &AdventurePlugin{}
// No expedition, no zone run — should bounce cleanly with the new
// combined hint, not panic on a nil run.
if err := p.handleHarvestCmd(MessageContext{Sender: uid}, HarvestForage); err != nil {
t.Fatalf("handleHarvestCmd: %v", err)
}
}
func TestStandaloneHarvest_FishGatedToFishingZones(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@stand-harvest-fish:example")
zoneCmdTestCharacter(t, uid, 1)
t.Cleanup(func() { cleanupZoneRuns(uid) })
// Goblin Warrens isn't a fishing zone — !fish must reject.
if _, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil); err != nil {
t.Fatal(err)
}
p := &AdventurePlugin{}
if err := p.handleHarvestCmd(MessageContext{Sender: uid}, HarvestFish); err != nil {
t.Fatalf("handleHarvestCmd: %v", err)
}
// (no easy way to assert the rejection without capturing SendDM —
// the no-error return + no node persistence is the contract.)
var raw string
_ = db.Get().QueryRow(
`SELECT COALESCE(harvest_nodes_json,'') FROM dnd_zone_run
WHERE user_id = ? ORDER BY started_at DESC LIMIT 1`, string(uid)).Scan(&raw)
if raw != "" && raw != "{}" {
t.Errorf("fish in non-fishing zone should not seed nodes; got %q", raw)
}
}