Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from '@playwright/test';

import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../../testUtils';

/**
* Tests that the token cache's proactive refresh timer does not accumulate
* orphaned timers across set() calls.
*
* Background: Every API response that piggybacks client data triggers _updateClient,
* which reconstructs Session objects and calls #hydrateCache → SessionTokenCache.set().
* Without proper timer cleanup, each set() call would leave the previous refresh timer
* running, causing the effective polling rate to accelerate over time.
*/
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'Token refresh timer cleanup @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('touch does not cause clustered token refresh requests', async ({ page, context }) => {
test.setTimeout(120_000);
const u = createTestUtils({ app, page, context });

await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

// Track token fetch requests with timestamps
const tokenRequests: number[] = [];
await page.route('**/v1/client/sessions/*/tokens**', async route => {
tokenRequests.push(Date.now());
await route.continue();
});

// Trigger multiple touch() calls — each causes _updateClient → Session constructor
// → #hydrateCache → set(), which previously leaked orphaned refresh timers
for (let i = 0; i < 5; i++) {
await page.evaluate(async () => {
await (window as any).Clerk?.session?.touch();
});
}

// Wait 50s — enough for one refresh cycle (~43s) but not two
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(50_000);

await page.unrouteAll();

// With the fix: at most 1-2 refresh requests (one cycle at ~43s)
// Without the fix: 5+ requests from orphaned timers all firing at different offsets
expect(tokenRequests.length).toBeLessThanOrEqual(3);
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This test is unmergeable without the matching token-cache fix.

These assertions require behavior that current main does not have yet: the SessionTokenCache.set() timer cleanup is intentionally omitted in this PR. Merging this file by itself will keep CI red, so the implementation fix needs to land with the test, or this validation test should stay skipped/xfail on this branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/tests/session-token-cache/refresh-timer-cleanup.test.ts` around
lines 63 - 65, The test relies on SessionTokenCache clearing existing refresh
timers when a new token is set, but the current main omits that logic; update
the SessionTokenCache.set() implementation to cancel any existing refresh timer
for the same session/key before scheduling a new one (clearTimeout/clearInterval
on the stored timer reference), ensure the timer reference is stored on the
SessionTokenCache instance (e.g., a map of sessionId -> refreshTimer), and start
the new refresh timer only after cleaning up the previous one so orphaned timers
don't fire and the tokenRequests assertion passes; alternatively, if you
deliberately don't want the fix here, keep the test skipped/xfail in this branch
rather than merging it.


// If multiple requests occurred, verify they aren't clustered together
// (clustering = orphaned timers firing near-simultaneously)
if (tokenRequests.length >= 2) {
for (let i = 1; i < tokenRequests.length; i++) {
const gap = tokenRequests[i] - tokenRequests[i - 1];
expect(gap).toBeGreaterThan(10_000);
}
}
});
},
);
Loading