diff --git a/internal/web/adventure.go b/internal/web/adventure.go index eb0aa3b..e50ea93 100644 --- a/internal/web/adventure.go +++ b/internal/web/adventure.go @@ -188,6 +188,8 @@ func advEventMeta(eventType string) (label, emoji string) { return "Pete's duels", "🤝" case "milestone": return "Milestone", "🏅" + case "retreat": + return "Pulled out", "🎒" } return "Dispatch", "📣" } @@ -375,6 +377,19 @@ func renderAdventure(f AdvFact) (headline, lede string, ok bool) { case "death": return fmt.Sprintf("We lost %s in %s.", f.Subject, f.Zone), fmt.Sprintf("Sad news to pass along: %s fell at level %d in %s. The graveyard's a little fuller tonight. Rest easy.", f.Subject, f.Level, f.Zone), true + case "retreat": + // An expedition that came apart without killing anyone. Until gogobee + // started sending these, the feed had no way to say "it went badly and + // everyone lived" — so it never said it, and the classes that retreat + // often simply never appeared. Warm, not a failure notice: everyone came + // home, and that is the part Pete leads with. + howFar := "barely a day in" + if f.Count > 1 { + howFar = fmt.Sprintf("%d days in", f.Count) + } + return fmt.Sprintf("%s backed out of %s.", f.Subject, f.Zone), + fmt.Sprintf("%s turned around %s — %s got the better of them this time%s, and they made the call to walk out rather than push it. Everybody came home breathing, which is the bit that counts. That dungeon'll still be there next week.", + f.Subject, howFar, f.Zone, atLevel), true case "arrival": return fmt.Sprintf("Welcome to the realm, %s!", f.Subject), fmt.Sprintf("A new %s just walked through the gates. Say hello if you see them out there.", f.ClassRace), true diff --git a/internal/web/adventure_retreat_test.go b/internal/web/adventure_retreat_test.go new file mode 100644 index 0000000..4dab3df --- /dev/null +++ b/internal/web/adventure_retreat_test.go @@ -0,0 +1,55 @@ +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) + } +}