fix(mock): scope /reset by MAC so parallel specs stop wiping each other

The re-authentication test passed locally but failed all three CI
attempts: no request carried a token, because self-hosted.e2e.ts issues
a GLOBAL `POST /reset` against the same mock from a parallel Playwright
worker, destroying the session mid-import. Running only stalker.e2e.ts
locally never triggered it.

Serializing within one file (4b31f7167) could not fix this — the
interference is between files. Mock state is per-MAC, so `/reset` now
accepts `?macAddress=` and clears only that MAC's data, favorites,
session and watchdog counters; the unscoped form is kept for callers
that own the whole server. Both spec files now reset only the MACs they
own, so no worker can disturb another.

Verified: a scoped reset of one MAC leaves another MAC's session intact
(and its own dies), and stalker.e2e.ts + self-hosted.e2e.ts run together
23/23 green — the combination that reproduced the CI failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-01 19:55:09 +02:00
1 parent 23d0ca8afd
commit 3a93fef0f3
6 files changed
+89 -13

No files matched your search

@@ -201,7 +201,11 @@ export function invalidateSession(mac: string): void {
session.pendingToken = undefined;
}
export function resetAuthState(): void {
export function resetAuthState(mac?: string): void {
if (mac) {
sessions.delete(mac.toLowerCase());
return;
}
sessions.clear();
tokenCounter = 0;
}
@@ -44,6 +44,17 @@ export function resetFavorites(mac: string): void {
favoritesStore.delete(mac.toLowerCase());
}
/**
* Reset everything cached for one MAC. Preferred over `resetAll()` in tests:
* mock state is per-MAC, so a scoped reset cannot disturb a spec that runs
* concurrently against a different MAC.
*/
export function resetMac(mac: string): void {
const key = mac.toLowerCase();
portalCache.delete(key);
favoritesStore.delete(key);
}
/** Reset all cached data (exposed via /reset endpoint). */
export function resetAll(): void {
portalCache.clear();
@@ -35,6 +35,10 @@ export function getWatchdogPings(
return watchdogPings.get(mac.toLowerCase());
}
export function resetWatchdogPings(): void {
export function resetWatchdogPings(mac?: string): void {
if (mac) {
watchdogPings.delete(mac.toLowerCase());
return;
}
watchdogPings.clear();
}
+29 -7
View File
@@ -6,7 +6,7 @@ import portalRouter, { createPortalRouter } from './app/routes/portal.route.js';
import dispatchPortalAction from './app/routes/dispatch.js';
import { invalidateSession, resetAuthState } from './app/auth-store.js';
import { resetWatchdogPings } from './app/handlers/get-events.handler.js';
import { resetAll } from './app/data-store.js';
import { resetAll, resetMac } from './app/data-store.js';
import { SCENARIOS } from './app/scenarios.js';
import {
buildRequestOrigin,
@@ -173,12 +173,34 @@ app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Reset all in-memory data (useful between Playwright test runs)
app.post('/reset', (_req: Request, res: Response) => {
resetAll();
resetAuthState();
resetWatchdogPings();
res.json({ status: 'reset', timestamp: new Date().toISOString() });
/**
* Reset in-memory state between test runs.
*
* `?macAddress=<mac>` scopes the reset to that MAC and is what specs should
* use: mock state is per-MAC, so a scoped reset cannot wipe the session of a
* spec file running concurrently in another Playwright worker. Without the
* parameter everything is cleared, which is only safe when nothing else is
* talking to this server.
*/
app.post('/reset', (req: Request, res: Response) => {
const macParam = req.query['macAddress'];
const mac = typeof macParam === 'string' ? macParam : undefined;
if (mac) {
resetMac(mac);
resetAuthState(mac);
resetWatchdogPings(mac);
} else {
resetAll();
resetAuthState();
resetWatchdogPings();
}
res.json({
status: 'reset',
...(mac ? { mac } : {}),
timestamp: new Date().toISOString(),
});
});
/**
+7 -1
View File
@@ -102,7 +102,13 @@ function expectRequestsUseTargetId(requests: string[], path: string): void {
test.beforeEach(async ({ page, request }) => {
await request.post(`${XTREAM_MOCK_SERVER}/reset`);
await request.post(`${STALKER_MOCK_SERVER}/reset`);
// Scope the Stalker reset to the MAC this file uses: a global reset would
// wipe the sessions of stalker.e2e.ts running in a parallel worker.
await request.post(
`${STALKER_MOCK_SERVER}/reset?macAddress=${encodeURIComponent(
DEFAULT_MAC
)}`
);
await installRuntimeConfig(page);
await page.goto('/');
});
+32 -3
View File
@@ -106,12 +106,38 @@ async function interceptStalkerRequests(page: Page): Promise<void> {
});
}
/** Every MAC this file touches; each is reset individually (see below). */
const OWNED_MACS = [
DEFAULT_MAC,
MINIMAL_MAC,
EMBEDDED_SERIES_MAC,
LEGACY_PAGINATION_MAC,
AUTH_FLOW_MAC,
AUTH_REAUTH_MAC,
AUTH_REJECTED_MAC,
];
/**
* Reset only the MACs this file owns. Mock state is per-MAC and other spec
* files (self-hosted.e2e.ts) talk to the same server from a parallel worker,
* so a global reset here would wipe their sessions mid-test — and theirs
* would wipe ours.
*/
async function resetMockServer(request: APIRequestContext): Promise<void> {
await Promise.all(OWNED_MACS.map((mac) => resetMockServerMac(request, mac)));
}
async function resetMockServerMac(
request: APIRequestContext,
mac: string
): Promise<void> {
let lastError: unknown;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const response = await request.post(`${MOCK_SERVER}/reset`);
const response = await request.post(
`${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(mac)}`
);
if (response.ok()) {
return;
}
@@ -630,9 +656,12 @@ test('@stalker mock server reset clears cached state', async ({ request }) => {
);
expect(before.ok()).toBeTruthy();
// Reset
const reset = await request.post(`${MOCK_SERVER}/reset`);
// Scoped reset — a global one would clear MACs owned by parallel specs.
const reset = await request.post(
`${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(DEFAULT_MAC)}`
);
expect(reset.ok()).toBeTruthy();
expect((await reset.json()).mac).toBe(DEFAULT_MAC);
// Data is regenerated identically (deterministic seed)
const after = await request.get(