mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
test(alerts): verify saved recovery intent across backend restart
Exercise the production Schedule control and real configuration API on an owned local backend without alert endpoint mocks. Keep persistence evidence distinct from installed notification delivery. Change-source: pulse-maintainer
This commit is contained in:
@@ -81,3 +81,27 @@ Chromium or Playwright changes. Ordinary runs use the unmodified base fixtures.
|
||||
For the ordinary-journey control, repeat the command above with
|
||||
`PULSE_E2E_INCIDENT_FOREGROUND` unset. Retain both exact-revision results;
|
||||
a prior success is not evidence for a later untested revision.
|
||||
|
||||
## Saved alert intent (unmocked local backend)
|
||||
|
||||
```sh
|
||||
pulse-heavy-run -- env PULSE_E2E_USE_LOCAL_BACKEND=1 PULSE_MOCK_MODE=false \
|
||||
PULSE_E2E_ALERT_CONFIG_PERSISTENCE=1 PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL=1 \
|
||||
npm --prefix tests/integration test -- tests/97-alert-config-persistence.spec.ts \
|
||||
--project=chromium --workers=1 --retries=0
|
||||
```
|
||||
|
||||
This opt-in test requires the managed, disposable backend. It activates that
|
||||
instance through the real configuration API, changes Recovery notifications
|
||||
through the production Schedule UI, verifies the staged value has not reached
|
||||
the server, and clicks Save Changes. It checks the real PUT result, subsequent
|
||||
GETs and rendered control after both page reload and managed backend restart
|
||||
with preserved data. It does not intercept HTTP or WebSocket responses and
|
||||
must not be run against a shared installation. The attachment contains only
|
||||
expected non-secret settings and evidence boundaries, not full configuration.
|
||||
|
||||
This proves saved recovery intent only when the test passes. API activation
|
||||
setup is not UI activation coverage, and no resource or destination is seeded.
|
||||
It does not establish alert-engine firing, retry exhaustion, delivered recovery,
|
||||
recurrence or off-LAN receipt. Those require separate evidence; notification
|
||||
package loopback tests are not a substitute for installed exact-pair acceptance.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { expect, test as base } from '@playwright/test';
|
||||
|
||||
import { restartManagedLocalBackend } from '../scripts/managed-local-backend.mjs';
|
||||
import { readRuntimeState } from '../scripts/runtime-state.mjs';
|
||||
import {
|
||||
apiRequest,
|
||||
createAuthenticatedStorageState,
|
||||
ensureAuthenticated,
|
||||
} from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) =>
|
||||
use(authStorageStatePath),
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`alert-config-persistence-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
});
|
||||
|
||||
// No HTTP or WebSocket interception: this owns an isolated managed backend.
|
||||
test('saves recovery intent through the UI and preserves it across reload and restart', async ({ page }, testInfo) => {
|
||||
test.skip(process.env.PULSE_E2E_ALERT_CONFIG_PERSISTENCE !== '1', 'Requires an owned managed backend');
|
||||
test.skip(testInfo.project.name !== 'chromium', 'One worker owns backend restart');
|
||||
test.setTimeout(240_000);
|
||||
const runtime = await readRuntimeState();
|
||||
expect(runtime?.managedLocalBackend).toBe(true);
|
||||
expect(process.env.PULSE_MOCK_MODE).toBe('false');
|
||||
const readConfig = async () => {
|
||||
const response = await apiRequest(page, '/api/alerts/config');
|
||||
expect(response.ok(), `config GET: ${response.status()}`).toBe(true);
|
||||
return response.json();
|
||||
};
|
||||
await page.goto('/alerts/overview', { waitUntil: 'domcontentloaded' });
|
||||
const original = await readConfig();
|
||||
// Activate only the disposable instance; no destinations or resources are seeded.
|
||||
const setup = await apiRequest(page, '/api/alerts/config', {
|
||||
method: 'PUT', data: { ...original, enabled: true, activationState: 'active',
|
||||
schedule: { ...original.schedule, notifyOnResolve: true } },
|
||||
});
|
||||
expect(setup.ok(), `config setup: ${setup.status()}`).toBe(true);
|
||||
await page.goto('/alerts/schedule', { waitUntil: 'domcontentloaded' });
|
||||
const recovery = page.getByRole('button', { name: /^Recovery notifications/ });
|
||||
// Persist false, not the default true, so a reset cannot satisfy restart proof.
|
||||
const initial = true;
|
||||
await expect(recovery).toHaveAttribute('aria-pressed', String(initial));
|
||||
await recovery.click();
|
||||
// A staged edit must not already have altered the server.
|
||||
expect((await readConfig()).schedule.notifyOnResolve).toBe(initial);
|
||||
const saved = page.waitForResponse(response =>
|
||||
new URL(response.url()).pathname === '/api/alerts/config' && response.request().method() === 'PUT');
|
||||
await page.getByRole('button', { name: 'Save Changes', exact: true }).click();
|
||||
expect((await saved).ok()).toBe(true);
|
||||
const expected = { enabled: true, activationState: 'active', schedule: { notifyOnResolve: !initial } };
|
||||
expect(await readConfig()).toMatchObject(expected);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(recovery).toHaveAttribute('aria-pressed', String(!initial));
|
||||
expect(await readConfig()).toMatchObject(expected);
|
||||
await restartManagedLocalBackend();
|
||||
await ensureAuthenticated(page);
|
||||
await page.goto('/alerts/schedule', { waitUntil: 'domcontentloaded' });
|
||||
await expect(recovery).toHaveAttribute('aria-pressed', String(!initial));
|
||||
expect(await readConfig()).toMatchObject(expected);
|
||||
await testInfo.attach('saved-intent-proof.json', {
|
||||
body: Buffer.from(JSON.stringify({ expected, reload: true, backendRestart: true,
|
||||
mockedAlertEndpoints: false, destinationReceiptProven: false })),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user