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:
+7
-1
@@ -3,8 +3,14 @@ package space.nstart.pcp.pcp_mission_planing_service.controller
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/** Допустимые значения [MissionCloneRequest.mode]. */
|
||||
object CloneMode {
|
||||
const val REPLACE = "replace"
|
||||
const val FORK_FORWARD = "fork-forward"
|
||||
}
|
||||
|
||||
data class MissionCloneRequest(
|
||||
/** "replace" — полный клон; "fork-forward" — только включения, перекрывающиеся с окном. */
|
||||
/** [CloneMode.REPLACE] — полный клон; [CloneMode.FORK_FORWARD] — только включения, перекрывающиеся с окном. */
|
||||
val mode: String,
|
||||
/** Начало окна нового плана (UTC naive). Обязательно для fork-forward. */
|
||||
val windowStart: LocalDateTime? = null,
|
||||
|
||||
+12
-1
@@ -29,6 +29,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyModeResponseDT
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteAnchor
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildResponseDTO
|
||||
import space.nstart.pcp.pcp_mission_planing_service.controller.CloneMode
|
||||
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneRequest
|
||||
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneResponse
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeEntity
|
||||
@@ -135,12 +136,22 @@ class MissionPlaningService(
|
||||
* пересекается с окном `[windowStart, windowStop)`. DropMode клонируется только если хотя бы одна
|
||||
* из его связанных съёмок тоже попала в клон (иначе сброс становится пустым).
|
||||
*
|
||||
* Намеренные особенности fork-forward:
|
||||
* - Времена включений переносятся как есть. Включение, начавшееся до `windowStart` и закончившееся
|
||||
* внутри окна, клонируется со своим исходным `timeStart` — он может быть раньше `missionStart`
|
||||
* клона. Границы миссии описывают слот целевой ЗРВ, а не оболочку времён её включений.
|
||||
* - Съёмка переносится, даже если её сброс не попал в окно: сброс будет перевыдан в новом плане.
|
||||
* Обратное неверно — сброс без единой перенесённой съёмки отбрасывается.
|
||||
*
|
||||
* Новый `missionId` передаётся в ответе; он используется как `planId` черновика в pcp-tgu-service
|
||||
* (инвариант planId == missionId сохраняется).
|
||||
*/
|
||||
@Transactional
|
||||
fun cloneMission(sourceMissionId: UUID, req: MissionCloneRequest): MissionCloneResponse {
|
||||
val isForkForward = req.mode == "fork-forward"
|
||||
if (req.mode != CloneMode.REPLACE && req.mode != CloneMode.FORK_FORWARD) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown clone mode: ${req.mode}")
|
||||
}
|
||||
val isForkForward = req.mode == CloneMode.FORK_FORWARD
|
||||
if (isForkForward && (req.windowStart == null || req.windowStop == null)) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
|
||||
+22
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user