Починить активацию маршрута при отправке со статусом «Выполнение»
Tests / frontend-test (pull_request) Successful in 1m14s

Регрессия из 0cf8359: initialSubmitStatus стала возвращать «Выполнение»
для маршрута без согласующих, но writeRoute определяла факт отправки
строгим сравнением `status === onApproval`. В результате для такой заявки:

  - строки маршрута писались со статусом «Выполнение» — недопустимым для
    строки значением (его нет в APPROVAL_ROW_STATUS_CHOICES) вместо
    «Ждёт очереди»;
  - activateNextQueued не вызывался вовсе, поэтому ни одна строка не
    получала «На исполнении».

Очередь исполнителя фильтрует строки ровно по «На исполнении», поэтому
отправленные заявки в неё не попадали.

Заменено на предикат isSubmittedStatus, который покрывает все статусы,
возвращаемые initialSubmitStatus. Тест связывает эти две функции
напрямую, чтобы добавление нового статуса отправки не могло снова
разойтись с writeRoute молча.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-08-21 17:23:19 +05:00
parent 69ab08825f
commit 8b09243c2e
4 changed files with 67 additions and 2 deletions
@@ -3,7 +3,7 @@ import { resolveRoute } from './approvalRoute.service';
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
import { IMatrixRow } from './models/IMatrixRow';
import { ApprovalStatus, IResolveFieldValue } from './models/IApprovalRouteApi';
import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
import { APPROVAL_PHASE, REQUEST_STATUS, isSubmittedStatus } from './models/statuses';
// Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы.
// status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution'
@@ -82,7 +82,11 @@ export async function writeRoute(
// При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый —
// так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки
// (Черновик), без активации.
const submitted = status === REQUEST_STATUS.onApproval;
//
// Именно предикат, а не `status === onApproval`: отправка маршрута без
// согласующих даёт «Выполнение» (см. initialSubmitStatus), и при строгом
// сравнении такая заявка не активировала ни одного шага исполнения.
const submitted = isSubmittedStatus(status);
const rowStatus = submitted ? REQUEST_STATUS.queued : status;
// Смещение фазы исполнения — по МАКСИМАЛЬНОМУ номеру шага согласующих, а не по
@@ -8,6 +8,7 @@ export {
EDITABLE_STATUSES,
isEditableStatus,
initialSubmitStatus,
isSubmittedStatus,
} from './models/statuses';
export type { ISiteUser } from './models/ISiteUser';
export type { IMatrixRow } from './models/IMatrixRow';
@@ -5,7 +5,9 @@ import {
EDITABLE_STATUSES,
REQUEST_STATUS,
REQUEST_STATUS_CHOICES,
initialSubmitStatus,
isEditableStatus,
isSubmittedStatus,
} from './statuses';
describe('isEditableStatus', () => {
@@ -32,6 +34,43 @@ describe('isEditableStatus', () => {
});
});
describe('initialSubmitStatus', () => {
it('есть согласующие — заявка идёт на согласование', () => {
expect(initialSubmitStatus(2, 0)).toBe(REQUEST_STATUS.onApproval);
expect(initialSubmitStatus(2, 3)).toBe(REQUEST_STATUS.onApproval);
});
it('согласующих нет, но есть исполнители — сразу в исполнение', () => {
expect(initialSubmitStatus(0, 1)).toBe(REQUEST_STATUS.execution);
});
it('маршрута нет вовсе — заявка сразу согласована', () => {
expect(initialSubmitStatus(0, 0)).toBe(REQUEST_STATUS.approved);
});
});
// writeRoute по этому предикату решает, писать строки как «Ждёт очереди» и
// активировать первую, или оставить как есть (черновик). Каждый статус, который
// возвращает initialSubmitStatus, ОБЯЗАН считаться отправкой: иначе строки
// маршрута получают недопустимый статус, а активации не происходит вовсе —
// именно так заявка без согласующих перестала попадать в очередь исполнения.
describe('isSubmittedStatus', () => {
it('любой результат initialSubmitStatus считается отправкой', () => {
[initialSubmitStatus(1, 0), initialSubmitStatus(0, 1), initialSubmitStatus(0, 0)].forEach((s) =>
expect(isSubmittedStatus(s)).toBe(true)
);
});
it('черновик отправкой не считается', () => {
expect(isSubmittedStatus(REQUEST_STATUS.draft)).toBe(false);
});
it('возврат и отклонение отправкой не считаются (маршрут не переактивируется)', () => {
expect(isSubmittedStatus(REQUEST_STATUS.returned)).toBe(false);
expect(isSubmittedStatus(REQUEST_STATUS.rejected)).toBe(false);
});
});
// Наборы Choice-значений держатся в синхроне с provisioning.json списков —
// расхождение ломает запись (SharePoint отвергает значение вне Choice).
describe('наборы Choice-значений', () => {
@@ -61,6 +61,27 @@ export function initialSubmitStatus(approversCount: number, executorsCount: numb
return REQUEST_STATUS.approved;
}
/**
* Статус заявки означает «отправлена» (в отличие от черновика)?
*
* Нужен writeRoute: он по этому признаку решает, писать строки маршрута как
* «Ждёт очереди» + активировать первую, или сохранить их «как есть» (черновик).
* Раньше там стояло строгое `status === onApproval` — единственный статус,
* которым формы отправляли заявку. С появлением initialSubmitStatus отправка
* может дать «Выполнение» (маршрут без согласующих), и строгое сравнение молча
* ломалось: строки получали ApprovalStatus «Выполнение» (недопустимое значение
* для строки, см. APPROVAL_ROW_STATUS_CHOICES), а activateNextQueued не
* вызывался — ни одна строка не становилась «На исполнении», и заявка не
* попадала в очередь исполнителя.
*/
export function isSubmittedStatus(status: string): boolean {
return (
status === REQUEST_STATUS.onApproval ||
status === REQUEST_STATUS.execution ||
status === REQUEST_STATUS.approved
);
}
// Наборов значений ДВА, и они не совпадают — это разные сущности:
//
// 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется