test(ci): скрипт test, тесты в pr-check, фикс sporadic 403 в trackUserActivity
Шаг A3 (из 1.6) плана production-готовности: - package.json: скрипт "test": "vitest run" - pr-check.yml: шаг Tests после type check - server/utils/userActivity.ts: try/catch в trackUserActivity — синхронная ошибка трекинга активности больше не срывает authenticateToken (ложный 403 на первом запросе пользователя за минуту) - tests/auto-transitions.test.ts: 3 устаревших теста обновлены под намеренное поведение (forward-only position, синк assignee, аудит вместо системного сообщения) 45/45 тестов зелёные, npm run check чисто.
This commit is contained in:
@@ -15,7 +15,6 @@ const mockStorage = vi.hoisted(() => ({
|
||||
updateTask: vi.fn(),
|
||||
addTaskAssignee: vi.fn().mockResolvedValue(undefined),
|
||||
addTaskAuditLog: vi.fn().mockResolvedValue(undefined),
|
||||
createTaskMessage: vi.fn().mockResolvedValue(undefined),
|
||||
getForm: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
@@ -58,6 +57,11 @@ vi.mock('../server/routes/task-helpers', () => ({
|
||||
indexTaskAsync: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../server/services/delegation.service', () => ({
|
||||
ensureReviewAssignees: vi.fn().mockResolvedValue(undefined),
|
||||
cleanupStaleDelegates: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { evaluateAutoTransitions } from '../server/utils/auto-transitions';
|
||||
@@ -236,9 +240,10 @@ describe('evaluateAutoTransitions', () => {
|
||||
const result = await evaluateAutoTransitions(1, 1);
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
// 10→20→30, then 30→20 but 20 already visited → stop
|
||||
// 10→20→30, дальше цепочка обрывается: автопереходы двигают задачу
|
||||
// только ВПЕРЁД по position (auto-transitions.ts), поэтому отката 30→20 нет
|
||||
expect(result.visitedStatusIds).toEqual([10, 20, 30]);
|
||||
expect(mockStorage.updateTask.mock.calls.length).toBe(3);
|
||||
expect(mockStorage.updateTask.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should stop at max depth to prevent infinite loops', async () => {
|
||||
@@ -261,7 +266,7 @@ describe('evaluateAutoTransitions', () => {
|
||||
const result = await evaluateAutoTransitions(1, 1, { maxDepth: 3 });
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
// Should stop after maxDepth transitions (3 updates: A→B→A→B, but maxDepth=3 means 3 iterations)
|
||||
// Правило «только вперёд» останавливает цепочку на B (10→20), maxDepth не достигается
|
||||
expect(mockStorage.updateTask.mock.calls.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
@@ -325,6 +330,9 @@ describe('evaluateAutoTransitions', () => {
|
||||
mockStorage.getTaskFieldValues.mockResolvedValue([
|
||||
{ fieldId: 4, value: 'yes' },
|
||||
]);
|
||||
// Явно пустой список переходов: правил назначения нет, поэтому
|
||||
// triggeredBy не должен попадать в исполнители задачи
|
||||
mockStorage.getStatusTransitions.mockResolvedValue([]);
|
||||
mockStorage.updateTask.mockResolvedValue(makeTask({ currentStatusId: 20 }));
|
||||
|
||||
await evaluateAutoTransitions(1, 1, { triggeredBy: 1 });
|
||||
@@ -332,8 +340,10 @@ describe('evaluateAutoTransitions', () => {
|
||||
expect(mockStorage.addTaskAssignee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should keep authorId=0 in system message for auto-transition', async () => {
|
||||
mockStorage.getTask.mockResolvedValue(makeTask({ currentStatusId: 10 }));
|
||||
it('should sync resolved assignee into task_assignees on auto-transition', async () => {
|
||||
// Намеренное поведение (auto-transitions.ts): основной исполнитель
|
||||
// синкается в task_assignees, чтобы TaskDetail мог его отобразить
|
||||
mockStorage.getTask.mockResolvedValue(makeTask({ currentStatusId: 10, assignedTo: null }));
|
||||
mockStorage.getFormStatuses.mockResolvedValue([
|
||||
makeStatus({ id: 10, name: 'Initial', position: 0 }),
|
||||
makeStatus({ id: 20, name: 'Auto', position: 1, entryConditions: [{ fieldCode: 'field_4', value: 'yes' }] }),
|
||||
@@ -344,14 +354,14 @@ describe('evaluateAutoTransitions', () => {
|
||||
mockStorage.getTaskFieldValues.mockResolvedValue([
|
||||
{ fieldId: 4, value: 'yes' },
|
||||
]);
|
||||
mockStorage.updateTask.mockResolvedValue(makeTask({ currentStatusId: 20 }));
|
||||
mockStorage.getStatusTransitions.mockResolvedValue([
|
||||
{ id: 1, fromStatusId: 10, toStatusId: 20, assigneeUserId: 42 },
|
||||
]);
|
||||
mockStorage.updateTask.mockResolvedValue(makeTask({ currentStatusId: 20, assignedTo: 42 }));
|
||||
|
||||
await evaluateAutoTransitions(1, 1, { triggeredBy: 1 });
|
||||
|
||||
expect(mockStorage.createTaskMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ authorId: 0 }),
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockStorage.addTaskAssignee).toHaveBeenCalledWith(1, 42, 1);
|
||||
});
|
||||
|
||||
it('should use AND logic for entryConditions and not bounce back', async () => {
|
||||
|
||||
Reference in New Issue
Block a user