pcp: Ф2 — доработки по ревью (валидация ошибок клона, идемпотентность DRAFT, тесты)

- MissionPlaningClient: маппинг ошибок mission-planing (4xx как есть, 5xx → 502) + таймаут 30с
- CreateDraftUseCase: гард идемпотентности — второй открытый DRAFT на тот же план → 409
- cloneMission: явная валидация mode (неизвестный → 400 вместо тихого полного клона)
- константы DraftMode/CloneMode вместо строковых литералов
- kdoc: семантика fork-forward (границы-слот vs времена включений; съёмка без сброса)
- тесты: CreateDraftUseCaseTest (13 кейсов), MissionCloneTest (+съёмка без сброса)
This commit is contained in:
Дмитрий Соловьев
2026-06-03 13:09:52 +03:00
parent 0d69617420
commit 85ce238f52
8 changed files with 347 additions and 11 deletions
@@ -217,6 +217,28 @@ class MissionCloneTest {
assertTrue(clonedDrops.isEmpty())
}
@Test
fun `fork-forward keeps a survey even when its drop falls outside the window`() {
// Съёмка внутри окна [t(1), t(2)), но её сброс — до окна. Сброс не клонируется,
// съёмка остаётся (будет пересброшена в новом плане) — намеренная семантика fork-forward.
val source = MissionEntity(satelliteId = 100)
missionStore[source.missionId] = source
val plan = PlanEntity(id = ++planAutoId, mission = source, planNumber = 1)
planStore[plan.id!!] = plan
val inSurvey = survey(plan, start = t(1).plusMinutes(10), duration = 600.0)
val outDrop = drop(plan, start = t(0))
addDropLink(outDrop, inSurvey)
val result = service.cloneMission(
source.missionId,
MissionCloneRequest(mode = "fork-forward", windowStart = t(1), windowStop = t(2))
)
val clonedSurveys = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
val clonedDrops = dropStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
assertEquals(1, clonedSurveys.size)
assertTrue(clonedDrops.isEmpty())
}
@Test
fun `fork-forward requires windowStart and windowStop`() {
val (missionId) = buildSourceMission(t(0), t(1))