Headless real-character sim + new-feature exercise harness

Run the actual Adventure module against a copy of the prod DB with no
Matrix client, to smoke-test before deploy.

- expedition-sim: -real-user @mxid runs an EXISTING character loaded from
  -data's gogobee.db instead of a synthetic build. SimRunner gains
  PrepareRealCharacter (heals to full + tops up bankroll; keeps real
  race/class/subclass/level/gear/spells).
- plugin.SendReply now honors the MessageSink like SendMessage/SendDM.
  Reply-based handlers (duels, !town, !rivals, !achievements) previously
  bypassed the capture seam and hit a nil client under the sink. Prod
  behavior is unchanged (sink is nil in production).
- exercise_prod_test.go (build tag: prodexercise) drives every N-series
  feature — world boss, duels, Shadow, Renown, achievements, journal,
  town registries, vault, gifting — against a prod DB copy with all
  outbound messages captured. Gated on GOGOBEE_PROD_DB_DIR; never runs in
  normal CI.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-10 23:32:52 -07:00
parent a6f1de4e74
commit 3f4b4ece5c
4 changed files with 372 additions and 0 deletions

View File

@@ -638,6 +638,14 @@ func (b *Base) SendNotice(roomID id.RoomID, text string) error {
// SendReply sends a reply to a specific event.
func (b *Base) SendReply(roomID id.RoomID, eventID id.EventID, text string) error {
// The sink is the headless capture seam: a reply is an outbound room
// message, so — like SendMessage/SendDM — it diverts here when installed and
// the live client is never touched. Without this branch, reply-only handlers
// (duels, !town, !rivals, !achievements) hit a nil client under the sink.
if b != nil && b.Sink != nil {
_, err := b.Sink.Capture(outboundMessage{ToRoom: roomID, Text: text})
return err
}
content := textContent(text)
if eventID != "" {
content.RelatesTo = &event.RelatesTo{