Files
dc-observatio/services/pcp-tgu-ops-ui/src/features/tgu-planning/tguTimelineMapper.test.ts
T
Дмитрий Соловьев 5049395911 pcp-tgu: пауза цепочки после REJECTED + режим «Черновики» + UI цепочек/ЗРВ
Бэкенд (pcp-tgu-service):
- REJECTED трактуется как «план неправильный»: авто-перевыдача больше НЕ
  взводится, цепочка встаёт на паузу (новый флаг
  SpacecraftPlanningStateEntity.pausedAfterReject, миграция V13). Технические
  негативы («не заложен»/«не принят») по-прежнему дают срочную перевыдачу.
- Пауза централизована в PlanProjectionService.project(): для отклонённой
  цепочки проекция пустая, воркер выдачи ничего не материализует. Флаг
  снимается при ручной выдаче/промоуте черновика/приёмке (оператор вмешался).
- Режим «Черновики»: GET /plans/drafts (опц. ?spacecraftId) -> только DRAFT.
- Гард дублей черновиков: (КА + начало + конец) уникальны среди DRAFT,
  повторное создание окна -> 409.
- ZrvWindowMatcher: сравнение по истинной abs-длительности Duration (<=1с),
  устойчиво к усечению ISO на фронте до целых секунд.

Фронт (pcp-tgu-ops-ui): вкладка «Планы» — таймлайн цепочек, детали плана,
панель ЗРВ (TguZrvPanel), диалог создания цепочки, режим черновиков.

Удалён устаревший docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md.
2026-06-04 00:04:47 +03:00

80 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TguPlatform, TguPlanStatus } from "../../model/tguTypes";
import type { TguPlanUi } from "../../model/timelineTypes";
import { isOperationalPlatform, operationalSpacecraftIds, pausedSpacecraftIds } from "./tguTimelineMapper";
function platform(overrides: Partial<TguPlatform>): TguPlatform {
return { id: "x", noradId: 1, status: "OPERATIONAL", ...overrides };
}
function uiPlan(spacecraftId: string, status: TguPlanStatus, startMs: number): TguPlanUi {
return {
planId: `${spacecraftId}-${status}-${startMs}`,
spacecraftId,
startTime: new Date(startMs).toISOString().slice(0, 19),
endTime: new Date(startMs + 3_600_000).toISOString().slice(0, 19),
kppId: "KPP-1",
status,
startMs,
endMs: startMs + 3_600_000
};
}
describe("isOperationalPlatform", () => {
it("принимает только статус OPERATIONAL без учёта регистра и пробелов", () => {
expect(isOperationalPlatform(platform({ status: "OPERATIONAL" }))).toBe(true);
expect(isOperationalPlatform(platform({ status: " operational " }))).toBe(true);
expect(isOperationalPlatform(platform({ status: "STANDBY" }))).toBe(false);
expect(isOperationalPlatform(platform({ status: "LOST" }))).toBe(false);
expect(isOperationalPlatform(platform({ status: null }))).toBe(false);
expect(isOperationalPlatform(platform({ status: undefined }))).toBe(false);
});
});
describe("operationalSpacecraftIds", () => {
it("собирает norad-id в виде строк и пропускает платформы без norad-id", () => {
const ids = operationalSpacecraftIds([
platform({ noradId: 2 }),
platform({ noradId: 30 }),
platform({ noradId: null }),
platform({ noradId: undefined })
]);
expect(ids).toEqual(new Set(["2", "30"]));
});
});
describe("pausedSpacecraftIds", () => {
const t0 = Date.UTC(2026, 5, 4, 0, 0, 0);
const hour = 3_600_000;
it("помечает КА паузой, если есть REJECTED и нет продвинувшего цепочку плана после него", () => {
const paused = pausedSpacecraftIds([
uiPlan("100", "REJECTED", t0),
// проекция и черновик паузу не снимают
uiPlan("100", "PLANNED", t0 + 2 * hour),
uiPlan("100", "DRAFT", t0)
]);
expect(paused).toEqual(new Set(["100"]));
});
it("снимает паузу, если оператор отправил исправленный план (в полёте) на том же/более позднем окне", () => {
const paused = pausedSpacecraftIds([
uiPlan("100", "REJECTED", t0),
uiPlan("100", "WAITING_DECISION", t0)
]);
expect(paused.has("100")).toBe(false);
});
it("не помечает паузой, если цепочка ушла вперёд (ACCEPTED после отказа)", () => {
const paused = pausedSpacecraftIds([
uiPlan("100", "REJECTED", t0),
uiPlan("100", "ACCEPTED", t0 + 5 * hour)
]);
expect(paused.has("100")).toBe(false);
});
it("без REJECTED никого не помечает", () => {
expect(pausedSpacecraftIds([uiPlan("100", "WAITING_DECISION", t0)])).toEqual(new Set());
});
});