fix(test-dispatch): fall back to tracker defaults, surface soft errors

- 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.
This commit is contained in:
2026-04-22 01:25:35 +03:00
parent a7a2b4efa4
commit 80c034d2af
2 changed files with 35 additions and 9 deletions
@@ -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 {
@@ -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."
),
}