fix: emit rich mutation events for status changes (bd-313v)

- Add emitRichMutation() function for events with metadata
- handleClose now emits MutationStatus with old/new status
- handleUpdate detects status changes and emits MutationStatus
- Add comprehensive tests for rich mutation events

Also:
- Add activity.go test coverage (bd-3jcw):
  - Tests for parseDurationString, filterEvents, formatEvent
  - Tests for all mutation type displays
- Fix silent error handling in --follow mode (bd-csnr):
  - Track consecutive daemon failures
  - Show warning after 5 failures (rate-limited to 30s)
  - Show reconnection message on recovery

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 04:16:26 -08:00
parent 3f51ca5454
commit da809061db
5 changed files with 662 additions and 8 deletions

View File

@@ -167,6 +167,11 @@ func runActivityFollow(sinceTime time.Time) {
ticker := time.NewTicker(activityInterval)
defer ticker.Stop()
// Track consecutive failures for error reporting
consecutiveFailures := 0
const failureWarningThreshold = 5
lastWarningTime := time.Time{}
for {
select {
case <-rootCtx.Done():
@@ -174,10 +179,39 @@ func runActivityFollow(sinceTime time.Time) {
case <-ticker.C:
newEvents, err := fetchMutations(lastPoll)
if err != nil {
// Daemon might be down, continue trying
consecutiveFailures++
// Show warning after threshold failures, but not more than once per 30 seconds
if consecutiveFailures >= failureWarningThreshold {
if time.Since(lastWarningTime) >= 30*time.Second {
if jsonOutput {
// Emit error event in JSON mode
errorEvent := map[string]interface{}{
"type": "error",
"message": fmt.Sprintf("daemon unreachable (%d failures)", consecutiveFailures),
"timestamp": time.Now().Format(time.RFC3339),
}
data, _ := json.Marshal(errorEvent)
fmt.Fprintln(os.Stderr, string(data))
} else {
timestamp := time.Now().Format("15:04:05")
fmt.Fprintf(os.Stderr, "[%s] %s daemon unreachable (%d consecutive failures)\n",
timestamp, ui.RenderWarn("!"), consecutiveFailures)
}
lastWarningTime = time.Now()
}
}
continue
}
// Reset failure counter on success
if consecutiveFailures > 0 {
if consecutiveFailures >= failureWarningThreshold && !jsonOutput {
timestamp := time.Now().Format("15:04:05")
fmt.Fprintf(os.Stderr, "[%s] %s daemon reconnected\n", timestamp, ui.RenderPass("✓"))
}
consecutiveFailures = 0
}
newEvents = filterEvents(newEvents)
for _, e := range newEvents {
if jsonOutput {