Skip to content

feat(notifications): Render Rocket Chat as event-typed attachments#1238

Merged
srtab merged 3 commits into
mainfrom
refactor/rocketchat-attachment-renderers
May 20, 2026
Merged

feat(notifications): Render Rocket Chat as event-typed attachments#1238
srtab merged 3 commits into
mainfrom
refactor/rocketchat-attachment-renderers

Conversation

@srtab
Copy link
Copy Markdown
Owner

@srtab srtab commented May 20, 2026

Stacked on #1237 — please merge that one first; this PR's diff includes only the Piece 2+3 changes once #1237 is in.

Summary

Replaces the plain-text RC payload with class-based renderers keyed by EventType, mirroring the existing @register_channel pattern.

New package `daiv/notifications/channels/rocketchat_renderers/`

  • `base.py` — `RocketChatRenderer` ABC + shared helpers (`_fmt_tokens`, `_fmt_cost`, `_fmt_duration`, `_usage_field`, `_cost_field`, `_link`) + color constants.
  • `registry.py` — `@register_renderer` decorator + `get_renderer(event_type)` lookup.
  • `job_finished.py`, `schedule_finished.py`, `job_batch_finished.py` — one renderer per terminal event.

Channel change (`rocketchat.py`)

  • New `_build_payload(notification, delivery)` calls `get_renderer(event_type)` and renders to `(text, attachments)`.
  • Falls back to `_compose_text` when no renderer is registered, so any new `EventType` ships before its renderer.

What the user sees now

✅ Agent run on acme/api succeeded
┌─ (green sidebar)
│ Agent run on acme/api succeeded                                  ← clickable
│ ┌─────────────────────┬──────────────────────┐
│ │ Trigger             │ Duration             │
│ │ Manual              │ 1m 24s               │
│ ├─────────────────────┼──────────────────────┤
│ │ Usage               │ Cost                 │
│ │ 12.4k in · 38.1k out│ \$0.21                │
│ └─────────────────────┴──────────────────────┘
│ DAIV · just now

Three colors: green (success), red (failure), yellow (partial batch). Batch rollups also include a per-repo `✓ / ✗` breakdown (capped at 8 with overflow marker).

Notes

  • Missing data hides fields rather than rendering `—`: a notification with no token/cost data shows just Trigger + Duration.
  • Top-level `text` is the OS-notification line (`✅ Subject`); structured detail lives in attachments. Body content is intentionally not duplicated.
  • The existing `test_happy_path_posts_to_user_dm` was updated to assert the new payload shape; `test_unknown_event_type_falls_back_to_plain_text` covers the fallback.

Test plan

  • New `test_rocketchat_renderers.py` (35 tests): registry lookup (incl. EventType-vs-str equality), helper formatters (parametrized over None / 0 / k-rounding / edge cases), each renderer × success/failure, batch × {all-green, all-red, partial-yellow}, repo breakdown truncation.
  • Updated `test_rocketchat_channel.py`: happy path now asserts attachment shape; new test for unknown-event-type fallback.
  • Full notifications suite: 185 tests passing.
  • CI: `make test` + lint + ty.
  • Manual smoke: send a real notification to a Rocket Chat DM and confirm rendering matches the mockup.

srtab added 2 commits May 20, 2026 13:20
…hments

Replaces the plain-text concatenation in the Rocket Chat channel with a
class-based renderer registry keyed by EventType, modeled on the
existing @register_channel pattern.

- New package: notifications/channels/rocketchat_renderers/ with a
  RocketChatRenderer ABC (shared `_fmt_tokens` / `_fmt_cost` /
  `_fmt_duration` / `_usage_field` / `_cost_field` / `_link` helpers
  and color constants) plus three @register_renderer subclasses, one
  per terminal event: JobFinishedRenderer, ScheduleFinishedRenderer,
  JobBatchFinishedRenderer.
- rocketchat.py now dispatches via _build_payload(notification,
  delivery), which calls get_renderer(event_type) and falls back to the
  prior `_compose_text` plain-text format when no renderer is
  registered. New events deliver immediately on the fallback path.
- Channel messages now include a colored sidebar (green / red /
  yellow for partial batches), a leading status emoji, structured
  fields for repository / trigger / duration / usage / cost, and a
  per-repo ✓/✗ breakdown for batch rollups.
- Log a warning on the _build_payload plain-text fallback so a missing
  renderer for a known event type is observable instead of silently
  degrading messages forever.
- Enforce `event_type: ClassVar[EventType]` at subclass-definition time
  via __init_subclass__ on RocketChatRenderer, so a forgotten
  assignment surfaces at import rather than inside chat.postMessage.
- Mark _usage_field, _cost_field, and _color as @staticmethod to match
  the rest of the helpers in base.py — they don't use self.

Test additions covering branches the reviewers flagged as uncovered:
- ScheduleFinishedRenderer's Usage/Cost append paths (production
  signals emit these for every schedule run).
- End-to-end batch wire-format pin: JOB_BATCH_FINISHED dispatched
  through RocketChatChannel.send() must produce a payload containing
  the per-repo ✓/✗ breakdown.
- RocketChatChannel.send() rejects with UnrecoverableDeliveryError
  when the channel is disabled.
Base automatically changed from refactor/notification-token-context to main May 20, 2026 12:58
@srtab srtab merged commit 3bfd09e into main May 20, 2026
6 checks passed
@srtab srtab deleted the refactor/rocketchat-attachment-renderers branch May 20, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant