Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/strategy_plugin_runtime_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ structured sent/skipped/failed diagnostics, and can use
sent.

Platforms should expose this as Google Voice notification config, not as a
generic email alert surface. The public configuration names should be channel
specific:
generic email alert surface. The recipient value is still an email-form address:
a normal mailbox receives an email, while a Google Voice mailbox/address can
also surface the Google Voice prompt. The public configuration names should be
channel specific:

- `CRISIS_ALERT_GOOGLE_VOICE_GATEWAY`
- `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS`
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER`
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

@dataclass(frozen=True)
class StrategyPluginGoogleVoiceSettings:
gateway_recipients: tuple[str, ...] = ()
recipients: tuple[str, ...] = ()
gmail_user: str | None = None
gmail_app_password: str | None = field(default=None, repr=False)
timeout: float = 10.0
Expand All @@ -36,17 +36,17 @@ def from_object(cls, value: object) -> "StrategyPluginGoogleVoiceSettings":
if isinstance(value, cls):
return value
return cls(
gateway_recipients=tuple(
parse_email_recipients(_get_value(value, "crisis_alert_google_voice_gateway", ()))
recipients=tuple(
parse_email_recipients(_get_value(value, "crisis_alert_google_voice_recipients", ()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve legacy gateway key during recipients migration

This change makes StrategyPluginGoogleVoiceSettings.from_object read only crisis_alert_google_voice_recipients, so existing environments that still provide CRISIS_ALERT_GOOGLE_VOICE_GATEWAY will now parse zero recipients and be treated as misconfigured, causing every Google Voice alert to be skipped with missing_google_voice_config. Because this is a runtime config regression for already-deployed setups, the loader should accept both keys (at least during a deprecation window).

Useful? React with 👍 / 👎.

),
gmail_user=_first_non_empty(_get_value(value, "crisis_alert_google_voice_gmail_user")),
gmail_app_password=_get_value(value, "crisis_alert_google_voice_gmail_app_password"),
)

def missing_fields(self) -> tuple[str, ...]:
missing: list[str] = []
if not parse_email_recipients(self.gateway_recipients):
missing.append("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY")
if not parse_email_recipients(self.recipients):
missing.append("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")
if not str(self.gmail_user or "").strip():
missing.append("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")
if not str(self.gmail_app_password or "").strip():
Expand Down Expand Up @@ -281,7 +281,7 @@ def _send_message(
smtp_host=_GOOGLE_VOICE_SMTP_HOST,
smtp_port=_GOOGLE_VOICE_SMTP_PORT,
sender=settings.gmail_user,
recipients=settings.gateway_recipients,
recipients=settings.recipients,
username=settings.gmail_user,
password=settings.gmail_app_password,
use_starttls=_GOOGLE_VOICE_SMTP_STARTTLS,
Expand Down
12 changes: 6 additions & 6 deletions tests/test_google_voice_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_missing_config():
assert result.sent_count == 0
assert result.skipped_count == 1
assert result.deliveries[0].reason == "missing_google_voice_config"
assert "CRISIS_ALERT_GOOGLE_VOICE_GATEWAY" in result.deliveries[0].error
assert "CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS" in result.deliveries[0].error
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER" in result.deliveries[0].error
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" in result.deliveries[0].error
assert observed == []
Expand All @@ -99,7 +99,7 @@ def test_publish_strategy_plugin_google_voice_alerts_sends_and_records_marker(tm
result = publish_strategy_plugin_google_voice_alerts(
[_alert_signal()],
google_voice_settings=StrategyPluginGoogleVoiceSettings(
gateway_recipients=("risk@example.com",),
recipients=("risk@example.com",),
gmail_user="bot@example.com",
gmail_app_password="app-password",
),
Expand Down Expand Up @@ -128,7 +128,7 @@ def test_publish_strategy_plugin_google_voice_alerts_sends_and_records_marker(tm
def test_publish_strategy_plugin_google_voice_alerts_skips_duplicate_marker(tmp_path):
store = StrategyPluginGoogleVoiceAlertMarkerStore(local_dir=tmp_path)
settings = StrategyPluginGoogleVoiceSettings(
gateway_recipients=("risk@example.com",),
recipients=("risk@example.com",),
gmail_user="bot@example.com",
gmail_app_password="app-password",
)
Expand Down Expand Up @@ -158,16 +158,16 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_duplicate_marker(tmp_
assert second.deliveries[0].reason == "duplicate_alert"


def test_google_voice_settings_reads_google_voice_gmail_names_only():
def test_google_voice_settings_reads_google_voice_recipient_names_only():
settings = StrategyPluginGoogleVoiceSettings.from_object(
SimpleNamespace(
crisis_alert_google_voice_gateway="gateway@txt.voice.google.com",
crisis_alert_google_voice_recipients="alerts@example.com; voice@example.com",
crisis_alert_google_voice_gmail_user="sender@gmail.com",
crisis_alert_google_voice_gmail_app_password="app-password",
)
)

assert settings.gmail_user == "sender@gmail.com"
assert settings.gateway_recipients == ("gateway@txt.voice.google.com",)
assert settings.recipients == ("alerts@example.com", "voice@example.com")
assert settings.gmail_app_password == "app-password"
assert settings.missing_fields() == ()