Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { queryClient } from '@/lib/query-client'
|
||||
|
||||
import { useAtCompletions } from './use-at-completions'
|
||||
|
||||
function gatewayStub(latencyMs = 40) {
|
||||
const calls: string[] = []
|
||||
|
||||
const gateway = {
|
||||
request: vi.fn(async (_method: string, params: { word: string }) => {
|
||||
calls.push(params.word)
|
||||
await new Promise(r => setTimeout(r, latencyMs))
|
||||
|
||||
return { items: [{ text: `@folder:${params.word.slice(1)}x/`, display: 'x/', meta: 'dir' }] }
|
||||
})
|
||||
}
|
||||
|
||||
return { calls, gateway }
|
||||
}
|
||||
|
||||
function setup(latencyMs = 40) {
|
||||
const { calls, gateway } = gatewayStub(latencyMs)
|
||||
|
||||
const { result } = renderHook(() => useAtCompletions({ gateway: gateway as never, sessionId: 's1', cwd: '/repo' }))
|
||||
|
||||
return { calls, result }
|
||||
}
|
||||
|
||||
/** Type a burst of keystrokes `gapMs` apart, like a person. */
|
||||
async function type(
|
||||
result: { current: { adapter: { search?: (q: string) => unknown } } },
|
||||
queries: string[],
|
||||
gapMs: number
|
||||
) {
|
||||
for (const q of queries) {
|
||||
act(() => {
|
||||
result.current.adapter.search?.(q)
|
||||
})
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(gapMs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('PERF: @ path completions are cached and skip the debounce', () => {
|
||||
it('serves a repeated query with no round trip and no spinner', async () => {
|
||||
vi.useFakeTimers()
|
||||
queryClient.clear()
|
||||
|
||||
const { calls, result } = setup()
|
||||
|
||||
// First visit to `apps/` pays the round trip.
|
||||
await type(result, ['apps/'], 0)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
})
|
||||
|
||||
const afterFirst = calls.length
|
||||
|
||||
expect(afterFirst).toBe(1)
|
||||
|
||||
// Walk away and come back — Tab in, Backspace out, retype. Every one of
|
||||
// these used to be a fresh git ls-files + rank on the backend.
|
||||
await type(result, ['apps/desktop/', 'apps/', 'apps/desktop/', 'apps/'], 0)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
})
|
||||
|
||||
// Two distinct paths, so exactly two round trips total — the repeats are free.
|
||||
expect(calls.length).toBe(2)
|
||||
expect(result.current.loading).toBe(false)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('a cached query paints without waiting out the debounce', async () => {
|
||||
vi.useFakeTimers()
|
||||
queryClient.clear()
|
||||
|
||||
const { calls, result } = setup()
|
||||
|
||||
await type(result, ['apps/'], 0)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
})
|
||||
|
||||
expect(calls.length).toBe(1)
|
||||
|
||||
// Re-ask for the cached query and advance by far less than the 60ms
|
||||
// debounce. A cached answer resolves in a microtask, so it must paint
|
||||
// without the timer and without ever flipping the spinner on.
|
||||
act(() => {
|
||||
result.current.adapter.search?.('apps/')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(calls.length).toBe(1)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user