Skip to content

Background-monitoring email alerts: real SMTP send + pluggable external mailer#171

Merged
HanSur94 merged 9 commits into
mainfrom
claude/unruffled-boyd-1288b4
May 29, 2026
Merged

Background-monitoring email alerts: real SMTP send + pluggable external mailer#171
HanSur94 merged 9 commits into
mainfrom
claude/unruffled-boyd-1288b4

Conversation

@HanSur94
Copy link
Copy Markdown
Owner

Summary

Makes the background monitoring pipeline (LiveEventPipeline) actually send per-event alert emails — the rule/template/snapshot machinery existed but was locked to dry-run and never sent. Implemented as a small, mockable transport seam (Approach B), plus an adapter to reuse an existing external mailer.

Real SMTP path (quick task 260529-rxf)

  • New EmailTransport — owns SMTP mechanics: auth + STARTTLS (587, default) / SSL (465) / none, a pure buildMailProps mapping (CI-testable without sending), env-var password support, and an Octave guard (logs-and-skips when sendmail is absent, never errors).
  • NotificationService now delegates sending to an injectable Transport and adds a per-(sensor, threshold) cooldown (default 5 min, dry-run honors it too) with a SuppressedCount.
  • LiveEventPipeline forwards real per-event sensorData (X/Y + threshold) so IncludeSnapshot rules attach PNGs in live mode.

Pluggable external mailer (fast task 260529-fnt)

  • New FunctionTransport — wraps any function handle as a Transport, so an existing site/company MATLAB mailer (e.g. companyMail(to,subject,body,attachments)) can be used for alerts with no SMTP config (no Gmail/App-Password). Normalizes recipients to a flat cellstr; Octave-safe.

Backward compatible: LiveEventPipeline still defaults to NotificationService('DryRun', true), so existing scripts behave identically. All changes are additive.

Test Plan

Verified in MATLAB R2025a (live session):

  • tests/test_email_transport.m — 5/5 (prop-map none/starttls/ssl, invalid-mode error, Octave-guard no-throw)
  • tests/test_function_transport.m — 5/5 (forwarding, recipient-flattening, attachments default, invalid-handle, NotificationService integration)
  • tests/test_notification_service.m — 10/10 (7 original + 3 new: delegation, cooldown-suppress, cooldown-expiry)
  • tests/test_live_event_pipeline_tag.m — 3/3 (no regression from 3-output processMonitorTag_)
  • Class suites: TestEmailTransport 5/5, TestNotificationService 7/7, TestLiveEventPipelineTag 3/3
  • MISS_HIT (mh_style + mh_lint) clean; MATLAB Code Analyzer clean on all touched libs
  • Real SMTP delivery — manual, out of CI, via examples/05-events/smoke_email_send.m (or wrap your own mailer in FunctionTransport)

🤖 Generated with Claude Code

HanSur94 and others added 8 commits May 29, 2026 20:15
…tave guard + tests

- New libs/EventDetection/EmailTransport.m: handle class with Server/Port/User/
  Password/PasswordEnv/SecurityMode/From NV-pair config
- PURE static buildMailProps(mode, port) returns containers.Map of mail.smtp.*
  keys for none/starttls/ssl modes without touching prefs or JVM
- send() has Octave guard (exist('sendmail','file')==0 -> log-and-return, no error);
  applies JVM props then delegates to MATLAB sendmail
- EmailTransport:invalidSecurityMode error on unknown mode
- tests/test_email_transport.m: function-based tests for all three prop maps,
  invalid mode, and octave-guard no-throw
- tests/suite/TestEmailTransport.m: class-based mirror using verifyEqual/verifyError

MATLAB test execution deferred to orchestrator (no MCP access in executor).
…otificationService

- NotificationService now accepts SmtpPort(587)/SmtpUser/SmtpPassword/PasswordEnv/
  SecurityMode('starttls')/CooldownMinutes(5)/Transport via constructor inputParser
- sendEmail_ delegates to Transport.send(...); lazily constructs real EmailTransport
  when Transport is empty (DI seam for mock injection)
- Added per-(SensorName|ThresholdLabel) cooldown in notify(): suppresses both
  real-send and dry-run within window; stamps AFTER Enabled+rule guards so
  test_disabled/test_default_rule stay green; SuppressedCount incremented on suppress
- Added hidden setLastSentForTesting_ seam for deterministic expiry testing
- tests/suite/MockEmailTransport.m: new test double recording send() call args
- tests/test_notification_service.m: extended with test_transport_delegation,
  test_cooldown_suppresses_within_window, test_cooldown_allows_after_expiry;
  add_event_path now also adds tests/suite to path for MockEmailTransport

MATLAB test execution deferred to orchestrator (no MCP access in executor).
…ive ticks

- processMonitorTag_ gains 3rd return value sensorData (struct X/Y/thresholdValue/
  thresholdDirection); well-formed empty struct on every early-return path
- sensorData built from fullX/fullY accumulated grid + first new event's
  ThresholdValue/Direction (matches generateEventSnapshot contract exactly)
- runCycle accumulates allSensorData cell array in parallel with allNewEvents;
  notify(ev, sd) now passes real per-event sensorData instead of struct()
- NotificationService('DryRun', true) constructor default unchanged (backward-compat)
- example_live_pipeline.m: runnable path stays dry-run; added commented real-send
  config block with SmtpServer/SmtpPort/PasswordEnv/SecurityMode/CooldownMinutes
- examples/05-events/smoke_email_send.m: documented manual one-shot real-send using
  FASTSENSE_SMTP_* env vars; STARTTLS:587; gracefully returns when vars unset;
  Octave-safe (EmailTransport guard logs-and-skips)

MATLAB test execution deferred to orchestrator (no MCP access in executor).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or()

Code Analyzer flagged the single-string literal wrapped in [...] in
validateSecurityMode_'s error() call. Cosmetic only; test_invalid_mode
still green. New file is now fully Code-Analyzer-clean (MISS_HIT was
already clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- STATE.md: add 260529-rxf row to Quick Tasks Completed (Status: Verified)
  and bump Last activity. STATE.md is force-tracked under the otherwise
  gitignored .planning/; gsd-tools commit skipped it on a naive .gitignore
  read, so committed directly.
- Force-add the plan doc (260529-rxf-PLAN.md) to match repo convention —
  prior quick tasks track both PLAN.md and SUMMARY.md; the executor's docs
  commit (0e88253) captured only the SUMMARY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nal mailer

FunctionTransport wraps a user-supplied function handle as a NotificationService
Transport, so an existing site/company MATLAB email function can be reused for
background-monitoring alerts with NO SMTP config (no server/port/credentials,
no Gmail App Password). Drop-in: same send(recipients,subject,body,attachments)
signature as EmailTransport (duck-typed), normalizes recipients to a flat
cellstr, defaults attachments to {}, Octave-safe (only calls user code).

Purely additive — EmailTransport/NotificationService behavior unchanged.

  transport = FunctionTransport(@(to,subject,body,attachments) ...
                  companyMail(to,subject,body,attachments));
  notif = NotificationService('DryRun',false,'Transport',transport);

Tests: tests/test_function_transport.m (5/5 — forwarding, recipients
normalization, attachments default, invalid-handle error, NotificationService
integration). test_notification_service still 10/10. MISS_HIT + Code Analyzer
clean. Example gains a commented FunctionTransport option.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 84.93151% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/EventDetection/EmailTransport.m 80.95% 12 Missing ⚠️
libs/EventDetection/NotificationService.m 81.48% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

…tificationService new logic

Codecov flagged FunctionTransport at 0% and the NotificationService
cooldown/delegation lines as uncovered. Root cause: CI's coverage runner
(scripts/run_tests_with_coverage.m) uses TestSuite.fromFolder('tests/suite'),
so it runs ONLY class-based suites — the function-based test_*.m files
(where the new logic was tested) never execute in CI. Mirror those tests
into the class suites so the new code is actually CI-gated and covered:

- NEW tests/suite/TestFunctionTransport.m (5 tests) — forwarding, recipient
  normalization, attachments default, invalid-handle error, NotificationService
  integration. Lands in the E-I batch (passing).
- tests/suite/TestNotificationService.m — +3 methods (transport delegation,
  cooldown-suppress, cooldown-expiry via the Hidden setLastSentForTesting_
  seam) and add tests/suite to the path for MockEmailTransport. J-P batch.

Local: TestFunctionTransport 5/5, TestNotificationService 10/10. MISS_HIT clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HanSur94
Copy link
Copy Markdown
Owner Author

Coverage addressed in a44106e.

Root cause: CI's coverage runner (scripts/run_tests_with_coverage.m) collects from TestSuite.fromFolder('tests/suite')class-based suites only. The new logic was tested in function-based test_*.m files, which that runner never executes — hence FunctionTransport at 0% and the NotificationService cooldown/delegation lines uncovered.

Fix: mirrored the new-logic tests into class suites that CI runs:

  • new tests/suite/TestFunctionTransport.m (5 tests) → covers FunctionTransport
  • extended tests/suite/TestNotificationService.m (+3: transport delegation, cooldown suppress, cooldown expiry) → covers the new NotificationService lines

The remaining EmailTransport uncovered lines are the inherently network-dependent real-send path (no live SMTP in CI — exercised manually via examples/05-events/smoke_email_send.m).

@HanSur94 HanSur94 merged commit 11f4298 into main May 29, 2026
18 of 22 checks passed
@HanSur94 HanSur94 deleted the claude/unruffled-boyd-1288b4 branch May 29, 2026 19:36
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