Files
iistwin/tests/task-pagination.test.ts
Ильяс Султанов a27766a38b perf(tasks): лимиты/пагинация /api/tasks и дельта-синк в SQL
Шаги 0.10 и 0.9 плана production-готовности:
- дефолт limit 50, кап 200; списковые ответы без description
- курсорная пагинация (updated_at, id) + nextCursor
- since-фильтр дельта-синка перенесён в SQL (updated_at >= since)
- embedding pre-count через COUNT(*) вместо выгрузки миллиона строк
- 11 новых юнит-тестов (56/56 зелёные)

Инвариант поиска сохранён: search-ветка /api/tasks?search= не изменена
2026-09-07 22:01:25 +03:00

86 lines
3.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
clampTasksLimit,
parseTasksCursor,
buildTasksNextCursor,
TASKS_LIST_DEFAULT_LIMIT,
TASKS_LIST_MAX_LIMIT,
} from '../server/utils/task-pagination';
describe('clampTasksLimit', () => {
it('без значения возвращает дефолт 50', () => {
expect(clampTasksLimit(undefined)).toBe(TASKS_LIST_DEFAULT_LIMIT);
expect(clampTasksLimit(null)).toBe(TASKS_LIST_DEFAULT_LIMIT);
expect(TASKS_LIST_DEFAULT_LIMIT).toBe(50);
});
it('неположительные и невалидные значения — дефолт 50', () => {
expect(clampTasksLimit(0)).toBe(50);
expect(clampTasksLimit(-5)).toBe(50);
expect(clampTasksLimit(NaN)).toBe(50);
});
it('явный лимит в пределах максимума проходит как есть', () => {
expect(clampTasksLimit(1)).toBe(1);
expect(clampTasksLimit(100)).toBe(100);
expect(clampTasksLimit(200)).toBe(200);
});
it('капает сверху на 200', () => {
expect(clampTasksLimit(201)).toBe(TASKS_LIST_MAX_LIMIT);
expect(clampTasksLimit(500)).toBe(200);
expect(clampTasksLimit(10000)).toBe(200);
expect(TASKS_LIST_MAX_LIMIT).toBe(200);
});
});
describe('parseTasksCursor', () => {
it('парсит валидный курсор <updatedAt ISO>_<id>', () => {
const cursor = parseTasksCursor('2026-09-08T12:34:56.789Z_123');
expect(cursor).not.toBeNull();
expect(cursor!.id).toBe(123);
expect(cursor!.updatedAt.toISOString()).toBe('2026-09-08T12:34:56.789Z');
});
it('null/пустая строка — null (первая страница)', () => {
expect(parseTasksCursor(null)).toBeNull();
expect(parseTasksCursor(undefined)).toBeNull();
expect(parseTasksCursor('')).toBeNull();
});
it('невалидные форматы — null', () => {
expect(parseTasksCursor('abc')).toBeNull();
expect(parseTasksCursor('_123')).toBeNull();
expect(parseTasksCursor('2026-09-08T12:34:56.789Z_')).toBeNull();
expect(parseTasksCursor('2026-09-08T12:34:56.789Z_abc')).toBeNull();
expect(parseTasksCursor('not-a-date_123')).toBeNull();
expect(parseTasksCursor('2026-09-08T12:34:56.789Z_0')).toBeNull();
expect(parseTasksCursor('2026-09-08T12:34:56.789Z_-1')).toBeNull();
expect(parseTasksCursor('2026-09-08T12:34:56.789Z_1.5')).toBeNull();
});
});
describe('buildTasksNextCursor', () => {
it('собирает курсор из последней строки выдачи', () => {
const cursor = buildTasksNextCursor({ id: 42, updatedAt: new Date('2026-09-08T10:00:00.000Z') });
expect(cursor).toBe('2026-09-08T10:00:00.000Z_42');
});
it('принимает updatedAt строкой', () => {
const cursor = buildTasksNextCursor({ id: 7, updatedAt: '2026-09-08T10:00:00.000Z' });
expect(cursor).toBe('2026-09-08T10:00:00.000Z_7');
});
it('без updatedAt — null', () => {
expect(buildTasksNextCursor({ id: 1, updatedAt: null })).toBeNull();
});
it('round-trip: build → parse возвращает ту же пару', () => {
const built = buildTasksNextCursor({ id: 555, updatedAt: new Date('2026-01-02T03:04:05.006Z') });
const parsed = parseTasksCursor(built);
expect(parsed).not.toBeNull();
expect(parsed!.id).toBe(555);
expect(parsed!.updatedAt.toISOString()).toBe('2026-01-02T03:04:05.006Z');
});
});