mirror of
https://github.com/prosolis/gogobee.git
synced 2026-09-14 19:01:09 +00:00
The morning digest looked up log entries by e.CurrentDay - 1, which assumed the day rollover happens in deliverBriefing. That only holds for legacy UTC-anchored runs; event-anchored expeditions roll current_day at night camp, so by 06:00 CurrentDay is already the day that is starting and the digest reported a near-empty day. Window on the last briefing timestamp instead, which also covers nights the autopilot never camped. Also count real rooms out of the auto-walk summary instead of counting ticks, and only emit the site footer when the Pete seam is on, since the roster push is what creates the page the link points at.
254 lines
8.6 KiB
Go
254 lines
8.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// Coverage for the once-a-day cadence (2026-07-26). The behavioural tests
|
|
// below are the point of the file: before this change nothing asserted that
|
|
// the recap and ambient paths sent a DM, so nothing would have caught them
|
|
// silently continuing to send — or, worse, silently dropping the mechanical
|
|
// effect along with the message.
|
|
|
|
func TestRenderOvernightDigest_CollapsesWalksAndSkipsFrameTypes(t *testing.T) {
|
|
entries := []ExpeditionEntry{
|
|
// One `walk` entry is one autopilot tick, and a tick can cover
|
|
// several rooms — the digest reports rooms, not ticks.
|
|
{Type: "walk", Summary: "auto-walk: 2 room(s)"},
|
|
{Type: "walk", Summary: "auto-walk: 3 room(s)"},
|
|
{Type: "briefing", Summary: "morning briefing — 1.0 SU consumed overnight"},
|
|
{Type: "narrative", Summary: "the corridor bends left"},
|
|
{Type: "ambient", Summary: "ambient: pack_rat — Supplies -0.5"},
|
|
{Type: "night", Summary: "Signs of passage near camp; no encounter."},
|
|
{Type: "recap", Summary: "evening recap — 6 log entries today"},
|
|
}
|
|
got := renderOvernightDigest(entries)
|
|
|
|
if !strings.Contains(got, "walked 5 rooms") {
|
|
t.Errorf("walks not collapsed to a count:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "ambient: pack_rat") {
|
|
t.Errorf("ambient entry missing from digest:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "Signs of passage") {
|
|
t.Errorf("night check missing from digest:\n%s", got)
|
|
}
|
|
for _, unwanted := range []string{"morning briefing", "evening recap", "corridor bends"} {
|
|
if strings.Contains(got, unwanted) {
|
|
t.Errorf("digest echoed frame/narration entry %q:\n%s", unwanted, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderOvernightDigest_CapsAndReportsOverflow(t *testing.T) {
|
|
var entries []ExpeditionEntry
|
|
for i := 0; i < digestMaxLines+3; i++ {
|
|
entries = append(entries, ExpeditionEntry{
|
|
Type: "ambient",
|
|
Summary: fmt.Sprintf("ambient event %d", i),
|
|
})
|
|
}
|
|
got := renderOvernightDigest(entries)
|
|
|
|
if n := strings.Count(got, "ambient event"); n != digestMaxLines {
|
|
t.Errorf("digest carried %d lines, want the cap of %d:\n%s", n, digestMaxLines, got)
|
|
}
|
|
// A truncated digest must say so — otherwise it reads as the whole day.
|
|
if !strings.Contains(got, "and 3 more") {
|
|
t.Errorf("overflow not reported:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRenderOvernightDigest_EmptyWhenNothingNotable(t *testing.T) {
|
|
entries := []ExpeditionEntry{
|
|
{Type: "briefing", Summary: "morning briefing"},
|
|
{Type: "narrative", Summary: "dust everywhere"},
|
|
}
|
|
if got := renderOvernightDigest(entries); got != "" {
|
|
t.Errorf("want no digest block for an unremarkable day, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestAdventureWhoURL_UsesRosterTokenNotHandle(t *testing.T) {
|
|
// The roster token is salted from a DB-persisted secret.
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@digest-url:example")
|
|
got := adventureWhoURL(uid)
|
|
|
|
want := "/adventure/who/" + eventToken(uid, "roster")
|
|
if !strings.HasSuffix(got, want) {
|
|
t.Errorf("who URL = %q, want suffix %q", got, want)
|
|
}
|
|
if strings.Contains(got, "digest-url") {
|
|
t.Errorf("who URL leaked the Matrix handle: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAdventureWhoURL_FallsBackToFeedWithoutUser(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
if got := adventureWhoURL(""); got != adventureFeedURL() {
|
|
t.Errorf("empty user should fall back to the feed, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestBuildAutoRunDM_NightCampIsSilent — the autopilot's end-of-day digest was
|
|
// the last recurring second DM of the day. It goes quiet; the camp writes its
|
|
// own `rest` log entry, so the morning briefing still reports it.
|
|
func TestBuildAutoRunDM_NightCampIsSilent(t *testing.T) {
|
|
r := autopilotWalkResult{rooms: 4, reason: stopOK, stream: []string{"…walked…"}}
|
|
camp := "\n\n⛺ **Autopilot camp** — night"
|
|
body, ok := buildAutoRunDM("expid", r, camp, autoCampDecision{
|
|
Kind: CampTypeStandard, Night: true,
|
|
})
|
|
if ok || body != "" {
|
|
t.Errorf("night camp should be silent, got ok=%v body=%q", ok, body)
|
|
}
|
|
}
|
|
|
|
// The two interactive surfaces the night-camp cut must not touch.
|
|
func TestBuildAutoRunDM_KeepSetStillSurfaces(t *testing.T) {
|
|
fork := autopilotWalkResult{rooms: 1, reason: stopFork, finalMsg: "pick a path"}
|
|
if body, ok := buildAutoRunDM("expid", fork, "", autoCampDecision{}); !ok ||
|
|
!strings.Contains(body, "pick a path") {
|
|
t.Errorf("fork must still surface, got ok=%v body=%q", ok, body)
|
|
}
|
|
|
|
hold := autopilotWalkResult{rooms: 2, reason: stopBossSafety}
|
|
holdCamp := "\n\n⛺ **Rest camp**"
|
|
if body, ok := buildAutoRunDM("expid", hold, holdCamp, autoCampDecision{
|
|
Reason: "boss-safety hold — resting before re-engaging",
|
|
}); !ok || !strings.Contains(body, "Holding before the boss") {
|
|
t.Errorf("boss-safety hold must still surface, got ok=%v body=%q", ok, body)
|
|
}
|
|
}
|
|
|
|
// TestDeliverAmbient_SilentButStillLogs — the ambient event still fires and
|
|
// still records itself; it just stops DMing.
|
|
func TestDeliverAmbient_SilentButStillLogs(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@digest-ambient:example")
|
|
defer cleanupExpeditions(uid)
|
|
|
|
p := &AdventurePlugin{}
|
|
sink := installSink(p)
|
|
|
|
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.deliverAmbient(exp, exp.StartDate.Add(time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if dms := sink.dmsTo(uid); len(dms) != 0 {
|
|
t.Errorf("ambient sent %d DM(s), want 0:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
|
}
|
|
entries, _ := recentExpeditionLog(exp.ID, 10)
|
|
found := false
|
|
for _, e := range entries {
|
|
if e.Type == "ambient" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("ambient event fired without writing a log entry — the digest would lose it")
|
|
}
|
|
}
|
|
|
|
// TestDeliverRecap_SilentButStillRunsNightCheck — the recap's mechanical half
|
|
// (wandering check, threat bump) must survive the message going away.
|
|
func TestDeliverRecap_SilentButStillRunsNightCheck(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@digest-recap:example")
|
|
defer cleanupExpeditions(uid)
|
|
|
|
p := &AdventurePlugin{}
|
|
sink := installSink(p)
|
|
|
|
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp.Camp = &CampState{Active: true, Type: CampTypeStandard, EstablishedAt: exp.StartDate}
|
|
if err := updateCamp(exp.ID, exp.Camp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
recapAt := exp.StartDate.Add(12 * time.Hour)
|
|
if err := p.deliverRecap(exp, recapAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if dms := sink.dmsTo(uid); len(dms) != 0 {
|
|
t.Errorf("recap sent %d DM(s), want 0:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
|
}
|
|
entries, _ := recentExpeditionLog(exp.ID, 10)
|
|
sawNight, sawRecap := false, false
|
|
for _, e := range entries {
|
|
switch e.Type {
|
|
case "night":
|
|
sawNight = true
|
|
case "recap":
|
|
sawRecap = true
|
|
}
|
|
}
|
|
if !sawNight {
|
|
t.Error("night wandering check did not run — the recap dropped its mechanics, not just its DM")
|
|
}
|
|
if !sawRecap {
|
|
t.Error("recap log entry missing — the site loses its day boundary")
|
|
}
|
|
}
|
|
|
|
// TestDeliverBriefing_IsTheOneDailyMessage — the payoff: one DM, carrying the
|
|
// prior day's silent activity and the reader's own site link.
|
|
func TestDeliverBriefing_CarriesDigestAndSiteLink(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@digest-briefing:example")
|
|
defer cleanupExpeditions(uid)
|
|
|
|
enablePeteSeam(t)
|
|
p := &AdventurePlugin{}
|
|
sink := installSink(p)
|
|
|
|
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Stand in for the day that just went by silently. The day number is
|
|
// deliberately not CurrentDay: on an event-anchored run the night camp
|
|
// rolls current_day when it pitches, so entries either side of the
|
|
// rollover carry different day numbers. The digest windows on time.
|
|
if err := appendExpeditionLog(exp.ID, exp.CurrentDay+1, "ambient",
|
|
"ambient: pack_rat — Supplies -0.5", "Something nibbled the stores."); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := p.deliverBriefing(exp, exp.StartDate.Add(20*time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dms := sink.dmsTo(uid)
|
|
if len(dms) != 1 {
|
|
t.Fatalf("briefing sent %d DM(s), want exactly 1:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
|
}
|
|
body := dms[0]
|
|
if !strings.Contains(body, "Since yesterday") {
|
|
t.Errorf("briefing missing the overnight digest block:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "ambient: pack_rat") {
|
|
t.Errorf("digest did not carry the silent ambient event:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, adventureWhoURL(uid)) {
|
|
t.Errorf("briefing missing the reader's own site link:\n%s", body)
|
|
}
|
|
}
|