gogobee has started filing a `retreat` bulletin — an expedition that ended with the player walking out alive. Pete had no case for it, and handleAdventureIngest answers an unrecognized event_type with 400. That failure is silent and total. gogobee's sender treats any non-2xx as a failure, retries eight times over roughly two hours, and then parks the row for good. A gogobee emitting `retreat` at a Pete who doesn't know the word would not log an error anyone reads — it would just quietly drop every retreat the realm ever files. So Pete has to learn it BEFORE gogobee starts saying it. Deploy order matters here, and it is Pete first. The copy leads with everyone coming home, because that is the true and the kind part: a retreat is a bad day, not a funeral. Bulletin tier — it rides the daily digest and does not interrupt the room. Announcing every failed run live would be a firehose, and an unkind one. https://claude.ai/code/session_01B2MwktU4RgfWkar8HM3zZn
56 lines
2.0 KiB
Go
56 lines
2.0 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"pete/internal/storage"
|
|
)
|
|
|
|
// The retreat bulletin — gogobee's newest event type.
|
|
//
|
|
// The failure this guards against is silent and total: handleAdventureIngest
|
|
// answers an unrecognized event_type with 400, and gogobee's sender treats any
|
|
// non-2xx as a failure, retries eight times over ~two hours, and then PARKS the
|
|
// row forever. So a gogobee that emits `retreat` against a Pete that doesn't
|
|
// know the word doesn't log an error anyone reads — it just quietly drops every
|
|
// retreat the realm ever files. Pete has to learn the word first, and this test
|
|
// is what says he has.
|
|
func TestAdventureIngest_AcceptsRetreat(t *testing.T) {
|
|
const token = "s3cret-token"
|
|
s, posted := newAdvServer(t, token)
|
|
|
|
f := AdvFact{
|
|
GUID: "retreat:abc:1000", EventType: "retreat", Tier: "bulletin",
|
|
Actors: []string{"Brannigan"}, Subject: "Brannigan",
|
|
Zone: "the Underforge", Level: 12, Count: 3, Outcome: "retreated",
|
|
OccurredAt: 1000,
|
|
}
|
|
if rw := postFact(t, s, token, f); rw.Code != 200 {
|
|
t.Fatalf("ingest status = %d body=%s — Pete rejected a retreat, so gogobee will "+
|
|
"retry it eight times and park it forever", rw.Code, rw.Body.String())
|
|
}
|
|
|
|
got, err := storage.GetStoryByGUID("retreat:abc:1000")
|
|
if err != nil || got == nil {
|
|
t.Fatalf("retreat was accepted but not stored: %v", err)
|
|
}
|
|
// It has to actually say what happened, in Pete's voice, using only the
|
|
// supplied facts.
|
|
body := got.Headline + " " + got.Lede
|
|
for _, want := range []string{"Brannigan", "the Underforge"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("rendered retreat is missing %q: %q", want, body)
|
|
}
|
|
}
|
|
if !strings.Contains(got.Lede, "3 days in") {
|
|
t.Errorf("the day count never made it into the copy: %q", got.Lede)
|
|
}
|
|
// Bulletin, not priority: a retreat goes in the daily digest, it does not
|
|
// interrupt the room. Pete announcing every failed run live would be a
|
|
// firehose — and an unkind one.
|
|
if len(*posted) != 0 {
|
|
t.Errorf("a bulletin was posted live: %+v", *posted)
|
|
}
|
|
}
|