Files
gogobee/internal/peteclient/client_test.go
prosolis b42beec348 Pete Adventure News: emit seam, !news command, cold-start backfill
gogobee's game-side of Pete's push-based Adventure news feed. Emits
structured event *facts* (not prose) to Pete's ingest endpoint over a
durable queue; Pete owns voice/authoring/publishing.

- internal/peteclient: durable pete_emit_queue + retry sender; Fact
  payload; FEATURE_PETE_NEWS master switch.
- pete.go: emitFact enforces runtime kill-switch + per-player opt-out
  (anonymize, never drop); zone-clear + realm-first tiering; !news
  command (player optout/optin, admin on/off).
- bootstrap_pete_news.go: cold-start backfill replays deaths +
  single-holder achievements + zone realm-firsts, backdated + NoPush,
  idempotent, fires only once the seam is live; seeds news_realm_firsts.
- Emit sites: death, zone_first, rival_result, milestone, arrival.
- db.go: pete_emit_queue, news_optout (+opted_out_at), news_realm_firsts.

Unshipped. Deploy Pete first, then set FEATURE_PETE_NEWS + ingest env.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
2026-07-11 00:53:25 -07:00

92 lines
2.6 KiB
Go

package peteclient
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"
"gogobee/internal/db"
)
// TestEmitDrainRoundTrip proves the durable path: Emit queues a fact, the sender
// POSTs it to Pete with bearer auth, and the row is marked sent (so it won't
// re-send), while a duplicate GUID collapses to one delivery.
func TestEmitDrainRoundTrip(t *testing.T) {
if err := db.Init(t.TempDir()); err != nil {
t.Fatal(err)
}
var mu sync.Mutex
var got []Fact
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/ingest/adventure" {
t.Errorf("unexpected path %q", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
var f Fact
_ = json.Unmarshal(body, &f)
mu.Lock()
got = append(got, f)
gotAuth = r.Header.Get("Authorization")
mu.Unlock()
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
std = &Client{cfg: Config{IngestURL: srv.URL, Token: "tok", Enabled: true}, http: srv.Client()}
Emit(Fact{GUID: "death:a:1", EventType: "death", Tier: "priority", Subject: "Brannigan", OccurredAt: 1})
Emit(Fact{GUID: "death:a:1", EventType: "death", Tier: "priority", Subject: "Brannigan", OccurredAt: 1}) // dup guid
std.drain(context.Background())
mu.Lock()
defer mu.Unlock()
if len(got) != 1 {
t.Fatalf("delivered %d facts, want 1 (dup should collapse)", len(got))
}
if got[0].Subject != "Brannigan" {
t.Errorf("subject = %q", got[0].Subject)
}
if gotAuth != "Bearer tok" {
t.Errorf("auth header = %q", gotAuth)
}
var pending int
if err := db.Get().QueryRow(`SELECT count(*) FROM pete_emit_queue WHERE sent_at IS NULL`).Scan(&pending); err != nil {
t.Fatal(err)
}
if pending != 0 {
t.Errorf("%d rows still pending after successful drain", pending)
}
// A second drain re-sends nothing.
std.drain(context.Background())
if len(got) != 1 {
t.Errorf("re-drained a sent row: %d deliveries", len(got))
}
}
// TestEmitDisabledNoQueue: when disabled, Emit is a durable no-op.
func TestEmitDisabledNoQueue(t *testing.T) {
if err := db.Init(t.TempDir()); err != nil {
t.Fatal(err)
}
std = &Client{cfg: Config{Enabled: false}, http: http.DefaultClient}
Emit(Fact{GUID: "disabled-guid", EventType: "death", OccurredAt: 1})
var n int
// db.Init is a process singleton, so this may share state with other tests;
// scope the check to this fact's guid.
if err := db.Get().QueryRow(`SELECT count(*) FROM pete_emit_queue WHERE guid = 'disabled-guid'`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("disabled Emit queued %d rows, want 0", n)
}
}