Запрет вложенных планов: строго монотонная цепочка в pcp-tgu-service
Цепочка планов теперь строит соседние планы со строго возрастающими стартом и концом, поэтому ни один план не может лежать целиком внутри другого. Нахлёст «на одну ЗРВ» считается по времени, а не по плоскому индексу кандидата — это убирает вложенность при нескольких станциях, видящих один пролёт одновременно (кейс Emissio). Подчистка устаревших PLANNED-планов при rebuild переведена на условие end_time > now, чтобы гасить и уже стартовавшие вложенные планы.
This commit is contained in:
+3
-3
@@ -24,11 +24,11 @@ interface PlannedPlanRepository : JpaRepository<PlannedPlanEntity, UUID> {
|
||||
/** Возвращает все планы в порядке старта. */
|
||||
fun findAllByOrderByStartTime(): List<PlannedPlanEntity>
|
||||
|
||||
/** Возвращает будущие planned-планы КА. */
|
||||
fun findAllBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime(
|
||||
/** Возвращает planned-планы КА, ещё не завершившиеся по времени (для supersede по интервалу). */
|
||||
fun findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime(
|
||||
spacecraftId: String,
|
||||
status: PlannedPlanStatus,
|
||||
startTime: LocalDateTime
|
||||
endTime: LocalDateTime
|
||||
): List<PlannedPlanEntity>
|
||||
|
||||
/** Возвращает ближайший future planned plan для продвижения после позднего REJECTED. */
|
||||
|
||||
+91
-34
@@ -9,32 +9,41 @@ import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Строит цепочку планов по окнам видимости КА.
|
||||
*
|
||||
* Главный инвариант цепочки: у соседних планов строго возрастают и старт, и конец
|
||||
* (`start[i] < start[i+1]` и `end[i] < end[i+1]`). Отсюда математически следует, что
|
||||
* ни один план не может лежать целиком внутри другого. Перекрытие сохраняется: следующий
|
||||
* план стартует на ЗРВ, попадающей внутрь предыдущего плана.
|
||||
*
|
||||
* Окна разных станций не сливаются — старт плана привязан к конкретной ЗРВ один к одному.
|
||||
*/
|
||||
@Service
|
||||
class PlanCalculationService(
|
||||
private val planningProperties: PlanningProperties
|
||||
) {
|
||||
|
||||
/** Количество ЗРВ, на которое следующий план должен заходить на предыдущий, если это возможно. */
|
||||
private val overlapShiftByWindows: Int = 1
|
||||
|
||||
/** Строит полную цепочку планов для КА по всем доступным окнам видимости. */
|
||||
fun calculatePlans(spacecraftPoints: SpacecraftPoints): List<CalculatedPlan> {
|
||||
/** Все доступные кандидаты начала/окончания плана для указанного КА. */
|
||||
val candidates: List<StartCandidate> = buildCandidates(spacecraftPoints)
|
||||
|
||||
return calculatePlansFromCandidates(
|
||||
return buildChain(
|
||||
spacecraftId = spacecraftPoints.spacecraftId,
|
||||
candidates = candidates,
|
||||
initialStartIndex = 0
|
||||
fromStartIndex = 0,
|
||||
initialLowerEndBound = null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит часть цепочки после уже известного плана.
|
||||
* Строит часть цепочки после уже известного (принятого) плана.
|
||||
*
|
||||
* Если предыдущий план удалось сопоставить с текущими окнами наблюдения,
|
||||
* следующий план начинается с нахлестом на одну ЗРВ.
|
||||
* Если сопоставление не удалось, используется безопасный fallback без нахлеста.
|
||||
* следующий план начинается с нахлестом на одну ЗРВ. Если сопоставление не удалось,
|
||||
* используется безопасный fallback без нахлеста. В обоих случаях конец первого нового
|
||||
* плана обязан быть строго позже конца принятого плана — это исключает вложенность с базой.
|
||||
*/
|
||||
fun calculatePlansAfter(spacecraftPoints: SpacecraftPoints, previousPlan: CalculatedPlan): List<CalculatedPlan> {
|
||||
/** Все доступные кандидаты начала/окончания плана для указанного КА. */
|
||||
@@ -53,11 +62,12 @@ class PlanCalculationService(
|
||||
?: -1
|
||||
|
||||
/** Стартовый индекс для пересчёта следующего хвоста цепочки. */
|
||||
val initialStartIndex: Int = if (
|
||||
val fromStartIndex: Int = if (
|
||||
previousStartIndex >= 0 &&
|
||||
previousEndIndex > previousStartIndex
|
||||
) {
|
||||
resolveNextStartIndex(
|
||||
candidates = candidates,
|
||||
currentStartIndex = previousStartIndex,
|
||||
endIndex = previousEndIndex
|
||||
)
|
||||
@@ -65,14 +75,15 @@ class PlanCalculationService(
|
||||
candidates.indexOfFirst { candidate -> candidate.startTime >= previousPlan.endTime }
|
||||
}
|
||||
|
||||
if (initialStartIndex == -1) {
|
||||
if (fromStartIndex !in candidates.indices) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return calculatePlansFromCandidates(
|
||||
return buildChain(
|
||||
spacecraftId = spacecraftPoints.spacecraftId,
|
||||
candidates = candidates,
|
||||
initialStartIndex = initialStartIndex
|
||||
fromStartIndex = fromStartIndex,
|
||||
initialLowerEndBound = previousPlan.endTime
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +107,8 @@ class PlanCalculationService(
|
||||
/** Индекс лучшего кандидата окончания для ближайшей закладки. */
|
||||
val endIndex: Int = findEndCandidateIndexOrNull(
|
||||
candidates = candidates,
|
||||
startIndex = nearestStartIndex
|
||||
startIndex = nearestStartIndex,
|
||||
lowerEndBound = null
|
||||
) ?: return null
|
||||
|
||||
return candidates[nearestStartIndex].toCalculatedPlan(
|
||||
@@ -105,37 +117,49 @@ class PlanCalculationService(
|
||||
)
|
||||
}
|
||||
|
||||
/** Преобразует отсортированных кандидатов в последовательную цепочку планов. */
|
||||
private fun calculatePlansFromCandidates(
|
||||
/**
|
||||
* Строит последовательную цепочку планов, начиная с `fromStartIndex`.
|
||||
*
|
||||
* Поддерживает инвариант строгого роста старта и конца: следующий план обязан
|
||||
* стартовать строго позже и заканчиваться строго позже предыдущего, поэтому ни один
|
||||
* план не оказывается внутри другого.
|
||||
*/
|
||||
private fun buildChain(
|
||||
spacecraftId: String,
|
||||
candidates: List<StartCandidate>,
|
||||
initialStartIndex: Int
|
||||
fromStartIndex: Int,
|
||||
initialLowerEndBound: LocalDateTime?
|
||||
): List<CalculatedPlan> {
|
||||
if (candidates.isEmpty() || initialStartIndex !in candidates.indices || initialStartIndex >= candidates.lastIndex) {
|
||||
if (candidates.isEmpty() || fromStartIndex !in candidates.indices || fromStartIndex >= candidates.lastIndex) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
/** Результирующая цепочка планов. */
|
||||
val resultPlans: MutableList<CalculatedPlan> = mutableListOf()
|
||||
/** Индекс кандидата, с которого сейчас пытаемся построить очередной план. */
|
||||
var currentStartIndex: Int = initialStartIndex
|
||||
var currentStartIndex: Int = fromStartIndex
|
||||
/** Конец последнего добавленного плана — нижняя граница для конца следующего. */
|
||||
var lowerEndBound: LocalDateTime? = initialLowerEndBound
|
||||
|
||||
while (currentStartIndex < candidates.lastIndex) {
|
||||
/** Текущий кандидат старта плана. */
|
||||
val currentCandidate: StartCandidate = candidates[currentStartIndex]
|
||||
/** Индекс найденного кандидата окончания плана. */
|
||||
/** Индекс найденного кандидата окончания плана со строго растущим концом. */
|
||||
val endIndex: Int = findEndCandidateIndexOrNull(
|
||||
candidates = candidates,
|
||||
startIndex = currentStartIndex
|
||||
startIndex = currentStartIndex,
|
||||
lowerEndBound = lowerEndBound
|
||||
) ?: break
|
||||
|
||||
resultPlans += currentCandidate.toCalculatedPlan(
|
||||
spacecraftId = spacecraftId,
|
||||
endTime = candidates[endIndex].endTime
|
||||
)
|
||||
lowerEndBound = candidates[endIndex].endTime
|
||||
|
||||
/** Индекс старта следующего плана с нахлестом на одну ЗРВ, если это безопасно. */
|
||||
val nextStartIndex: Int = resolveNextStartIndex(
|
||||
candidates = candidates,
|
||||
currentStartIndex = currentStartIndex,
|
||||
endIndex = endIndex
|
||||
)
|
||||
@@ -162,7 +186,7 @@ class PlanCalculationService(
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedBy { candidate -> candidate.startTime }
|
||||
.sortedWith(compareBy({ candidate -> candidate.startTime }, { candidate -> candidate.endTime }))
|
||||
|
||||
/** Возвращает целевую длительность плана из настроек. */
|
||||
private fun targetDuration(): Duration =
|
||||
@@ -173,31 +197,62 @@ class PlanCalculationService(
|
||||
Duration.ofMinutes(planningProperties.targetPlanDurationDeltaMinutes)
|
||||
|
||||
/**
|
||||
* Возвращает безопасный индекс старта следующего плана.
|
||||
* Возвращает индекс старта следующего плана с нахлестом на одну ЗРВ.
|
||||
*
|
||||
* Если длина текущего плана позволяет, следующий план стартует на одну ЗРВ раньше конца текущего.
|
||||
* Если такой нахлест невозможен, сервис просто двигается вперед на следующий кандидат,
|
||||
* Нахлест считается по времени, а не по плоскому индексу: берётся самая поздняя ЗРВ,
|
||||
* которая начинается строго раньше конечной ЗРВ текущего плана и строго позже его старта.
|
||||
* Это даёт ровно одно окно перекрытия внутри текущего плана и устойчиво к тому, что одну и
|
||||
* ту же ЗРВ видят несколько станций одновременно.
|
||||
*
|
||||
* Если такой ЗРВ нет, сервис двигается на ближайший более поздний старт (стык без нахлеста),
|
||||
* чтобы не зациклиться и не повторить тот же самый план.
|
||||
*/
|
||||
private fun resolveNextStartIndex(currentStartIndex: Int, endIndex: Int): Int =
|
||||
maxOf(currentStartIndex + 1, endIndex - overlapShiftByWindows)
|
||||
private fun resolveNextStartIndex(
|
||||
candidates: List<StartCandidate>,
|
||||
currentStartIndex: Int,
|
||||
endIndex: Int
|
||||
): Int {
|
||||
/** Время старта текущего плана. */
|
||||
val currentStartTime: LocalDateTime = candidates[currentStartIndex].startTime
|
||||
/** Время старта конечной ЗРВ текущего плана. */
|
||||
val endStartTime: LocalDateTime = candidates[endIndex].startTime
|
||||
|
||||
/** Самая поздняя ЗРВ строго внутри текущего плана — даёт одно окно нахлеста. */
|
||||
val overlappingIndex: Int = candidates.indices.lastOrNull { index ->
|
||||
candidates[index].startTime > currentStartTime &&
|
||||
candidates[index].startTime < endStartTime
|
||||
} ?: -1
|
||||
|
||||
if (overlappingIndex > currentStartIndex) {
|
||||
return overlappingIndex
|
||||
}
|
||||
|
||||
/** Ближайший более поздний старт, если нахлест невозможен. */
|
||||
return candidates.indexOfFirst { candidate -> candidate.startTime > currentStartTime }
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит лучший конечный кандидат для плана, начинающегося с `startIndex`.
|
||||
*
|
||||
* Сначала сервис старается попасть в целевое окно `target ± delta`.
|
||||
* Если это невозможно, он всё равно обязан построить план и берёт
|
||||
* любой доступный конец, наиболее близкий к целевой длительности.
|
||||
* Рассматриваются только концы строго позже старта плана и строго позже конца предыдущего
|
||||
* плана (`lowerEndBound`) — это гарантирует строгий рост конца цепочки и отсутствие вложенности.
|
||||
* Сначала сервис старается попасть в целевое окно `target ± delta`; если это невозможно,
|
||||
* берётся любой допустимый конец, наиболее близкий к целевой длительности.
|
||||
*/
|
||||
private fun findEndCandidateIndexOrNull(
|
||||
candidates: List<StartCandidate>,
|
||||
startIndex: Int
|
||||
startIndex: Int,
|
||||
lowerEndBound: LocalDateTime?
|
||||
): Int? {
|
||||
/** Кандидат, с которого начинается рассматриваемый план. */
|
||||
val startCandidate: StartCandidate = candidates[startIndex]
|
||||
/** Все доступные кандидаты окончания после выбранного старта. */
|
||||
/** Допустимые кандидаты окончания: позже старта, позже предыдущего конца. */
|
||||
val allEndCandidates: List<IndexedValue<StartCandidate>> = candidates.withIndex()
|
||||
.filter { indexedCandidate -> indexedCandidate.index > startIndex }
|
||||
.filter { indexedCandidate ->
|
||||
indexedCandidate.index > startIndex &&
|
||||
indexedCandidate.value.endTime > startCandidate.startTime &&
|
||||
(lowerEndBound == null || indexedCandidate.value.endTime > lowerEndBound)
|
||||
}
|
||||
|
||||
if (allEndCandidates.isEmpty()) {
|
||||
return null
|
||||
@@ -239,8 +294,8 @@ class PlanCalculationService(
|
||||
/**
|
||||
* Выбирает лучший конец по минимальному отклонению от целевой длительности.
|
||||
*
|
||||
* При равной близости предпочтение отдаётся более длинному плану,
|
||||
* чтобы не занижать результат без необходимости.
|
||||
* При равной близости предпочтение отдаётся более длинному плану, а затем более раннему концу,
|
||||
* чтобы выбор был детерминированным.
|
||||
*/
|
||||
private fun selectPreferredEndCandidate(
|
||||
candidates: List<IndexedValue<StartCandidate>>,
|
||||
@@ -257,6 +312,8 @@ class PlanCalculationService(
|
||||
)
|
||||
}.thenByDescending { candidate ->
|
||||
Duration.between(startTime, candidate.value.endTime)
|
||||
}.thenBy { candidate ->
|
||||
candidate.value.endTime
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -87,10 +87,10 @@ class RebuildPlanScheduleUseCase(
|
||||
calculatedBusinessKeys: Set<String>,
|
||||
now: LocalDateTime
|
||||
) {
|
||||
plannedPlanRepository.findAllBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime(
|
||||
plannedPlanRepository.findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime(
|
||||
spacecraftId = spacecraftId,
|
||||
status = PlannedPlanStatus.PLANNED,
|
||||
startTime = now
|
||||
endTime = now
|
||||
)
|
||||
.filter { plan -> plan.businessKey() !in calculatedBusinessKeys }
|
||||
.forEach { plan ->
|
||||
|
||||
+68
-8
@@ -1,9 +1,11 @@
|
||||
package space.nstart.pcp_tgu_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp_tgu_service.config.PlanningProperties
|
||||
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
|
||||
import space.nstart.pcp_tgu_service.domain.InsertionPoint
|
||||
import space.nstart.pcp_tgu_service.domain.ObservationWindow
|
||||
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
|
||||
@@ -29,16 +31,16 @@ class PlanCalculationServiceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(4, plans.size)
|
||||
assertChainInvariant(plans)
|
||||
assertEquals(3, plans.size)
|
||||
assertEquals(start, plans[0].startTime)
|
||||
assertEquals(start.plusDays(1).plusMinutes(10), plans[0].endTime)
|
||||
assertEquals(start.plusHours(16), plans[1].startTime)
|
||||
assertEquals(start.plusDays(1).plusHours(16).plusMinutes(10), plans[1].endTime)
|
||||
assertEquals(start.plusHours(40).plusMinutes(10), plans[1].endTime)
|
||||
assertEquals("KPP-3", plans[1].kppId)
|
||||
assertEquals(start.plusHours(32), plans[2].startTime)
|
||||
assertEquals(start.plusDays(2).plusMinutes(10), plans[2].endTime)
|
||||
assertEquals(start.plusHours(40), plans[3].startTime)
|
||||
assertEquals(start.plusDays(2).plusMinutes(10), plans[3].endTime)
|
||||
assertEquals("KPP-6", plans[2].kppId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,14 +61,15 @@ class PlanCalculationServiceTest {
|
||||
|
||||
val plansAfterFirst = service.calculatePlansAfter(spacecraftPoints, fullPlans.first())
|
||||
|
||||
assertEquals(3, plansAfterFirst.size)
|
||||
assertChainInvariant(plansAfterFirst)
|
||||
assertEquals(2, plansAfterFirst.size)
|
||||
assertEquals(start.plusHours(16), plansAfterFirst[0].startTime)
|
||||
assertEquals(start.plusDays(1).plusHours(16).plusMinutes(10), plansAfterFirst[0].endTime)
|
||||
assertEquals(start.plusHours(40).plusMinutes(10), plansAfterFirst[0].endTime)
|
||||
assertEquals("KPP-3", plansAfterFirst[0].kppId)
|
||||
assertEquals(start.plusHours(32), plansAfterFirst[1].startTime)
|
||||
assertEquals(start.plusDays(2).plusMinutes(10), plansAfterFirst[1].endTime)
|
||||
assertEquals(start.plusHours(40), plansAfterFirst[2].startTime)
|
||||
assertEquals(start.plusDays(2).plusMinutes(10), plansAfterFirst[2].endTime)
|
||||
// Хвост не должен заканчиваться раньше принятого базового плана.
|
||||
assertTrue(plansAfterFirst.all { plan -> plan.endTime > fullPlans.first().endTime })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,6 +99,7 @@ class PlanCalculationServiceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertEquals(2, plans.size)
|
||||
assertEquals(start, plans[0].startTime)
|
||||
assertEquals(start.plusDays(1).plusMinutes(10), plans[0].endTime)
|
||||
@@ -135,6 +139,7 @@ class PlanCalculationServiceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertEquals(2, plans.size)
|
||||
assertEquals(start, plans[0].startTime)
|
||||
assertEquals(start.plusMinutes(55), plans[1].startTime)
|
||||
@@ -153,6 +158,7 @@ class PlanCalculationServiceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertEquals(2, plans.size)
|
||||
assertEquals(start, plans[0].startTime)
|
||||
assertEquals(start.plusHours(1).plusMinutes(40), plans[0].endTime)
|
||||
@@ -174,6 +180,7 @@ class PlanCalculationServiceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertEquals(start.plusHours(1).plusMinutes(40), plans.first().endTime)
|
||||
}
|
||||
|
||||
@@ -192,12 +199,65 @@ class PlanCalculationServiceTest {
|
||||
|
||||
val plans = service.calculatePlans(spacecraftPoints(*simultaneousWindows.toTypedArray()))
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertTrue(plans.isNotEmpty())
|
||||
assertTrue(plans.size <= simultaneousWindows.size)
|
||||
assertEquals(start, plans.first().startTime)
|
||||
assertEquals(start.plusDays(1).plusMinutes(10), plans.first().endTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculatePlans never nests plans when several stations see the same passes (Emissio case)`() {
|
||||
// Прод-конфигурация: цель 3 часа, допуск 1 час.
|
||||
val service = createService(targetMinutes = 180L, deltaMinutes = 60L)
|
||||
val start = LocalDateTime.of(2026, 3, 10, 10, 0)
|
||||
// Пролёты примерно раз в час, каждый виден тремя станциями почти одновременно
|
||||
// (это и есть корень вложенности у Emissio): одинаковые/близкие старты, дублирующиеся ЗРВ.
|
||||
val stations = listOf("ST-A", "ST-B", "ST-C")
|
||||
val windows = buildList {
|
||||
for (passIndex in 0..8) {
|
||||
stations.forEachIndexed { stationOffset, stationId ->
|
||||
add(
|
||||
window(
|
||||
pointId = "$stationId-$passIndex",
|
||||
startTime = start.plusHours(passIndex.toLong()).plusMinutes(stationOffset.toLong())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val plans = service.calculatePlans(spacecraftPoints(*windows.toTypedArray()))
|
||||
|
||||
assertChainInvariant(plans)
|
||||
assertTrue(plans.size >= 2, "expected a multi-plan chain, got ${plans.size}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет главный инвариант цепочки: старт и конец строго возрастают, и ни один план
|
||||
* не лежит целиком внутри другого.
|
||||
*/
|
||||
private fun assertChainInvariant(plans: List<CalculatedPlan>) {
|
||||
for (index in 1 until plans.size) {
|
||||
assertTrue(
|
||||
plans[index].startTime > plans[index - 1].startTime,
|
||||
"start must strictly increase at index $index: $plans"
|
||||
)
|
||||
assertTrue(
|
||||
plans[index].endTime > plans[index - 1].endTime,
|
||||
"end must strictly increase at index $index: $plans"
|
||||
)
|
||||
}
|
||||
for (outer in plans.indices) {
|
||||
for (inner in plans.indices) {
|
||||
if (outer == inner) continue
|
||||
val contains = plans[outer].startTime <= plans[inner].startTime &&
|
||||
plans[inner].endTime <= plans[outer].endTime
|
||||
assertFalse(contains, "plan $inner must not be nested in plan $outer: $plans")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createService(targetMinutes: Long, deltaMinutes: Long): PlanCalculationService =
|
||||
PlanCalculationService(
|
||||
PlanningProperties(
|
||||
|
||||
+15
-2
@@ -27,6 +27,17 @@ class RebuildPlanScheduleUseCaseTest {
|
||||
fun `rebuild keeps stable planId by business key and supersedes obsolete planned only`() {
|
||||
val spacecraftId = "25544"
|
||||
val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
|
||||
// Устаревший PLANNED-план, который уже стартовал, но ещё не завершился —
|
||||
// должен быть погашен по новому условию end_time > now (а не start_time > now).
|
||||
val stalePlanId = UUID.randomUUID()
|
||||
planStore[stalePlanId] = PlannedPlanEntity(
|
||||
planId = stalePlanId,
|
||||
spacecraftId = spacecraftId,
|
||||
startTime = LocalDateTime.now().minusHours(1),
|
||||
endTime = LocalDateTime.now().plusHours(1),
|
||||
kppId = "KPP-STALE",
|
||||
status = PlannedPlanStatus.PLANNED
|
||||
)
|
||||
val plannedRepository = plannedRepository(planStore)
|
||||
val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
|
||||
val stateRepository = stateRepository(state)
|
||||
@@ -58,6 +69,8 @@ class RebuildPlanScheduleUseCaseTest {
|
||||
|
||||
service.rebuild(spacecraftId)
|
||||
val firstPlanId = planStore.values.first { plan -> plan.kppId == "KPP-1" }.planId
|
||||
// Устаревший активный план без места в новой цепочке погашен уже на первом rebuild.
|
||||
assertEquals(PlannedPlanStatus.SUPERSEDED, planStore.getValue(stalePlanId).status)
|
||||
|
||||
`when`(calculationService.calculatePlans(anyNonNull())).thenReturn(listOf(firstPlan))
|
||||
service.rebuild(spacecraftId)
|
||||
@@ -140,13 +153,13 @@ class RebuildPlanScheduleUseCaseTest {
|
||||
store[plan.planId] = plan
|
||||
plan
|
||||
}
|
||||
`when`(repository.findAllBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime(anyNonNull(), anyNonNull(), anyNonNull()))
|
||||
`when`(repository.findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime(anyNonNull(), anyNonNull(), anyNonNull()))
|
||||
.thenAnswer { invocation ->
|
||||
store.values
|
||||
.filter { plan ->
|
||||
plan.spacecraftId == invocation.arguments[0] &&
|
||||
plan.status == invocation.arguments[1] &&
|
||||
plan.startTime > invocation.arguments[2] as LocalDateTime
|
||||
plan.endTime > invocation.arguments[2] as LocalDateTime
|
||||
}
|
||||
.sortedBy { plan -> plan.startTime }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user