test #433
@@ -483,74 +483,89 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
|
||||
override fun findAiCharacterSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
val sql = """
|
||||
select cc.id as target_id,
|
||||
(((select count(cm.id)
|
||||
from chat_message cm
|
||||
join chat_participant cp on cp.id = cm.participant_id
|
||||
where cp.character_id = cc.id
|
||||
and cm.created_at >= :windowStart
|
||||
and cm.created_at <= :snapshotAt
|
||||
and cm.is_active = true
|
||||
and cp.is_active = true
|
||||
and cp.participant_type = 'CHARACTER') * ${RecommendationScoreSpec.AI_RECENT_CHAT_WEIGHT} +
|
||||
(select count(distinct up.member_id)
|
||||
from chat_message cm
|
||||
join chat_participant up on up.id = cm.participant_id
|
||||
join chat_participant cp on cp.chat_room_id = cm.chat_room_id
|
||||
join member m on m.id = up.member_id
|
||||
where cp.character_id = cc.id
|
||||
and cm.created_at >= :windowStart
|
||||
and cm.created_at <= :snapshotAt
|
||||
and cm.is_active = true
|
||||
and up.is_active = true
|
||||
and cp.is_active = true
|
||||
and up.participant_type = 'USER'
|
||||
and cp.participant_type = 'CHARACTER'
|
||||
and m.is_active = true) * ${RecommendationScoreSpec.AI_RECENT_ACTIVE_USER_WEIGHT}) *
|
||||
case
|
||||
when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.NEW_BOOST_10_DAYS}
|
||||
when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.NEW_BOOST_20_DAYS}
|
||||
when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.NEW_BOOST_30_DAYS}
|
||||
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
|
||||
end) as score,
|
||||
rand() as random_tie_breaker
|
||||
from chat_character cc
|
||||
where cc.is_active = true
|
||||
and (exists (
|
||||
select 1
|
||||
select ranked.target_id,
|
||||
ranked.score,
|
||||
ranked.snapshot_rank as random_tie_breaker
|
||||
from (
|
||||
select scored.target_id,
|
||||
scored.score,
|
||||
row_number() over (order by scored.score desc, scored.character_created_at desc, scored.target_id desc) as snapshot_rank
|
||||
from (
|
||||
select cc.id as target_id,
|
||||
cc.created_at as character_created_at,
|
||||
((coalesce(acs.ai_chat_count, 0) * ${RecommendationScoreSpec.AI_CHAT_WEIGHT} +
|
||||
coalesce(aus.active_user_count, 0) * ${RecommendationScoreSpec.AI_ACTIVE_USER_WEIGHT} +
|
||||
coalesce(fs.follow_increase_count, 0) * ${RecommendationScoreSpec.AI_FOLLOW_INCREASE_WEIGHT}) *
|
||||
case
|
||||
when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.AI_NEW_BOOST_10_DAYS}
|
||||
when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.AI_NEW_BOOST_20_DAYS}
|
||||
when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.AI_NEW_BOOST_30_DAYS}
|
||||
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
|
||||
end) as score
|
||||
from chat_character cc
|
||||
join member creator_member on creator_member.id = cc.creator_member_id
|
||||
left join (
|
||||
select cp.character_id as character_id, count(cm.id) as ai_chat_count
|
||||
from chat_message cm
|
||||
join chat_participant cp on cp.id = cm.participant_id
|
||||
where cp.character_id = cc.id
|
||||
and cm.created_at >= :windowStart
|
||||
and cm.created_at <= :snapshotAt
|
||||
where cm.created_at >= :windowStart
|
||||
and cm.created_at < :windowEndExclusive
|
||||
and cm.is_active = true
|
||||
and cp.is_active = true
|
||||
and cp.participant_type = 'CHARACTER'
|
||||
) or exists (
|
||||
select 1
|
||||
group by cp.character_id
|
||||
) acs on acs.character_id = cc.id
|
||||
left join (
|
||||
select cp.character_id as character_id, count(distinct up.member_id) as active_user_count
|
||||
from chat_message cm
|
||||
join chat_participant up on up.id = cm.participant_id
|
||||
join chat_participant cp on cp.chat_room_id = cm.chat_room_id
|
||||
join member m on m.id = up.member_id
|
||||
where cp.character_id = cc.id
|
||||
and cm.created_at >= :windowStart
|
||||
and cm.created_at <= :snapshotAt
|
||||
where cm.created_at >= :windowStart
|
||||
and cm.created_at < :windowEndExclusive
|
||||
and cm.is_active = true
|
||||
and up.is_active = true
|
||||
and cp.is_active = true
|
||||
and up.participant_type = 'USER'
|
||||
and cp.participant_type = 'CHARACTER'
|
||||
and m.is_active = true
|
||||
))
|
||||
order by score desc, random_tie_breaker asc
|
||||
group by cp.character_id
|
||||
) aus on aus.character_id = cc.id
|
||||
left join (
|
||||
select cf.creator_id as creator_id, count(cf.id) as follow_increase_count
|
||||
from creator_following cf
|
||||
join member follower_member on follower_member.id = cf.member_id
|
||||
where cf.created_at >= :windowStart
|
||||
and cf.created_at < :windowEndExclusive
|
||||
and cf.is_active = true
|
||||
and follower_member.is_active = true
|
||||
group by cf.creator_id
|
||||
) fs on fs.creator_id = cc.creator_member_id
|
||||
where cc.is_active = true
|
||||
and cc.created_at is not null
|
||||
and cc.created_at < :windowEndExclusive
|
||||
and creator_member.is_active = true
|
||||
and creator_member.role = 'CREATOR'
|
||||
and creator_member.member_kind = 'AI_CHARACTER'
|
||||
) scored
|
||||
where scored.score > 0
|
||||
) ranked
|
||||
order by ranked.score desc, ranked.snapshot_rank asc
|
||||
limit :limit
|
||||
""".trimIndent()
|
||||
|
||||
return executeSnapshotQuery(sql, RecommendedSectionType.AI_CHARACTER, windowStart, snapshotAt, limit)
|
||||
return executeSnapshotQuery(
|
||||
sql,
|
||||
RecommendedSectionType.AI_CHARACTER,
|
||||
windowStart,
|
||||
windowEndExclusive.minusSeconds(1),
|
||||
limit,
|
||||
windowEndExclusive
|
||||
)
|
||||
}
|
||||
|
||||
override fun findCheerCreatorSnapshots(
|
||||
@@ -1129,12 +1144,19 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
sectionType: RecommendedSectionType,
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
limit: Int
|
||||
limit: Int,
|
||||
windowEndExclusive: LocalDateTime? = null
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("windowStart", windowStart)
|
||||
.setParameter("snapshotAt", snapshotAt)
|
||||
.setParameter("limit", limit)
|
||||
if (sql.contains(":snapshotAt")) {
|
||||
query.setParameter("snapshotAt", snapshotAt)
|
||||
}
|
||||
if (windowEndExclusive != null) {
|
||||
query.setParameter("windowEndExclusive", windowEndExclusive)
|
||||
}
|
||||
query
|
||||
.setParameter(
|
||||
"boost10Start",
|
||||
snapshotAt.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT).atStartOfDay()
|
||||
|
||||
@@ -40,7 +40,7 @@ interface HomeRecommendationQueryPort {
|
||||
|
||||
fun findAiCharacterSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord>
|
||||
|
||||
|
||||
@@ -523,12 +523,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 스냅샷은 AI 발화 수와 중복 없는 활성 사용자 수를 집계하고 팔로우 증가량은 제외한다")
|
||||
fun shouldFindAiCharacterSnapshotsWithoutFollowIncrease() {
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
@DisplayName("AI 캐릭터 스냅샷은 전날 AI 발화 수, 중복 없는 활성 사용자 수, 신규 팔로우 증가량을 집계한다")
|
||||
fun shouldFindAiCharacterSnapshotsWithFollowIncrease() {
|
||||
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||
val windowEndExclusive = snapshotAt.plusSeconds(1)
|
||||
val user1 = saveMember("ai-user-1", MemberRole.USER)
|
||||
val user2 = saveMember("ai-user-2", MemberRole.USER)
|
||||
val inactiveFollower = saveMember("ai-inactive-follower", MemberRole.USER)
|
||||
val inactiveMemberFollower = saveMember("ai-inactive-member-follower", MemberRole.USER, isActive = false)
|
||||
val character = saveCharacter("character-1", isActive = true)
|
||||
val inactiveCharacter = saveCharacter("character-2", isActive = false)
|
||||
val room = saveChatRoom("room-1")
|
||||
@@ -547,23 +550,32 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val inactiveMessage = saveMessage(room, characterParticipant, "message-4", isActive = false)
|
||||
val oldMessage = saveMessage(room, characterParticipant, "message-5", isActive = true)
|
||||
val inactiveCharacterMessage = saveMessage(otherRoom, inactiveCharacterUser, "message-6", isActive = true)
|
||||
updateCreatedAt("ChatMessage", message1.id!!, windowStart.plusDays(1))
|
||||
updateCreatedAt("ChatMessage", message2.id!!, windowStart.plusDays(2))
|
||||
updateCreatedAt("ChatMessage", message1.id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("ChatMessage", message2.id!!, windowStart.plusHours(2))
|
||||
updateCreatedAt("ChatMessage", message3.id!!, snapshotAt)
|
||||
updateCreatedAt("ChatMessage", characterMessage1.id!!, windowStart.plusDays(1))
|
||||
updateCreatedAt("ChatMessage", characterMessage1.id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("ChatMessage", characterMessage2.id!!, snapshotAt)
|
||||
updateCreatedAt("ChatMessage", inactiveMessage.id!!, windowStart.plusDays(3))
|
||||
updateCreatedAt("ChatMessage", inactiveMessage.id!!, windowStart.plusHours(3))
|
||||
updateCreatedAt("ChatMessage", oldMessage.id!!, windowStart.minusSeconds(1))
|
||||
updateCreatedAt("ChatMessage", inactiveCharacterMessage.id!!, windowStart.plusDays(1))
|
||||
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 5, 20, 12, 0))
|
||||
updateCreatedAt("ChatMessage", inactiveCharacterMessage.id!!, windowStart.plusHours(1))
|
||||
val follow1 = saveFollowing(user1, character.creatorMember!!, isActive = true)
|
||||
val follow2 = saveFollowing(user2, character.creatorMember!!, isActive = true)
|
||||
val inactiveFollow = saveFollowing(inactiveFollower, character.creatorMember!!, isActive = false)
|
||||
val inactiveMemberFollow = saveFollowing(inactiveMemberFollower, character.creatorMember!!, isActive = true)
|
||||
updateCreatedAt("CreatorFollowing", follow1.id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("CreatorFollowing", follow2.id!!, snapshotAt)
|
||||
updateCreatedAt("CreatorFollowing", inactiveFollow.id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("CreatorFollowing", inactiveMemberFollow.id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 7, 1, 12, 0))
|
||||
flushAndClear()
|
||||
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, snapshotAt, limit = 10)
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
|
||||
|
||||
val expectedScore = scorePolicy.calculateAiChatScore(
|
||||
recentChatCount = 2,
|
||||
recentActiveUserCount = 2,
|
||||
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 5, 20, 12, 0), snapshotAt)
|
||||
followIncreaseCount = 2,
|
||||
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 7, 1, 12, 0), snapshotAt)
|
||||
)
|
||||
assertEquals(1, snapshots.size)
|
||||
assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType)
|
||||
@@ -572,11 +584,75 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
assertEquals(snapshotAt, snapshots.single().snapshotAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 스냅샷은 종료 시각을 exclusive 경계로 집계한다")
|
||||
fun shouldFindAiCharacterSnapshotsWithExclusiveWindowEnd() {
|
||||
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 7, 9, 15, 0)
|
||||
val snapshotAt = windowEndExclusive.minusSeconds(1)
|
||||
val includedUser = saveMember("ai-window-included-user", MemberRole.USER)
|
||||
val excludedUser = saveMember("ai-window-excluded-user", MemberRole.USER)
|
||||
val character = saveCharacter("ai-window-character", isActive = true)
|
||||
val excludedCharacter = saveCharacter("ai-window-excluded-character", isActive = true)
|
||||
val room = saveChatRoom("ai-window-room")
|
||||
val excludedRoom = saveChatRoom("ai-window-excluded-room")
|
||||
val includedUserParticipant = saveParticipant(room, ParticipantType.USER, member = includedUser)
|
||||
val includedCharacterParticipant = saveParticipant(room, ParticipantType.CHARACTER, character = character)
|
||||
val excludedUserParticipant = saveParticipant(excludedRoom, ParticipantType.USER, member = excludedUser)
|
||||
val excludedCharacterParticipant = saveParticipant(excludedRoom, ParticipantType.CHARACTER, character = excludedCharacter)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(room, includedUserParticipant, "included-user", true).id!!,
|
||||
windowEndExclusive.minusSeconds(1)
|
||||
)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(room, includedCharacterParticipant, "included-character", true).id!!,
|
||||
windowEndExclusive.minusSeconds(1)
|
||||
)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(excludedRoom, excludedUserParticipant, "excluded-user", true).id!!,
|
||||
windowEndExclusive
|
||||
)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(excludedRoom, excludedCharacterParticipant, "excluded-character", true).id!!,
|
||||
windowEndExclusive
|
||||
)
|
||||
updateCreatedAt(
|
||||
"CreatorFollowing",
|
||||
saveFollowing(includedUser, character.creatorMember!!, isActive = true).id!!,
|
||||
windowEndExclusive.minusSeconds(1)
|
||||
)
|
||||
updateCreatedAt(
|
||||
"CreatorFollowing",
|
||||
saveFollowing(excludedUser, excludedCharacter.creatorMember!!, isActive = true).id!!,
|
||||
windowEndExclusive
|
||||
)
|
||||
updateCreatedAt("ChatCharacter", character.id!!, windowStart)
|
||||
updateCreatedAt("ChatCharacter", excludedCharacter.id!!, windowEndExclusive)
|
||||
flushAndClear()
|
||||
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
|
||||
|
||||
val expectedScore = scorePolicy.calculateAiChatScore(
|
||||
recentChatCount = 1,
|
||||
recentActiveUserCount = 1,
|
||||
followIncreaseCount = 1,
|
||||
newBoost = scorePolicy.calculateAiCharacterNewBoost(windowStart, snapshotAt)
|
||||
)
|
||||
assertEquals(listOf(character.id), snapshots.map { it.targetId })
|
||||
assertEquals(expectedScore, snapshots.single().score, 0.0001)
|
||||
assertEquals(snapshotAt, snapshots.single().snapshotAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 스냅샷은 DB에서 최종 점수를 계산한 뒤 정렬하고 limit을 적용한다")
|
||||
fun shouldFindAiCharacterSnapshotsWithDbScoreOrderAndLimit() {
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowEndExclusive = snapshotAt.plusSeconds(1)
|
||||
val user = saveMember("ai-score-user", MemberRole.USER)
|
||||
val oldHighActivityCharacter = saveCharacter("old-high-activity", isActive = true)
|
||||
val newLowerActivityCharacter = saveCharacter("new-lower-activity", isActive = true)
|
||||
@@ -610,20 +686,92 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
updateCreatedAt("ChatCharacter", newLowerActivityCharacter.id!!, LocalDateTime.of(2026, 5, 20, 0, 0))
|
||||
flushAndClear()
|
||||
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, snapshotAt, limit = 1)
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 1)
|
||||
|
||||
val expectedScore = scorePolicy.calculateAiChatScore(
|
||||
recentChatCount = 2,
|
||||
recentChatCount = 3,
|
||||
recentActiveUserCount = 1,
|
||||
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 5, 20, 0, 0), snapshotAt)
|
||||
followIncreaseCount = 0,
|
||||
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 4, 1, 0, 0), snapshotAt)
|
||||
)
|
||||
assertEquals(1, snapshots.size)
|
||||
assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType)
|
||||
assertEquals(newLowerActivityCharacter.id, snapshots.single().targetId)
|
||||
assertEquals(oldHighActivityCharacter.id, snapshots.single().targetId)
|
||||
assertEquals(expectedScore, snapshots.single().score, 0.0001)
|
||||
assertEquals(snapshotAt, snapshots.single().snapshotAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 스냅샷은 생성일 누락/미래와 유효하지 않은 AI 크리에이터 회원을 제외한다")
|
||||
fun shouldExcludeInvalidAiCharacterSnapshotCandidates() {
|
||||
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||
val windowEndExclusive = snapshotAt.plusSeconds(1)
|
||||
val user = saveMember("ai-exclude-user", MemberRole.USER)
|
||||
val visibleCharacter = saveCharacter("ai-exclude-visible", isActive = true)
|
||||
val futureCharacter = saveCharacter("ai-exclude-future", isActive = true)
|
||||
val inactiveCreatorCharacter = saveCharacter("ai-exclude-inactive-creator", isActive = true).apply {
|
||||
creatorMember!!.isActive = false
|
||||
}
|
||||
val userCreatorCharacter = saveCharacter("ai-exclude-user-creator", isActive = true).apply {
|
||||
creatorMember!!.role = MemberRole.USER
|
||||
}
|
||||
val humanCreatorCharacter = saveCharacter("ai-exclude-human-creator", isActive = true).apply {
|
||||
creatorMember!!.memberKind = MemberKind.HUMAN
|
||||
}
|
||||
listOf(visibleCharacter, futureCharacter, inactiveCreatorCharacter, userCreatorCharacter, humanCreatorCharacter)
|
||||
.forEachIndexed { index, character ->
|
||||
val room = saveChatRoom("ai-exclude-room-$index")
|
||||
val userParticipant = saveParticipant(room, ParticipantType.USER, member = user)
|
||||
val characterParticipant = saveParticipant(room, ParticipantType.CHARACTER, character = character)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(room, userParticipant, "user-$index", true).id!!,
|
||||
windowStart.plusHours(1)
|
||||
)
|
||||
updateCreatedAt(
|
||||
"ChatMessage",
|
||||
saveMessage(room, characterParticipant, "character-$index", true).id!!,
|
||||
windowStart.plusHours(1)
|
||||
)
|
||||
}
|
||||
updateCreatedAt("ChatCharacter", visibleCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
|
||||
updateCreatedAt("ChatCharacter", futureCharacter.id!!, snapshotAt.plusSeconds(1))
|
||||
updateCreatedAt("ChatCharacter", inactiveCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
|
||||
updateCreatedAt("ChatCharacter", userCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
|
||||
updateCreatedAt("ChatCharacter", humanCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
|
||||
flushAndClear()
|
||||
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
|
||||
|
||||
assertEquals(listOf(visibleCharacter.id), snapshots.map { it.targetId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 스냅샷은 점수 동점이면 더 늦게 생성된 캐릭터를 먼저 반환하고 상위 20개만 저장 후보로 반환한다")
|
||||
fun shouldOrderAiCharacterSnapshotsByScoreThenCreatedAtDescWithTopTwentyLimit() {
|
||||
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||
val windowEndExclusive = snapshotAt.plusSeconds(1)
|
||||
val user = saveMember("ai-top-user", MemberRole.USER)
|
||||
val characters = (0..20).map { index ->
|
||||
val character = saveCharacter("ai-top-$index", isActive = true)
|
||||
val room = saveChatRoom("ai-top-room-$index")
|
||||
val participant = saveParticipant(room, ParticipantType.USER, member = user)
|
||||
saveParticipant(room, ParticipantType.CHARACTER, character = character)
|
||||
updateCreatedAt("ChatMessage", saveMessage(room, participant, "message-$index", true).id!!, windowStart.plusHours(1))
|
||||
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 6, 1, 0, 0).plusMinutes(index.toLong()))
|
||||
character
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 20)
|
||||
|
||||
assertEquals(20, snapshots.size)
|
||||
assertEquals(characters.drop(1).reversed().map { it.id }, snapshots.map { it.targetId })
|
||||
assertEquals(false, snapshots.any { it.targetId == characters.first().id })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 응원 스냅샷은 CHANNEL_DONATION 후원 금액과 후원 수만 집계한다")
|
||||
fun shouldFindCheerCreatorSnapshotsWithChannelDonationOnly() {
|
||||
|
||||
Reference in New Issue
Block a user