Шаги 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= не изменена
86 lines
3.4 KiB
TypeScript
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');
|
|
});
|
|
});
|