22 KiB
Executable File
Telegram Notification Provider — Test Failure Remediation Plan
Date: 2026-03-11
Author: Planning Agent
Status: Remediation Required — All security scans pass, test failures block merge
Previous Plan: Archived as docs/plans/telegram_implementation_spec.md
1. Introduction
The Telegram notification provider feature is functionally complete with passing security scans and coverage gates. However, 56 E2E test failures and 2 frontend unit test failures block the PR merge. This plan identifies root causes, categorises each failure set, and provides specific remediation steps.
Failure Summary
| Spec File | Failures | Browsers | Unique Est. | Category |
|---|---|---|---|---|
notifications.spec.ts |
48 | 3 | ~16 | Our change |
notifications-payload.spec.ts |
18 | 3 | ~6 | Our change |
telegram-notification-provider.spec.ts |
4 | 1–3 | ~2 | Our change |
encryption-management.spec.ts |
20 | 3 | ~7 | Pre-existing |
auth-middleware-cascade.spec.ts |
18 | 3 | 6 | Pre-existing |
Notifications.test.tsx (unit) |
2 | — | 2 | Our change |
CI retries: 2 per test (playwright.config.js L144). Failure counts above represent unique test failures × browser projects.
2. Root Cause Analysis
Root Cause A: isNew Guard on Test Button (CRITICAL — Causes ~80% of failures)
What changed: The Telegram feature added a guard in Notifications.tsx (L117-124) that blocks the "Test" button for new (unsaved) providers:
// Line 117-124: handleTest() early return guard
const handleTest = () => {
const formData = watch();
const currentType = normalizeProviderType(formData.type);
if (!formData.id && currentType !== 'email') {
toast.error(t('notificationProviders.saveBeforeTesting'));
return;
}
testMutation.mutate({ ...formData, type: currentType } as Partial<NotificationProvider>);
};
And a disabled attribute on the test button at Notifications.tsx (L382):
// Line 382: Button disabled state
disabled={testMutation.isPending || (isNew && !isEmail)}
Why it was added: The backend Test handler at notification_provider_handler.go (L333-336) requires a saved provider ID for all non-email types. For Gotify/Telegram, the server needs the stored token. For Discord/Webhook, the server still fetches the provider from DB. Without a saved provider, the backend returns MISSING_PROVIDER_ID.
Why it breaks tests: Many existing E2E and unit tests click the test button from a new (unsaved) provider form using mocked endpoints. With the new guard:
- The
<button>isdisabled→ browser ignores clicks → mocked routes never receive requests - Even if not disabled,
handleTest()returns early with a toast instead of callingtestMutation.mutate() - Tests that
waitForRequeston/providers/testtime out (60s default) - Tests that assert on
capturedTestPayloadfindnull
Is the guard correct? Yes — it matches the backend's security-by-design constraint. The tests need to be adapted to the new behavior, not the guard removed.
Root Cause B: Pre-existing Infrastructure Failures (encryption-management, auth-middleware-cascade)
encryption-management.spec.ts (17 tests, ~7 unique failures) navigates to /security/encryption and tests key rotation, validation, and history display. Zero overlap with notification provider code paths. No files modified in the Telegram PR affect encryption.
auth-middleware-cascade.spec.ts (6 tests, all 6 fail) uses deprecated waitUntil: 'networkidle', creates proxy hosts via UI forms (getByLabel(/domain/i)), and tests auth flows through Caddy. Zero overlap with notification code. These tests have known fragility from UI element selectors and networkidle waits.
Verdict: Both are pre-existing failures. They should be tracked separately and not block the Telegram PR.
Root Cause C: Telegram E2E Spec Issues (4 failures)
The telegram-notification-provider.spec.ts has 8 tests, with ~2 unique failures. Most likely candidates:
-
"should edit telegram notification provider and preserve token" (L159): Uses fragile keyboard navigation (focus Send Test → Tab → Enter) to reach the Edit button. If the
titleattribute on the Send Test button doesn't match the accessible name pattern/send test/i, or if the tab order is affected by any intermediate focusable element, the Enter press activates the wrong button or nothing at all. -
"should test telegram notification provider" (L265): Clicks the row-level "Send Test" button. The locator uses
getByRole('button', { name: /send test/i }). The button hastitle={t('notificationProviders.sendTest')}which renders as "Send Test". This should work, but thetitleattribute contributing to accessible name can be browser-dependent, particularly in WebKit.
3. Affected Tests — Complete Inventory
3.1 E2E Tests: notifications.spec.ts (Test Button on New Form)
These tests open the "Add Provider" form (no id), click provider-test-btn, and expect API interactions. The disabled button now prevents all interaction.
| # | Test Name | Line | Type Used | Failure Mode |
|---|---|---|---|---|
| 1 | should test notification provider | L1085 | discord | waitForRequest times out — button disabled |
| 2 | should show test success feedback | L1142 | discord | Success icon never appears — no click fires |
| 3 | should preserve Discord request payload contract for save, preview, and test | L1236 | discord | capturedTestPayload is null — button disabled |
| 4 | should show error when test fails | L1665 | discord | Error icon never appears — no click fires |
Additional cascade effects: The user reports ~16 unique failures from this file. The 4 above are directly caused by the isNew guard. Remaining failures may stem from cascading timeout effects, beforeEach state leakage after long timeouts, or other pre-existing flakiness amplified by the 60s timeout waterfall.
3.2 E2E Tests: notifications-payload.spec.ts (Test Button on New Form)
| # | Test Name | Line | Type Used | Failure Mode |
|---|---|---|---|---|
| 1 | provider-specific transformation strips gotify token from test and preview payloads | L264 | gotify | provider-test-btn disabled for new gotify form; capturedTestPayload is null |
| 2 | retry split distinguishes retryable and non-retryable failures | L410 | webhook | provider-test-btn disabled for new webhook form; waitForResponse times out |
Tests that should still pass:
valid payload flows for discord, gotify, and webhook(L54) — usesprovider-save-btn, not test buttonmalformed payload scenarios(L158) — API-level tests viapage.request.postmissing required fields block submit(L192) — uses save buttonauth/header behavior checks(L217) — API-level testssecurity: SSRF(L314) — API-level testssecurity: DNS-rebinding(L381) — API-level testssecurity: token does not leak(L512) — API-level tests
3.3 E2E Tests: telegram-notification-provider.spec.ts
| # | Test Name | Line | Probable Failure Mode |
|---|---|---|---|
| 1 | should edit telegram notification provider and preserve token | L159 | Keyboard navigation (Tab from Send Test → Edit) fragility; may hit wrong element on some browsers |
| 2 | should test telegram notification provider | L265 | Row-level Send Test button; possible accessible name mismatch in WebKit with title attribute |
Tests that should pass:
- Form rendering tests (L25, L65) — UI assertions only
- Create telegram provider (L89) — mocked POST
- Delete telegram provider (L324) — mocked DELETE + confirm dialog
- Security tests (L389, L436) — mock-based assertions
3.4 Frontend Unit Tests: Notifications.test.tsx
| # | Test Name | Line | Failure Mode |
|---|---|---|---|
| 1 | submits provider test action from form using normalized discord type | L447 | userEvent.click() on disabled button is no-op → testProvider never called → waitFor times out |
| 2 | shows error toast when test mutation fails | L569 | Same — disabled button prevents click → toast.error with saveBeforeTesting fires instead of mutation error |
3.5 Pre-existing (Not Caused By Telegram PR)
| Spec | Tests | Rationale |
|---|---|---|
encryption-management.spec.ts |
~7 unique | Tests encryption page at /security/encryption. No code overlap. |
auth-middleware-cascade.spec.ts |
6 unique | Tests proxy creation + auth middleware. Uses networkidle. No code overlap. |
4. Remediation Plan
Priority Order
- P0 — Fix unit tests (fastest, unblocks local dev verification)
- P1 — Fix E2E test-button tests (the core regression from our change)
- P2 — Fix telegram spec fragility (new tests we added)
- P3 — Document pre-existing failures (not our change, track separately)
4.1 P0: Frontend Unit Test Fixes
File: frontend/src/pages/__tests__/Notifications.test.tsx
Fix 1: "submits provider test action from form using normalized discord type" (L447)
Problem: Test opens "Add Provider" (new form, no id), clicks test button. Button is now disabled for new providers.
Fix: Change to test from an existing provider's edit form instead of a new form. This preserves the original intent (verifying the test payload uses normalized type).
// BEFORE (L447-462):
it('submits provider test action from form using normalized discord type', async () => {
vi.mocked(notificationsApi.testProvider).mockResolvedValue()
const user = userEvent.setup()
renderWithQueryClient(<Notifications />)
await user.click(await screen.findByTestId('add-provider-btn'))
await user.type(screen.getByTestId('provider-name'), 'Preview/Test Provider')
await user.type(screen.getByTestId('provider-url'), 'https://example.com/webhook')
await user.click(screen.getByTestId('provider-test-btn'))
await waitFor(() => {
expect(notificationsApi.testProvider).toHaveBeenCalled()
})
const payload = vi.mocked(notificationsApi.testProvider).mock.calls[0][0]
expect(payload.type).toBe('discord')
})
// AFTER:
it('submits provider test action from form using normalized discord type', async () => {
vi.mocked(notificationsApi.testProvider).mockResolvedValue()
setupMocks([baseProvider]) // baseProvider has an id
const user = userEvent.setup()
renderWithQueryClient(<Notifications />)
// Open edit form for existing provider (has id → test button enabled)
const row = await screen.findByTestId(`provider-row-${baseProvider.id}`)
const buttons = within(row).getAllByRole('button')
await user.click(buttons[1]) // Edit button
await user.click(screen.getByTestId('provider-test-btn'))
await waitFor(() => {
expect(notificationsApi.testProvider).toHaveBeenCalled()
})
const payload = vi.mocked(notificationsApi.testProvider).mock.calls[0][0]
expect(payload.type).toBe('discord')
})
Fix 2: "shows error toast when test mutation fails" (L569)
Problem: Same — test opens new form, clicks test button, expects mutation error toast. Button is disabled.
Fix: Test from an existing provider's edit form.
// BEFORE (L569-582):
it('shows error toast when test mutation fails', async () => {
vi.mocked(notificationsApi.testProvider).mockRejectedValue(new Error('Connection refused'))
const user = userEvent.setup()
renderWithQueryClient(<Notifications />)
await user.click(await screen.findByTestId('add-provider-btn'))
await user.type(screen.getByTestId('provider-name'), 'Failing Provider')
await user.type(screen.getByTestId('provider-url'), 'https://example.com/webhook')
await user.click(screen.getByTestId('provider-test-btn'))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Connection refused')
})
})
// AFTER:
it('shows error toast when test mutation fails', async () => {
vi.mocked(notificationsApi.testProvider).mockRejectedValue(new Error('Connection refused'))
setupMocks([baseProvider])
const user = userEvent.setup()
renderWithQueryClient(<Notifications />)
// Open edit form for existing provider
const row = await screen.findByTestId(`provider-row-${baseProvider.id}`)
const buttons = within(row).getAllByRole('button')
await user.click(buttons[1]) // Edit button
await user.click(screen.getByTestId('provider-test-btn'))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Connection refused')
})
})
Bonus: Add a NEW unit test for the saveBeforeTesting guard
it('disables test button when provider is new (unsaved) and not email type', async () => {
const user = userEvent.setup()
renderWithQueryClient(<Notifications />)
await user.click(await screen.findByTestId('add-provider-btn'))
const testBtn = screen.getByTestId('provider-test-btn')
expect(testBtn).toBeDisabled()
})
4.2 P1: E2E Test Fixes — notifications.spec.ts
File: tests/settings/notifications.spec.ts
Strategy: For tests that click the test button from a new form, restructure the flow to:
- First save the provider (mocked create → returns id)
- Then test from the saved provider row's Send Test button (row buttons are not gated by
isNew)
Fix 3: "should test notification provider" (L1085)
Current flow: Add form → fill → mock test endpoint → click provider-test-btn → verify request
Problem: Test button disabled for new form
Fix: Save first, then click test from the provider row's Send Test button.
// In the test, after filling the form and before clicking test:
// 1. Mock the create endpoint to return a provider with an id
await page.route('**/api/v1/notifications/providers', async (route, request) => {
if (request.method() === 'POST') {
const payload = await request.postDataJSON();
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ id: 'saved-test-id', ...payload }),
});
} else if (request.method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{
id: 'saved-test-id',
name: 'Test Provider',
type: 'discord',
url: 'https://discord.com/api/webhooks/test/token',
enabled: true
}]),
});
} else {
await route.continue();
}
});
// 2. Save the provider first
await page.getByTestId('provider-save-btn').click();
// 3. Wait for the provider to appear in the list
await expect(page.getByText('Test Provider')).toBeVisible({ timeout: 5000 });
// 4. Click row-level Send Test button
const providerRow = page.getByTestId('provider-row-saved-test-id');
const sendTestButton = providerRow.getByRole('button', { name: /send test/i });
await sendTestButton.click();
Fix 4: "should show test success feedback" (L1142)
Same pattern as Fix 3: save provider first, then test from row.
Fix 5: "should preserve Discord request payload contract for save, preview, and test" (L1236)
Current flow: Add form → fill → click preview → click test → save → verify all payloads Problem: Test button disabled for new form Fix: Reorder to: Add form → fill → click preview → save → test from row → verify payloads
The preview button is NOT disabled for new forms (only the test button is), so preview still works from the new form. The test step must happen after save.
Fix 6: "should show error when test fails" (L1665)
Same pattern: save first, then test from row.
4.3 P1: E2E Test Fixes — notifications-payload.spec.ts
File: tests/settings/notifications-payload.spec.ts
Fix 7: "provider-specific transformation strips gotify token from test and preview payloads" (L264)
Current flow: Add gotify form → fill with token → click preview → click test → verify token not in payloads Problem: Test button disabled for new gotify form Fix: Preview still works from new form. For test, save first, then test from the saved provider row.
Note: The row-level test call uses { ...provider, type: normalizeProviderType(provider.type) } where provider is the list item (which never contains token/gotify_token per the List handler that strips tokens). So the token-stripping assertion naturally holds for row-level tests.
Fix 8: "retry split distinguishes retryable and non-retryable failures" (L410)
Current flow: Add webhook form → fill → click test → verify retry semantics
Problem: Test button disabled for new webhook form
Fix: Save first (mock create), then open edit form (which has id) or test from the row.
4.4 P2: Telegram E2E Spec Hardening
File: tests/settings/telegram-notification-provider.spec.ts
Fix 9: "should edit telegram notification provider and preserve token" (L159)
Problem: Uses fragile keyboard navigation to reach the Edit button:
await sendTestButton.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
This assumes Tab from Send Test lands on Edit. Tab order can vary across browsers.
Fix: Use a direct locator for the Edit button instead of keyboard navigation:
// BEFORE:
await sendTestButton.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// AFTER:
const editButton = providerRow.getByRole('button').nth(1); // Send Test=0, Edit=1
await editButton.click();
Or use a structural locator based on the edit icon class.
Fix 10: "should test telegram notification provider" (L265)
Probable issue: The getByRole('button', { name: /send test/i }) relies on title for accessible name. WebKit may not compute accessible name from title the same way.
Fix (source — preferred): Add explicit aria-label to the row Send Test button in Notifications.tsx (L703):
<Button
variant="secondary"
size="sm"
onClick={() => testMutation.mutate({...})}
title={t('notificationProviders.sendTest')}
aria-label={t('notificationProviders.sendTest')}
>
Fix (test — alternative): Use structural locator:
const sendTestButton = providerRow.locator('button').first();
4.5 P3: Document Pre-existing Failures
Action: File separate issues (not part of this PR) for:
-
encryption-management.spec.ts — ~7 unique test failures in
/security/encryption. Likely UI rendering timing issues or flaky selectors. No code overlap with Telegram PR. -
auth-middleware-cascade.spec.ts — All 6 tests fail × 3 browsers. Uses deprecated
waitUntil: 'networkidle', creates proxy hosts through fragile UI selectors (getByLabel(/domain/i)), and tests auth middleware cascade. Needs modernization pass for locators and waits.
5. Implementation Plan
Phase 1: Unit Test Fixes (Immediate)
| Task | File | Lines | Complexity |
|---|---|---|---|
| Fix "submits provider test action" test | Notifications.test.tsx |
L447-462 | Low |
| Fix "shows error toast" test | Notifications.test.tsx |
L569-582 | Low |
Add saveBeforeTesting guard unit test |
Notifications.test.tsx |
New | Low |
Validation: cd frontend && npx vitest run src/pages/__tests__/Notifications.test.tsx
Phase 2: E2E Test Fixes — Core Regression
| Task | File | Lines | Complexity |
|---|---|---|---|
| Fix "should test notification provider" | notifications.spec.ts |
L1085-1138 | Medium |
| Fix "should show test success feedback" | notifications.spec.ts |
L1142-1178 | Medium |
| Fix "should preserve Discord payload contract" | notifications.spec.ts |
L1236-1340 | Medium |
| Fix "should show error when test fails" | notifications.spec.ts |
L1665-1706 | Medium |
| Fix "transformation strips gotify token" | notifications-payload.spec.ts |
L264-312 | Medium |
| Fix "retry split retryable/non-retryable" | notifications-payload.spec.ts |
L410-510 | High |
Validation per test: npx playwright test --project=firefox <spec-file> -g "<test-name>"
Phase 3: Telegram Spec Hardening
| Task | File | Lines | Complexity |
|---|---|---|---|
| Replace keyboard nav with direct locator | telegram-notification-provider.spec.ts |
L220-223 | Low |
Add aria-label to row Send Test button |
Notifications.tsx |
L703-708 | Low |
| Verify all 8 telegram tests pass 3 browsers | All | — | Low |
Validation: npx playwright test tests/settings/telegram-notification-provider.spec.ts
Phase 4: Accessibility Hardening (Optional — Low Priority)
Consider adding aria-label attributes to all icon-only buttons in the provider row for improved accessibility and test resilience:
| Button | Current Accessible Name Source | Recommended |
|---|---|---|
| Send Test | title attribute |
Add aria-label |
| Edit | None (icon only) | Add aria-label={t('common.edit')} |
| Delete | None (icon only) | Add aria-label={t('common.delete')} |
6. Commit Slicing Strategy
Decision: Single PR with 2 focused commits
Rationale: All fixes are tightly coupled to the Telegram feature PR and represent test adaptations to a correct behavioral change. No cross-domain changes. Small total diff.
Commit 1: "fix(test): adapt notification tests to save-before-test guard"
- Scope: All unit test and E2E test fixes (Phases 1-3)
- Files:
Notifications.test.tsx,notifications.spec.ts,notifications-payload.spec.ts,telegram-notification-provider.spec.ts - Dependencies: None
- Validation Gate: All notification-related tests pass locally on at least one browser
Commit 2: "feat(a11y): add aria-labels to notification provider row buttons"
- Scope: Source code accessibility improvement (Phase 4)
- Files:
Notifications.tsx - Dependencies: Depends on Commit 1 (tests must pass first)
- Validation Gate: Telegram spec tests pass consistently on WebKit
Rollback
- These are test-only changes (except the optional aria-label). Reverting either commit has zero production impact.
- If tests still fail after fixes, the next step is to run with
--debugand capture trace artifacts.
7. Acceptance Criteria
Notifications.test.tsx— all 2 previously failing tests passnotifications.spec.ts— all 4 isNew-guard-affected tests pass on 3 browsersnotifications-payload.spec.ts— "transformation" and "retry split" tests pass on 3 browserstelegram-notification-provider.spec.ts— all 8 tests pass on 3 browsers- No regressions in other notification tests
- New unit test validates the
saveBeforeTestingguard / disabled button behavior encryption-management.spec.tsandauth-middleware-cascade.spec.tsfailures documented as separate issues (not blocked by this PR)