Files
gogobee/internal/plugin/bootstrap_realm_firsts_test.go
T
prosolis 509df7fadf adventure: stop the log claiming a win it didn't see
Four things the review found, all of them the code telling a player
something that isn't true.

A run that walks into a dead end filed its ending as "cleared" whatever
the caller said, so both the liveblog and the summary reported a clear
for a party that merely ran out of map — and the end beat is
first-writer-wins, so nothing later could take it back. It says
"cleared" only for a boss now.

The re-offer branches of babysit and resume returned a zero cost, so a
player who was in fact charged read "0 coins" in the verdict. Both
re-quote the price they actually took.

And the realm-firsts reseed retired its one-shot job even when the read
under it had failed, which on a transient fault at Init would have left
the ledger mis-dated permanently. The read now says whether it worked,
and the job stays open when it didn't.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
2026-07-24 22:07:38 -07:00

161 lines
6.0 KiB
Go

package plugin
import (
"testing"
"time"
"gogobee/internal/db"
)
// ledgerFirstAt reads a zone's recorded claim time, or -1 if the ledger has no
// row for it at all.
func ledgerFirstAt(t *testing.T, zoneID string) int64 {
t.Helper()
var at int64
err := db.Get().QueryRow(
`SELECT first_at FROM news_realm_firsts WHERE kind = 'zone' AND target = ?`,
zoneID).Scan(&at)
if err != nil {
return -1
}
return at
}
func unixOf(t *testing.T, sqliteTime string) int64 {
t.Helper()
ts, ok := parseSQLiteTime(sqliteTime)
if !ok {
t.Fatalf("parseSQLiteTime(%q) failed", sqliteTime)
}
return ts.Unix()
}
// TestReseedClaimsAZoneWhoseClearsWereAllRetired is the regression for the bug
// that made this job necessary: every clear of forest_shadows carries
// abandoned = 1, which is how the game stores a kill the expedition walked on
// from, and the original one-shot's `abandoned = 0` filter therefore never saw
// the zone at all. An unclaimed zone is a spurious PRIORITY "realm first"
// waiting to fire the next time somebody clears it, months after the fact — so
// the assertion that matters is the claimRealmFirst one at the end.
func TestReseedClaimsAZoneWhoseClearsWereAllRetired(t *testing.T) {
seedRealmFixture(t)
db.Exec("seed retired-only zone", `INSERT INTO dnd_zone_run
(run_id, user_id, zone_id, total_rooms, boss_defeated, abandoned, completed_at)
VALUES ('r7', '@josie:x', 'forest_shadows', 6, 1, 1, '2026-02-14 09:00:00')`)
if got := ledgerFirstAt(t, "forest_shadows"); got != -1 {
t.Fatalf("forest_shadows already claimed before the reseed (first_at=%d) — fixture drift", got)
}
bootstrapRealmFirstsReseed()
if got := ledgerFirstAt(t, "forest_shadows"); got != unixOf(t, "2026-02-14 09:00:00") {
t.Errorf("forest_shadows first_at = %d, want %d (the real clear, not the minute the job ran)",
got, unixOf(t, "2026-02-14 09:00:00"))
}
// The point of the whole job. Before the reseed this returns true and the
// next clear of a zone beaten in February announces itself as a realm first.
if claimRealmFirst("zone", "forest_shadows") {
t.Error("forest_shadows was still unclaimed after the reseed — the next clear would fire a spurious realm-first")
}
}
// TestReseedCorrectsTheBackfillsDates pins the second half. claimRealmFirst
// stamps unixepoch(), so every row the original one-shot wrote carries the one
// minute that job ran — in prod, all six share the identical timestamp. The
// reseed has to overwrite an existing row, not INSERT OR IGNORE past it.
func TestReseedCorrectsTheBackfillsDates(t *testing.T) {
seedRealmFixture(t)
// The fixture claims both zones the way the backfill did: at claim time.
before := ledgerFirstAt(t, "goblin_warrens")
if before < time.Now().Unix()-300 {
t.Fatalf("fixture claim for goblin_warrens is not a now-stamp (%d) — fixture drift", before)
}
bootstrapRealmFirstsReseed()
// Josie's r1, January, not r2 or r3 and not today.
if got, want := ledgerFirstAt(t, "goblin_warrens"), unixOf(t, "2026-01-10 12:00:00"); got != want {
t.Errorf("goblin_warrens first_at = %d, want %d (earliest real clear)", got, want)
}
// crypt_valdris has a clean clear (r4) and a retired-but-won one (r5). The
// earliest is r4.
if got, want := ledgerFirstAt(t, "crypt_valdris"), unixOf(t, "2026-05-01 12:00:00"); got != want {
t.Errorf("crypt_valdris first_at = %d, want %d", got, want)
}
}
// TestReseedEmitsNothing is the reason this is a re-seed and not a re-run of the
// backfill. The ledger has to be repaired without any historical realm-first
// dispatch reaching the room; a zone beaten in February is not news in July.
func TestReseedEmitsNothing(t *testing.T) {
seedRealmFixture(t)
db.Exec("seed retired-only zone", `INSERT INTO dnd_zone_run
(run_id, user_id, zone_id, total_rooms, boss_defeated, abandoned, completed_at)
VALUES ('r7', '@josie:x', 'forest_shadows', 6, 1, 1, '2026-02-14 09:00:00')`)
bootstrapRealmFirstsReseed()
var queued int
if err := db.Get().QueryRow(`SELECT COUNT(*) FROM pete_emit_queue`).Scan(&queued); err != nil {
t.Fatalf("count pete_emit_queue: %v", err)
}
if queued != 0 {
t.Errorf("reseed queued %d dispatches, want 0 — the ledger repair must be silent", queued)
}
}
// TestReseedIsAOneShot. It is a bootstrap kept in place for fresh deploys (per
// feedback_loader_rewire_needs_bootstrap), so it runs on every start and must
// cost nothing after the first — and, more importantly, must not undo a
// later live claim by rewriting the ledger from stale history on every boot.
func TestReseedIsAOneShot(t *testing.T) {
seedRealmFixture(t)
bootstrapRealmFirstsReseed()
// A zone cleared after the reseed, claimed live.
if !claimRealmFirst("zone", "sunken_temple") {
t.Fatal("sunken_temple should have been an unclaimed realm-first")
}
live := ledgerFirstAt(t, "sunken_temple")
bootstrapRealmFirstsReseed()
if got := ledgerFirstAt(t, "sunken_temple"); got != live {
t.Errorf("second reseed moved a live claim: %d -> %d", live, got)
}
if claimRealmFirst("zone", "goblin_warrens") {
t.Error("second reseed dropped an existing claim")
}
}
// TestZoneFirstClearsCountsRetiredKills guards the shared query itself, which
// the kept backfill also uses. `abandoned` means the run row was retired, not
// that anybody gave up.
func TestZoneFirstClearsCountsRetiredKills(t *testing.T) {
seedRealmFixture(t)
db.Exec("seed retired-only zone", `INSERT INTO dnd_zone_run
(run_id, user_id, zone_id, total_rooms, boss_defeated, abandoned, completed_at)
VALUES ('r7', '@josie:x', 'forest_shadows', 6, 1, 1, '2026-02-14 09:00:00')`)
clears, ok := zoneFirstClears()
if !ok {
t.Fatal("zoneFirstClears reported a read failure")
}
byZone := map[string]zoneFirstClear{}
for _, f := range clears {
byZone[f.zoneID] = f
}
if len(byZone) != 3 {
t.Fatalf("zoneFirstClears returned %d zones, want 3 (a regression to `abandoned = 0` gives 2)", len(byZone))
}
if got := byZone["forest_shadows"].userID; got != "@josie:x" {
t.Errorf("forest_shadows first clearer = %q, want @josie:x", got)
}
if _, ok := byZone["arena"]; ok {
t.Error("an unfinished run counted as a clear")
}
}