Skip to content

Commit 7c93bbe

Browse files
committed
perf(tests): drive retry backoff and scheduled flushes with fake timers
Real sleeps and repeated setup were the run-phase cost in seven files: - tools/index.test.ts: `retryDelayMs: 0` falls through to the 500ms default, so every retry test slept for real. Fake the clock for the retry describe. - embeddings/client.test.ts: two retry tests waited the 1s initial backoff. - hosted-key-rate-limiter.test.ts: the queue-head test slept two 200ms polls; the abort test slept 20ms wall clock and asserted on Date.now. - event-buffer.test.ts: sleep(60)/sleep(5) waited on the 15ms flush timer. - events.attribution.test.ts: walked and read the whole app twice, serially. Walk once in a beforeAll with batched concurrent reads. - sso-trust.test.ts: imported the Better Auth graph in each of three tests. Import once in a beforeAll that carries the 30s budget. - markdown-parse.test.ts: the one-shot helper built and destroyed a tiptap Editor per document; reuse one across the 400-seed property corpus. No production timing constants, corpus sizes, or assertions changed.
1 parent a38c0bf commit 7c93bbe

7 files changed

Lines changed: 163 additions & 88 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-parse.test.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment jsdom
33
*/
44
import { Editor } from '@tiptap/core'
5-
import { afterEach, describe, expect, it } from 'vitest'
5+
import { afterAll, describe, expect, it } from 'vitest'
66
import { createMarkdownContentExtensions } from './extensions'
77
import { parseMarkdownToDoc, serializeMarkdownBody, splitMarkdownBlocks } from './markdown-parse'
88
import { isRoundTripSafe } from './round-trip-safety'
@@ -12,19 +12,20 @@ const isEmptyPara = (n: { type?: string; content?: unknown[] }): boolean =>
1212
n.type === 'paragraph' && !n.content?.length
1313

1414
let editor: Editor | null = null
15-
afterEach(() => {
15+
afterAll(() => {
1616
editor?.destroy()
1717
editor = null
1818
})
1919

20-
/** The current whole-document path: parse markdown in one shot, serialize back. */
20+
/**
21+
* The current whole-document path: parse markdown in one shot, serialize back. One editor serves
22+
* every call — `setContent` replaces the document wholesale, so a fresh instance per call only adds
23+
* the cost of building the view, which the property tests below paid hundreds of times over.
24+
*/
2125
function oneShot(body: string): string {
22-
editor = new Editor({ extensions: createMarkdownContentExtensions() })
26+
editor ??= new Editor({ extensions: createMarkdownContentExtensions() })
2327
editor.commands.setContent(body, { contentType: 'markdown' })
24-
const out = editor.getMarkdown()
25-
editor.destroy()
26-
editor = null
27-
return out
28+
return editor.getMarkdown()
2829
}
2930

3031
/**

‎apps/sim/lib/auth/sso-trust.test.ts‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
* domain-verification proof entirely.
99
*/
1010
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
11-
import { afterAll, expect, it, vi } from 'vitest'
12-
13-
// Structurally slow — it imports the entire Better Auth module graph — so under a fully-parallel local run this file
14-
// blows the default timeout while passing in isolation and on CI. Give it a
15-
// real budget instead of letting machine load decide the verdict.
16-
vi.setConfig({ testTimeout: 30_000 })
11+
import { afterAll, beforeAll, expect, it, vi } from 'vitest'
1712

1813
const { ssoOptions } = vi.hoisted(() => ({
1914
ssoOptions: { current: undefined as Record<string, unknown> | undefined },
@@ -28,24 +23,30 @@ vi.mock('@better-auth/sso', () => ({
2823

2924
setEnvFlags({ isSsoEnabled: true })
3025

31-
afterAll(resetEnvFlagsMock)
32-
33-
it('never trusts the IdP-supplied email_verified claim for SSO linking', async () => {
26+
/**
27+
* Structurally slow — it imports the entire Better Auth module graph — so under
28+
* a fully-parallel local run this import blows the default budget while passing
29+
* in isolation and on CI. The plugin options are captured once at module
30+
* evaluation, so every assertion reads the same object: import once, outside
31+
* any per-test budget, with a real budget of its own instead of letting machine
32+
* load decide the verdict.
33+
*/
34+
beforeAll(async () => {
3435
await import('@/lib/auth/auth')
36+
}, 30_000)
3537

38+
afterAll(resetEnvFlagsMock)
39+
40+
it('never trusts the IdP-supplied email_verified claim for SSO linking', () => {
3641
expect(ssoOptions.current).toBeDefined()
3742
expect(ssoOptions.current?.trustEmailVerified).toBe(false)
3843
})
3944

40-
it('keeps domain verification as the sole SSO linking trust source', async () => {
41-
await import('@/lib/auth/auth')
42-
45+
it('keeps domain verification as the sole SSO linking trust source', () => {
4346
expect(ssoOptions.current?.domainVerification).toEqual({ enabled: true })
4447
})
4548

46-
it('disables Better Auth membership writes so Sim owns JIT admission', async () => {
47-
await import('@/lib/auth/auth')
48-
49+
it('disables Better Auth membership writes so Sim owns JIT admission', () => {
4950
expect(ssoOptions.current?.organizationProvisioning).toEqual({
5051
disabled: true,
5152
defaultRole: 'member',

‎apps/sim/lib/core/rate-limiter/hosted-key/hosted-key-rate-limiter.test.ts‎

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { sleep } from '@sim/utils/helpers'
21
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
32
import type {
43
ConsumeResult,
@@ -314,17 +313,25 @@ describe('HostedKeyRateLimiter', () => {
314313
.mockResolvedValueOnce('waiting')
315314
.mockResolvedValueOnce('head')
316315

317-
const result = await rateLimiter.acquireKey(
318-
testProvider,
319-
envKeyPrefix,
320-
perRequestRateLimit,
321-
'workspace-1'
322-
)
316+
// Each "waiting" answer sleeps one real poll period; drive those with fake timers.
317+
vi.useFakeTimers()
318+
try {
319+
const pending = rateLimiter.acquireKey(
320+
testProvider,
321+
envKeyPrefix,
322+
perRequestRateLimit,
323+
'workspace-1'
324+
)
325+
await vi.runAllTimersAsync()
326+
const result = await pending
323327

324-
expect(result.success).toBe(true)
325-
expect(mockQueue.checkHead).toHaveBeenCalledTimes(3)
326-
// Bucket is only consumed once we reach the head.
327-
expect(mockAdapter.consumeTokens).toHaveBeenCalledTimes(1)
328+
expect(result.success).toBe(true)
329+
expect(mockQueue.checkHead).toHaveBeenCalledTimes(3)
330+
// Bucket is only consumed once we reach the head.
331+
expect(mockAdapter.consumeTokens).toHaveBeenCalledTimes(1)
332+
} finally {
333+
vi.useRealTimers()
334+
}
328335
})
329336

330337
it('refreshes the heartbeat while waiting at the head of the queue', async () => {
@@ -428,24 +435,31 @@ describe('HostedKeyRateLimiter', () => {
428435
}
429436
mockAdapter.consumeTokens.mockResolvedValue(blocked)
430437

431-
const controller = new AbortController()
432-
const start = Date.now()
433-
const promise = rateLimiter.acquireKey(
434-
testProvider,
435-
envKeyPrefix,
436-
perRequestRateLimit,
437-
'workspace-1',
438-
controller.signal
439-
)
440-
// Let the first bucket check run and the sleep begin, then abort.
441-
await sleep(20)
442-
controller.abort()
443-
const result = await promise
438+
vi.useFakeTimers()
439+
try {
440+
const controller = new AbortController()
441+
const start = Date.now()
442+
const promise = rateLimiter.acquireKey(
443+
testProvider,
444+
envKeyPrefix,
445+
perRequestRateLimit,
446+
'workspace-1',
447+
controller.signal
448+
)
449+
// Let the first bucket check run and the sleep begin, then abort. No timer
450+
// advances after the abort, so the wait can only settle by waking on it —
451+
// a sleep that ran to its cap would leave the promise pending.
452+
await vi.advanceTimersByTimeAsync(20)
453+
controller.abort()
454+
const result = await promise
444455

445-
expect(result.success).toBe(false)
446-
expect(result.billingActorRateLimited).toBe(true)
447-
// Resolved well before the 10s capped sleep would otherwise have elapsed.
448-
expect(Date.now() - start).toBeLessThan(2000)
456+
expect(result.success).toBe(false)
457+
expect(result.billingActorRateLimited).toBe(true)
458+
// Resolved well before the 10s capped sleep would otherwise have elapsed.
459+
expect(Date.now() - start).toBeLessThan(HEARTBEAT_REFRESH_INTERVAL_MS)
460+
} finally {
461+
vi.useRealTimers()
462+
}
449463
})
450464

451465
it('keeps waiting past the no-signal fallback cap while the signal is live', async () => {

‎apps/sim/lib/embeddings/client.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,17 @@ describe('embed', () => {
459459
})
460460

461461
it('retries a rate-limited request and succeeds on a later attempt', async () => {
462+
vi.useFakeTimers()
462463
fetchMock
463464
.mockResolvedValueOnce(jsonResponse({ error: 'slow down' }, 429))
464465
.mockResolvedValueOnce(jsonResponse(openAIBody([[7, 8]])))
465466

466-
const result = await embed(['hello'], {
467+
const pending = embed(['hello'], {
467468
model: 'text-embedding-3-small',
468469
apiKey: 'sk-test',
469470
})
471+
await vi.runAllTimersAsync()
472+
const result = await pending
470473

471474
expect(fetchMock).toHaveBeenCalledTimes(2)
472475
expect(result.embeddings[0].slice(0, 2)).toEqual([7, 8])
@@ -620,16 +623,19 @@ describe('embed', () => {
620623
})
621624

622625
it('projects once even when the request is retried', async () => {
626+
vi.useFakeTimers()
623627
const projectInputs = vi.fn((values: readonly string[]) => values.map(() => 'projected'))
624628
fetchMock
625629
.mockResolvedValueOnce(jsonResponse({ error: 'rate limited' }, 429))
626630
.mockResolvedValueOnce(jsonResponse(openAIBody([[1]])))
627631

628-
await embed(['secret'], {
632+
const pending = embed(['secret'], {
629633
model: 'text-embedding-3-small',
630634
apiKey: 'sk-test',
631635
projectInputs,
632636
})
637+
await vi.runAllTimersAsync()
638+
await pending
633639

634640
expect(fetchMock).toHaveBeenCalledTimes(2)
635641
expect(projectInputs).toHaveBeenCalledTimes(1)

‎apps/sim/lib/execution/event-buffer.test.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { redisConfigMockFns, resetEnvMock, resetRedisConfigMock, setEnv } from '@sim/testing'
5-
import { sleep } from '@sim/utils/helpers'
6-
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
76
import type { ExecutionEventEntry } from '@/lib/execution/event-buffer'
87
import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache'
98
import { LARGE_VALUE_REF_MARKER } from '@/lib/execution/payloads/large-value-ref'
@@ -51,6 +50,10 @@ afterAll(() => {
5150
resetRedisConfigMock()
5251
})
5352

53+
afterEach(() => {
54+
vi.useRealTimers()
55+
})
56+
5457
import {
5558
createExecutionEventWriter,
5659
flushExecutionStreamReplayBuffer,
@@ -654,15 +657,18 @@ describe('execution event buffer', () => {
654657
})
655658
})
656659

660+
vi.useFakeTimers()
657661
const writer = createExecutionEventWriter('exec-1')
658662
await writer.write(makeEvent('first'))
663+
// The write only arms the flush timer; fire it so the flush is in flight.
664+
await vi.runOnlyPendingTimersAsync()
659665
await firstFlushStarted
660666

661667
const terminalWrite = writer.writeTerminal(makeEvent('terminal'), 'complete')
662668
// Let writeTerminal's queued body actually enqueue its entry before the
663669
// in-flight flush resolves — otherwise the scheduled loop finds nothing left
664670
// to drain and the race under test never forms.
665-
await sleep(5)
671+
await vi.advanceTimersByTimeAsync(5)
666672
releaseFirstFlush?.()
667673
await terminalWrite
668674

@@ -776,10 +782,12 @@ describe('execution event buffer', () => {
776782
return [1, 'ok', 0, 0]
777783
})
778784

785+
vi.useFakeTimers()
779786
const writer = createExecutionEventWriter('exec-1')
780787
await writer.write(makeEvent('a'))
781788

782-
await sleep(60)
789+
// Fire the scheduled flush (and any backoff it arms) before the caller's own.
790+
await vi.runAllTimersAsync()
783791

784792
await expect(writer.flush()).resolves.toBeUndefined()
785793
})

‎apps/sim/lib/table/events.attribution.test.ts‎

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
*/
44
import { readdir, readFile } from 'node:fs/promises'
55
import { join } from 'node:path'
6-
import { describe, expect, it, vi } from 'vitest'
7-
8-
// Structurally slow — it scans call sites across the repo — so under a fully-parallel local run this file
9-
// blows the default timeout while passing in isolation and on CI. Give it a
10-
// real budget instead of letting machine load decide the verdict.
11-
vi.setConfig({ testTimeout: 30_000 })
6+
import { beforeAll, describe, expect, it } from 'vitest'
127

138
/**
149
* `signalTableRowsChangedByActor` lets the acting tab skip its own refetch, which is only sound
@@ -51,42 +46,76 @@ const FORWARDING_MODULE = 'lib/table/application/rows.ts'
5146
*/
5247
const SUPPLIER_PATTERNS = [/actorClientId:/, /signalTableRowsChangedByActor\([^)]*,/] as const
5348

54-
async function* walk(dir: string): AsyncGenerator<string> {
55-
for (const entry of await readdir(dir, { withFileTypes: true })) {
56-
if (entry.name === 'node_modules' || entry.name === '.next') continue
57-
const full = join(dir, entry.name)
58-
if (entry.isDirectory()) yield* walk(full)
59-
else if (entry.name.endsWith('.ts') && !entry.name.includes('.test.')) yield full
49+
/** Files read per batch; bounds open descriptors while keeping the disk busy. */
50+
const READ_BATCH_SIZE = 64
51+
52+
async function walk(dir: string): Promise<string[]> {
53+
const entries = await readdir(dir, { withFileTypes: true })
54+
const nested = await Promise.all(
55+
entries.map(async (entry) => {
56+
if (entry.name === 'node_modules' || entry.name === '.next') return []
57+
const full = join(dir, entry.name)
58+
if (entry.isDirectory()) return walk(full)
59+
return entry.name.endsWith('.ts') && !entry.name.includes('.test.') ? [full] : []
60+
})
61+
)
62+
return nested.flat()
63+
}
64+
65+
/**
66+
* Every source file under the app root, keyed by its relative path. Read once
67+
* for the file: both sweeps scan the same tree, and walking it per test was the
68+
* whole cost of this file.
69+
*/
70+
let sources: Map<string, string>
71+
72+
async function readSources(): Promise<Map<string, string>> {
73+
const files = await walk(APP_ROOT)
74+
const found = new Map<string, string>()
75+
for (let start = 0; start < files.length; start += READ_BATCH_SIZE) {
76+
const batch = files.slice(start, start + READ_BATCH_SIZE)
77+
const contents = await Promise.all(batch.map((file) => readFile(file, 'utf8')))
78+
batch.forEach((file, index) => {
79+
found.set(file.slice(APP_ROOT.length + 1), contents[index])
80+
})
6081
}
82+
return found
6183
}
6284

63-
async function filesMatching(
85+
function filesMatching(
6486
matches: (source: string) => boolean,
6587
skip: (relative: string) => boolean = () => false
66-
) {
88+
): string[] {
6789
const found: string[] = []
68-
for await (const file of walk(APP_ROOT)) {
69-
const source = await readFile(file, 'utf8')
90+
for (const [relative, source] of sources) {
7091
if (!matches(source)) continue
71-
const relative = file.slice(APP_ROOT.length + 1)
7292
if (skip(relative)) continue
7393
found.push(relative)
7494
}
7595
return found.sort()
7696
}
7797

7898
describe('signalTableRowsChangedByActor call sites', () => {
79-
it('is called only where the acting tab reconciles the write locally', async () => {
80-
const callers = await filesMatching(
99+
/**
100+
* Structurally slow — it reads every source file in the app — so under a
101+
* fully-parallel local run the scan blows the default budget while passing in
102+
* isolation and on CI. Give it a real budget of its own, outside any test's.
103+
*/
104+
beforeAll(async () => {
105+
sources = await readSources()
106+
}, 30_000)
107+
108+
it('is called only where the acting tab reconciles the write locally', () => {
109+
const callers = filesMatching(
81110
(source) => source.includes('signalTableRowsChangedByActor('),
82111
(relative) => relative === DECLARING_MODULE
83112
)
84113

85114
expect(callers).toEqual([...ATTRIBUTED_CALL_SITES].sort())
86115
})
87116

88-
it('is given an actor only by surfaces whose client hook reconciles locally', async () => {
89-
const suppliers = await filesMatching(
117+
it('is given an actor only by surfaces whose client hook reconciles locally', () => {
118+
const suppliers = filesMatching(
90119
(source) => SUPPLIER_PATTERNS.some((pattern) => pattern.test(source)),
91120
(relative) => relative === DECLARING_MODULE || relative === FORWARDING_MODULE
92121
)

0 commit comments

Comments
 (0)