82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
// The optional-module helpers run against a fake KV store: the modules' clients are
|
|
// replaced, so each test asserts what the helper asked for.
|
|
const mocks = vi.hoisted(() => {
|
|
const store = new Map<string, number>()
|
|
return {
|
|
store,
|
|
kvClient: null as null | {
|
|
incr: (key: string) => Promise<number>
|
|
expire: (key: string, seconds: number) => Promise<number>
|
|
get: <T>(key: string) => Promise<T | null>
|
|
},
|
|
expire: vi.fn(async () => 1),
|
|
}
|
|
})
|
|
|
|
vi.mock('@/shared/api', () => ({
|
|
kv: () => mocks.kvClient,
|
|
nats: () => null,
|
|
s3: () => null,
|
|
ensureCollection: vi.fn(async () => {}),
|
|
upsertPoints: vi.fn(async () => {}),
|
|
searchPoints: vi.fn(async () => []),
|
|
}))
|
|
vi.mock('@/shared/config', () => ({
|
|
getEnv: () => ({ QDRANT_URL: undefined }),
|
|
}))
|
|
|
|
import { consumeUploadQuota, embed, UPLOADS_PER_MINUTE, uploadsLeft } from '@/shared/lib/optional'
|
|
|
|
function fakeKv() {
|
|
mocks.kvClient = {
|
|
incr: async (key: string) => {
|
|
const next = (mocks.store.get(key) ?? 0) + 1
|
|
mocks.store.set(key, next)
|
|
return next
|
|
},
|
|
expire: mocks.expire,
|
|
get: async <T>(key: string) => (mocks.store.has(key) ? (mocks.store.get(key) as T) : null),
|
|
}
|
|
}
|
|
|
|
describe('consumeUploadQuota', () => {
|
|
beforeEach(() => {
|
|
mocks.store.clear()
|
|
mocks.expire.mockClear()
|
|
fakeKv()
|
|
})
|
|
|
|
it('allows the uploads of the minute up to the limit, refuses the next, and sets the window once', async () => {
|
|
let last: { allowed: boolean; remaining: number } | null = null
|
|
for (let i = 0; i < UPLOADS_PER_MINUTE; i += 1) last = await consumeUploadQuota('visitor-1')
|
|
expect(last).toEqual({ allowed: true, remaining: 0 })
|
|
|
|
expect(await consumeUploadQuota('visitor-1')).toEqual({ allowed: false, remaining: 0 })
|
|
expect(mocks.expire).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('counts each visitor separately', async () => {
|
|
await consumeUploadQuota('visitor-1')
|
|
expect(await uploadsLeft('visitor-2')).toBe(UPLOADS_PER_MINUTE)
|
|
})
|
|
|
|
it('is null, and counts nothing, when KV is not linked', async () => {
|
|
mocks.kvClient = null
|
|
expect(await consumeUploadQuota('visitor-1')).toBeNull()
|
|
expect(await uploadsLeft('visitor-1')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('embed', () => {
|
|
const dot = (x: number[], y: number[]) => x.reduce((sum, value, i) => sum + value * (y[i] ?? 0), 0)
|
|
|
|
it('is stable for the same text, and orthogonal for text with no word in common', () => {
|
|
const a = embed('plan the launch')
|
|
expect(embed('plan the launch')).toEqual(a)
|
|
expect(dot(a, a)).toBeCloseTo(1)
|
|
expect(dot(a, embed('zebra quartz'))).toBeCloseTo(0)
|
|
})
|
|
})
|