Lite's MCP reports every alert group Darling's does - #2410
Conversation
get_alert_settings reported four groups -- cpu, blocking, deadlocks, smtp -- where Darling reports nineteen, and the gap was never that Lite's alerting is narrower. The engine is shared: PerformanceMonitor.Alerting evaluates the same alerts on both SKUs against IAlertEngineSettings, and AppAlertEngineSettings has projected every one of Lite's App.Alert* statics onto that interface since the Phase-5 forwarding. So an operator could have tempdb-space, low-disk, PVS, file-growth, long-running-query, long-running-job, failed-job, database-state and analysis alerting all running, and an agent triaging that instance could not read whether any of it was even switched on. Thirteen groups are added, spelled Darling's way down to the last key, because one MCP schema across both apps is what #1839 and #1911 were for. The two that are easy to get wrong were checked against Darling's emitted shape rather than guessed: low_disk nests the #1136 CRITICAL floors as critical_free_percent and critical_free_gb, dropping the "disk" the App statics carry, and delivery.mode is a value-level alignment like cpu.mode -- though it is the one place ToString() is right rather than a mapping, because AlertNotificationMode is the shared enum both SKUs run on and Darling's store holds literally its ToString(). self_alerts is the one group deliberately left out. AppAlertEngineSettings returns shipped constants for three of its four members precisely because a single-instance WPF app has no headless store volume and no fleet collection loop to self-monitor, and Lite has no concept at all of the fourth, store_job_cadence_warn_percent. Reporting constants under names that read as knobs would tell an agent it can tune something Lite cannot, and an admitted gap beats an overstated capability. McpAlertSettingsKeyTests grows from pinning three renamed keys to pinning the whole shape, and it DERIVES Darling's shape from Darling's source rather than transcribing it -- a hand-copied list of nineteen groups is exactly the artifact that stays green on the day a twentieth arrives, which is the drift this turned out to be. Both directions are asserted, so a key Lite emits that Darling does not is a failure too, and smtp is pinned as its one legitimate addition. No update_alert_settings here. Darling's writes land in config_alert_settings and the service reloads through StoreConfigProvider.ApplyToConfig, whereas Lite's settings are App.Alert* statics the Settings window owns, and SaveAlertSettings reads its CONTROLS rather than those statics -- so an open Settings window would silently overwrite anything an MCP write applied. That wants a change-notification seam, not a tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewVerified this line-by-line against Darling's source of truth (
No correctness, security, or parity issues found. One non-blocking observation: Nice work on the self-verification methodology described in the PR body (running the shipped parser against real files, confirming the pre-fix failure mode, and confirming |
#2410 landed on dev between this branch's base and its first CI run, and it changed the rules: GetAlertSettings_ReportsEveryGroupDarlingDoes_SpelledDarlingsWay parses Darling's BuildAlertSettingsPayload and requires Lite's payload to carry every group and key it finds. So Darling's half of #2417 could not merge alone after all -- the Lite build went red naming exactly the three additions, missing group 'notify_connection_down_at_startup', 'connection_refire_minutes' and 'ag'. That test is doing precisely what it was written to do, and the answer is to make Lite follow now rather than to weaken it. Lite needs no new plumbing for six of the seven. App.NotifyConnectionDownAtStartup and App.ConnectionRefireMinutes have driven the connection-alert path since #1659, and App.NotifyAgHealth, App.AgLagAlertSeconds and App.AgRedoQueueAlertKb have driven all four AG conditions since #1726. Only the MCP surface was narrow, which is the same shape of gap #2394 fixed for the other nine groups. The seventh is a real asymmetry rather than an oversight, so it is exempted by name instead of faked. Darling's ag_disconnect_refire_minutes is #1696 / store V37, and Lite has no AG disconnect re-fire at ALL -- no static, no settings.json key, and no edge state that could re-announce a still-disconnected replica, because #1726 mirrored three of those four knobs. Emitting it as a constant 0 would tell an agent it can tune something Lite cannot, which is the reasoning the file already applies to self_alerts, here at member granularity instead of group. LiteOmittedMembers holds the one entry, the comparison skips exactly that key, and a new test asserts BOTH halves still hold -- that Darling still emits it, so the exemption cannot become dead weight hiding a Darling regression, and that Lite still does not, so it cannot mask a key that has since arrived. The set is pinned exactly, so a second exemption has to be justified in that test the way a second Lite-only group has to be justified in the smtp assertion. Lite.Tests cannot run on macOS, so the parity test's parser was ported to Python and validated against the test's OWN self-checks before being trusted -- the 15-40 group count, cpu/analysis/self_alerts present, smtp absent, and cpu's members exactly enabled/threshold_percent/mode. Run against dev's Lite payload it reproduces CI's failure message byte for byte; against this branch's it passes; and with the exemption removed it fails on ag.disconnect_refire_minutes and nothing else, which is what shows the exemption is both load-bearing and minimal. Darling's own round-trip invariant re-ran green after the merge. The gap itself is worth closing and is not closed here: a replica disconnected for a week is announced once on Lite where Darling can re-announce it. That is #1696's Lite half, a feature with its own static, settings row, box and edge state, and it does not belong in a payload-symmetry change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #2394 — scope 1 only, read-only parity.
Lite's
get_alert_settingsreported four groups (cpu,blocking,deadlocks,smtp) where Darling's reports nineteen. The issue's framing holds up completely against the source: nothing about Lite's alerting is narrower.PerformanceMonitor.Alertingevaluates the same alerts on both SKUs againstIAlertEngineSettings, andAppAlertEngineSettingsalready projects every one of Lite'sApp.Alert*statics onto that interface. So an operator could have tempdb-space, low-disk, PVS, file-growth, long-running-query, long-running-job, failed-job, database-state and analysis alerting all live, and an agent triaging that instance could not read whether any of it was even switched on. The gap was purely the MCP surface, and this closes it.Thirteen groups are added —
poison_wait,long_running_query,tempdb_space,low_disk,pvs,file_growth,long_running_job,failed_job,database_state,cooldown_minutes,excluded_databases,delivery,analysis— spelled Darling's way down to the last key, in Darling's order, because one MCP schema across both apps is what #1839 and #1911 were for.The two spellings that are easy to get wrong were checked against Darling's emitted shape rather than guessed.
low_disknests the #1136 CRITICAL floors ascritical_free_percent/critical_free_gb, dropping the "disk" that theApp.AlertDiskCriticalFree*statics carry — naming them after the statics would have read as correct and been a fifth key-level mismatch. Anddelivery.modeis a value-level alignment likecpu.mode, though it is the one placeToString()is right rather than a mapping:AlertNotificationModeis the shared enum both SKUs run on and Darling's store holds literallya.DeliveryMode.ToString(), so the two cannot drift the way Lite's app-localCpuAlertModecould.The one group not added
self_alerts— Darling's own store volume, collection staleness/failure counts, and store-job cadence. This is a genuine Darling-only concept rather than an omission of convenience.AppAlertEngineSettingsreturns shipped constants (10,30,10) for three of its four members precisely because a single-instance WPF app has no headless store volume and no fleet collection loop to self-monitor, and Lite has no member at all for the fourth,store_job_cadence_warn_percent. Reporting constants under names that read as knobs would tell an agent it can tune something Lite cannot, and an admitted gap beats an overstated capability. The omission is asserted as a decision, not left to look like an oversight.smtpstays as Lite's one addition — Lite delivers its own email, where Darling manages delivery credentials outside the settings row and reports nosmtpgroup at all.Why there is no
update_alert_settingsOut of scope, and the issue is right that it needs a design decision first. Darling's tools write
config_alert_settingsand the service picks the change up throughStoreConfigProvider.ApplyToConfig— a live reload with no restart. Lite has no equivalent plane: its settings areApp.Alert*statics that the Settings window writes to settings.json, andSaveAlertSettingsreads its controls, not the statics — so an open Settings window would silently overwrite an MCP-applied change on the next Save. That wants a change-notification seam (the issue's scope 3), not a tool bolted on next to the hazard.Tests
McpAlertSettingsKeyTestsis extended, not rewritten — its existing assertions are all still there. It grows from pinning three renamed keys to pinning the whole shape, which is the lesson of this issue: the drift it was written to catch had already happened at a scale no per-key assertion would notice.The new parity assertion derives Darling's shape from Darling's source (
BuildAlertSettingsPayload) rather than transcribing it. A hand-copied list of nineteen groups is exactly the artifact that stays green on the day a twentieth arrives. Both directions are asserted, so a key Lite emits that Darling does not fails too — two spellings of one setting across the two apps is the same class of bug as a missing one — andsmtpis pinned as the exact set of legitimate Lite-only additions, so a second one cannot appear without someone justifying it here.The parser proves itself before it is trusted: it asserts the parse found a plausible number of groups and that known anchors (
cpu,analysis,self_alerts, andcpu's three nested keys) are present, because a parse that quietly returned nothing would make every assertion built on it vacuously true.Verification, and what it does and does not cover
Lite.Teststargetsnet10.0-windows, so it builds on macOS but cannot run; CI is the arbiter.McpAlertSettingsKeyTestsis a runtime test, so rather than assert it passes, the logic was verified three ways against the real files:DarlingPayloadShapescanning loop was copied verbatim into a throwawaynet10.0harness and run against the actualDarlingMcpAlertTools.csandMcpAlertTools.cs. It reads 19 groups from Darling with correct nested keys, and the parity comparison passes group-for-group and key-for-key.dev's pre-fixMcpAlertTools.csreports 6 groups and 13 parity problems — every group this PR adds. A check that passed both before and after would be worse than none.McpHelpers.JsonOptionsby reflection (it isinternal) and confirms it carries no naming policy, that snake_case identifiers survive verbatim at both nesting levels, thatList<string>serializes as an array under its identifier, and thatAlertNotificationMode.Summary/PerEvent.ToString()are byte-for-byte the two values Darling'supdate_alert_settingsaccepts fordelivery.mode.What that does not cover: the
App.Alert*static reads themselves, which need a WPF assembly and therefore CI.Notes for review
No CHANGELOG entry, deliberately. #2395 is cutting
[3.5.1]right now and dev has no[Unreleased]section, so an entry here would either collide with that diff or wrongly claim to be in a release this landed after. Happy to fold one into the next section once the cut lands.One correction rode along: the class doc-comment claimed the payload is serialized "with a naming policy in
McpHelpers.JsonOptions". There is no naming policy — the options object isnew() { WriteIndented = false }. The reason the test is runtime rather than source-parsing is still sound (that object owns the mapping and could acquire a policy tomorrow, changing every key without this payload being touched), so the reasoning is kept and the false fact is fixed.Two things worth separate issues, found while matching the shape — neither touched here:
AlertSettingsReadRowreadsnotify_connection_down_at_startup,connection_refire_minutes,notify_ag_health,ag_lag_alert_seconds,ag_redo_queue_alert_kbandag_disconnect_refire_minutes, andBuildAlertSettingsPayloadreports none of them. The reader's own comment says it was widened so "an MCP client could not see the V33 connection opt-ins or the V35 Availability Group family at all" — the reader was widened and the payload was not, so neither SKU reports the AG family. Lite has the matching statics (App.NotifyAgHealth,AgLagAlertSeconds,AgRedoQueueAlertKb), so this is a both-SKUs gap, and it is Darling's payload that has to move first for parity to mean anything.get_alert_settingsemitsblocking.wait_threshold_seconds, butupdate_alert_settings'blockinggroup dispatches only onenabledandcount_threshold— so feeding a fullget_alert_settingspayload back to it is rejected withUnknown field 'blocking.wait_threshold_seconds'.BuildAlertSettingsPayload's doc comment promises "the same field names update_alert_settings accepts on the way in, so a read → modify → write round-trips", and for that one key it does not.🤖 Generated with Claude Code