test #433

Merged
klaus merged 41 commits from test into main 2026-07-10 07:20:07 +00:00
4 changed files with 154 additions and 6 deletions
Showing only changes of commit 64b05deee7 - Show all commits

View File

@@ -18,17 +18,30 @@ class RecommendationSnapshotPersistenceAdapter(
return repository.findLatestSnapshots(sectionType.name, offset, limit).map { it.toRecord() }
}
override fun findSnapshots(
sectionType: RecommendedSectionType,
snapshotAt: LocalDateTime,
offset: Long,
limit: Int
): List<RecommendationSnapshotRecord> {
return repository.findSnapshots(sectionType.name, snapshotAt, offset, limit).map { it.toRecord() }
}
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean {
return repository.existsBySectionType(sectionType)
}
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
return repository.existsBySectionTypeAndSnapshotAt(sectionType, snapshotAt)
}
override fun replaceSnapshots(
sectionType: RecommendedSectionType,
snapshotAt: LocalDateTime,
newSnapshots: List<RecommendationSnapshotRecord>
) {
if (newSnapshots.isEmpty() && sectionType == RecommendedSectionType.AI_CHARACTER) {
repository.deleteBySectionType(sectionType)
if (newSnapshots.isEmpty() && supportsEmptySnapshotMarker(sectionType)) {
repository.deleteBySectionTypeAndSnapshotAt(sectionType, snapshotAt)
repository.save(emptySnapshotMarker(sectionType, snapshotAt).toEntity())
return
}
@@ -70,6 +83,10 @@ class RecommendationSnapshotPersistenceAdapter(
)
}
private fun supportsEmptySnapshotMarker(sectionType: RecommendedSectionType): Boolean {
return sectionType == RecommendedSectionType.AI_CHARACTER || sectionType == RecommendedSectionType.CHEER_CREATOR
}
companion object {
private const val EMPTY_SNAPSHOT_MARKER_TARGET_ID = 0L
}

View File

@@ -29,9 +29,28 @@ interface RecommendationSnapshotRepository : JpaRepository<RecommendationSnapsho
@Param("limit") limit: Int
): List<RecommendationSnapshot>
@Query(
value = """
select *
from recommendation_snapshot rs
where rs.section_type = :sectionType
and rs.snapshot_at = :snapshotAt
and rs.target_id <> 0
order by rs.score desc, rs.random_tie_breaker asc
limit :limit offset :offset
""",
nativeQuery = true
)
fun findSnapshots(
@Param("sectionType") sectionType: String,
@Param("snapshotAt") snapshotAt: LocalDateTime,
@Param("offset") offset: Long,
@Param("limit") limit: Int
): List<RecommendationSnapshot>
fun deleteBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime)
fun deleteBySectionType(sectionType: RecommendedSectionType)
fun existsBySectionType(sectionType: RecommendedSectionType): Boolean
fun existsBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean
}

View File

@@ -10,8 +10,17 @@ interface RecommendationSnapshotPort {
limit: Int = Int.MAX_VALUE
): List<RecommendationSnapshotRecord>
fun findSnapshots(
sectionType: RecommendedSectionType,
snapshotAt: LocalDateTime,
offset: Long = 0,
limit: Int = Int.MAX_VALUE
): List<RecommendationSnapshotRecord>
fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean
fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean
fun replaceSnapshots(
sectionType: RecommendedSectionType,
snapshotAt: LocalDateTime,

View File

@@ -77,6 +77,61 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
assertEquals(listOf(2L, 3L), snapshots.map { it.targetId })
}
@Test
fun shouldFindSnapshotsByExactSnapshotAtAndExcludeEmptyMarker() {
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
repository.saveAll(
listOf(
snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 1L, score = 999.0, snapshotAt = oldSnapshotAt),
snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 0L, score = 0.0, snapshotAt = snapshotAt),
snapshot(
RecommendedSectionType.CHEER_CREATOR,
targetId = 2L,
score = 100.0,
snapshotAt = snapshotAt,
randomTieBreaker = 0.9
),
snapshot(
RecommendedSectionType.CHEER_CREATOR,
targetId = 3L,
score = 200.0,
snapshotAt = snapshotAt,
randomTieBreaker = 0.8
)
)
)
val snapshots = adapter.findSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, offset = 0, limit = 10)
assertEquals(listOf(3L, 2L), snapshots.map { it.targetId })
assertEquals(listOf(snapshotAt, snapshotAt), snapshots.map { it.snapshotAt })
}
@Test
fun shouldCheckSnapshotExistenceByExactSnapshotAtIncludingEmptyMarker() {
val staleSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
val expectedSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
repository.save(
snapshot(
RecommendedSectionType.CHEER_CREATOR,
targetId = 1L,
score = 100.0,
snapshotAt = staleSnapshotAt
)
)
assertEquals(false, adapter.existsSnapshot(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt))
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt, emptyList())
assertEquals(true, adapter.existsSnapshot(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt))
assertEquals(
emptyList<RecommendationSnapshotRecord>(),
adapter.findSnapshots(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt)
)
}
@Test
fun shouldReplaceSnapshotsByDeletingSameSectionSnapshotAtOnly() {
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
@@ -109,7 +164,7 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
}
@Test
fun shouldClearAiCharacterSectionSnapshotsWhenReplacingWithEmptySnapshots() {
fun shouldSaveAiCharacterEmptySnapshotMarkerWhenReplacingWithEmptySnapshots() {
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
repository.saveAll(
@@ -125,9 +180,10 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
assertEquals(listOf(2L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
assertEquals(
listOf(0L),
listOf(1L, 0L),
repository.findAll()
.filter { it.sectionType == RecommendedSectionType.AI_CHARACTER }
.sortedBy { it.snapshotAt }
.map { it.targetId }
)
}
@@ -142,6 +198,53 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
}
@Test
fun shouldSaveCheerCreatorEmptySnapshotMarkerWhenReplacingWithEmptySnapshots() {
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
repository.save(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 1L, score = 100.0, snapshotAt = oldSnapshotAt))
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, emptyList())
assertEquals(emptyList<RecommendationSnapshotRecord>(), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR))
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.CHEER_CREATOR))
assertEquals(
listOf(1L, 0L),
repository.findAll()
.filter { it.sectionType == RecommendedSectionType.CHEER_CREATOR }
.sortedBy { it.snapshotAt }
.map { it.targetId }
)
}
@Test
fun shouldReplaceCheerCreatorEmptyMarkerWithRealSnapshots() {
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, emptyList())
adapter.replaceSnapshots(
RecommendedSectionType.CHEER_CREATOR,
snapshotAt,
listOf(
RecommendationSnapshotRecord(
sectionType = RecommendedSectionType.CHEER_CREATOR,
targetId = 9L,
score = 90.0,
snapshotAt = snapshotAt,
randomTieBreaker = 0.9
)
)
)
assertEquals(listOf(9L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
assertEquals(
listOf(9L),
repository.findAll()
.filter { it.sectionType == RecommendedSectionType.CHEER_CREATOR }
.map { it.targetId }
)
}
private fun snapshot(
sectionType: RecommendedSectionType,
targetId: Long,