From 80c034d2afb2f9615a0fbc632f46409bc413d123 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Wed, 22 Apr 2026 01:25:35 +0300 Subject: [PATCH] fix(test-dispatch): fall back to tracker defaults, surface soft errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dispatch_test_notification now resolves tracking_config / template_config from the tracker's default_* fields when the per-link override is unset, matching what load_link_data does for the real watcher. Previously periodic/scheduled/memory tests silently failed with "no template defined" whenever the user configured the template config at the tracker level instead of on each link (the UI's normal default). - Distinguish the two missing-template cases in the returned error ("no template config linked" vs. "slot missing in linked config"). - Frontend testTrackerTarget now treats {success:false,error:"..."} in a 2xx body as a failure — previously any 2xx flashed a success snack so users never saw the real reason their test didn't deliver. --- .../routes/notification-trackers/+page.svelte | 15 ++++++++-- .../services/test_dispatch.py | 29 ++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/frontend/src/routes/notification-trackers/+page.svelte b/frontend/src/routes/notification-trackers/+page.svelte index b3c5f2a..108efa0 100644 --- a/frontend/src/routes/notification-trackers/+page.svelte +++ b/frontend/src/routes/notification-trackers/+page.svelte @@ -358,8 +358,19 @@ if (ttTesting[key]) return; ttTesting = { ...ttTesting, [key]: testType }; try { - await api(`/notification-trackers/${trackerId}/targets/${ttId}/test/${testType}?locale=${getLocale()}`, { method: 'POST' }); - snackSuccess(t('snack.targetTestSent')); + // The endpoint returns 200 OK with ``{success: false, error: "..."}`` + // on soft failures (missing template slot, no matching assets, + // provider unreachable, etc.), so checking for a thrown exception + // is not enough. Surface ``error`` as a snackError when present. + const res = await api<{ success?: boolean; error?: string; target?: string }>( + `/notification-trackers/${trackerId}/targets/${ttId}/test/${testType}?locale=${getLocale()}`, + { method: 'POST' }, + ); + if (res && res.success === false) { + snackError(res.error || t('common.error')); + } else { + snackSuccess(t('snack.targetTestSent')); + } } catch (err: any) { snackError(err.message); } finally { diff --git a/packages/server/src/notify_bridge_server/services/test_dispatch.py b/packages/server/src/notify_bridge_server/services/test_dispatch.py index a5c01b3..a8676da 100644 --- a/packages/server/src/notify_bridge_server/services/test_dispatch.py +++ b/packages/server/src/notify_bridge_server/services/test_dispatch.py @@ -55,17 +55,21 @@ async def dispatch_test_notification( provider_config = dict(provider.config) collection_ids = list(tracker.collection_ids or []) - # Load tracking config + # Resolve tracking config: per-link override, else the tracker's default. + # The real watcher applies this fallback in ``load_link_data`` — tests + # must use the same logic or the user's per-tracker defaults look broken. + tracking_config_id = tt.tracking_config_id or tracker.default_tracking_config_id tracking_config = None - if tt.tracking_config_id: - tracking_config = await session.get(TrackingConfig, tt.tracking_config_id) + if tracking_config_id: + tracking_config = await session.get(TrackingConfig, tracking_config_id) - # Load template slots keyed by EventType.SCHEDULED_MESSAGE.value + # Same fallback for template config. + template_config_id = tt.template_config_id or tracker.default_template_config_id template_config = None template_slots: dict[str, dict[str, str]] | None = None slot_name = _TEST_TYPE_SLOT_MAP.get(test_type, test_type) - if tt.template_config_id: - template_config = await session.get(TemplateConfig, tt.template_config_id) + if template_config_id: + template_config = await session.get(TemplateConfig, template_config_id) if template_config: slot_result = await session.exec( select(TemplateSlot).where( @@ -97,10 +101,21 @@ async def dispatch_test_notification( ) if not template_slots: + if not template_config_id: + return { + "success": False, + "error": ( + "This tracker has no Template Config linked (neither on the " + "tracker's default nor on this target link). Assign one in the " + "tracker settings and make sure it defines a " + f"'{slot_name}' slot." + ), + } return { "success": False, "error": ( - f"No '{slot_name}' template defined for this target's template config " + f"No '{slot_name}' template defined in the linked Template Config " + f"'{template_config.name if template_config else template_config_id}' " f"(locale: {locale}). Add the slot under Template Configs." ), }