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:
2026-09-07 20:28:44 +03:00
parent 76a0d859a5
commit b3d7923b23
5 changed files with 67 additions and 22 deletions

View File

@@ -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 () => {