mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
profile: harden batch analysis and compaction
This commit is contained in:
@@ -4,7 +4,7 @@ import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import top.jie65535.mirai.data.ChatMessageRecord
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConversationProfileReducerTest {
|
||||
@@ -59,36 +59,36 @@ class ConversationProfileReducerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsEvidenceAuthoredByAnotherUser() {
|
||||
val failure = assertFailsWith<IllegalArgumentException> {
|
||||
ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch(),
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "关注本地大模型",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(2),
|
||||
)
|
||||
),
|
||||
summary = "关注本地大模型。",
|
||||
)
|
||||
fun skipsEvidenceAuthoredByAnotherUser() {
|
||||
val reductions = ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch(),
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "关注本地大模型",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(2),
|
||||
)
|
||||
),
|
||||
summary = "关注本地大模型。",
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(failure.message.orEmpty().contains("目标用户"))
|
||||
val userReduction = reductions.single { it.profile.userId == USER_A }
|
||||
assertTrue(userReduction.operations.isEmpty())
|
||||
assertTrue(userReduction.skippedOperations.single().contains("目标用户"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,12 +120,12 @@ class ConversationProfileReducerTest {
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.CONFIRM,
|
||||
itemId = oldItem.id,
|
||||
itemRef = "P1",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "关注 Kotlin 开发。",
|
||||
summary = "本批再次提到 Kotlin。",
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -138,14 +138,14 @@ class ConversationProfileReducerTest {
|
||||
assertEquals(2, updated.version)
|
||||
assertEquals("existing-item", updated.items.single().id)
|
||||
assertEquals(ProfileConfidence.MEDIUM, updated.items.single().confidence)
|
||||
assertTrue(
|
||||
ProfilePromptStore.buildConversationUserPrompt(profiles, batch(), USERS)
|
||||
.contains("[P:existing-item]")
|
||||
)
|
||||
assertEquals(existing.summary, updated.summary)
|
||||
val prompt = ProfilePromptStore.buildConversationUserPrompt(profiles, batch(), USERS)
|
||||
assertTrue(prompt.contains("[P1]"))
|
||||
assertFalse(prompt.contains(oldItem.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsMoreThanFourOperationsForOneUser() {
|
||||
fun acceptsMoreThanFourOperationsForOneUser() {
|
||||
val operations = (1..5).map { index ->
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
@@ -156,27 +156,126 @@ class ConversationProfileReducerTest {
|
||||
)
|
||||
}
|
||||
|
||||
val failure = assertFailsWith<IllegalArgumentException> {
|
||||
ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch(),
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = operations,
|
||||
summary = "包含过多事实。",
|
||||
)
|
||||
val reductions = ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch(),
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = operations,
|
||||
summary = "包含多项事实。",
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(failure.message.orEmpty().contains("超过 4 项"))
|
||||
assertEquals(5, reductions.single { it.profile.userId == USER_A }.operations.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergesDuplicateUserResultGroupsAndDeduplicatesOperations() {
|
||||
val operation = ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "日常使用 Kotlin 开发",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
|
||||
val reductions = ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch(),
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = listOf(operation),
|
||||
summary = "使用 Kotlin。",
|
||||
),
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = listOf(
|
||||
operation,
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.PREFERENCE,
|
||||
content = "偏好 Kotlin",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
evidenceRefs = listOf(1),
|
||||
),
|
||||
),
|
||||
summary = "使用并偏好 Kotlin。",
|
||||
),
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
val reduction = reductions.single { it.profile.userId == USER_A }
|
||||
assertEquals(2, reduction.operations.size)
|
||||
assertEquals(2, reduction.profile.items.size)
|
||||
assertEquals("使用并偏好 Kotlin。", reduction.profile.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresModelResultsForNonCandidateContextUsers() {
|
||||
val contextUser = 400L
|
||||
val batch = batch().copy(aliases = batch().aliases + (contextUser to "U4"))
|
||||
val reductions = ConversationProfileReducer.reduce(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch,
|
||||
eligibleUserIds = USERS,
|
||||
response = ConversationProfileModelResponse(
|
||||
users = listOf(
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U4",
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.INTEREST,
|
||||
content = "关注 Kotlin",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "关注 Kotlin。",
|
||||
),
|
||||
ConversationProfileUserResponse(
|
||||
userAlias = "U1",
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "日常使用 Kotlin 开发",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "日常使用 Kotlin 开发。",
|
||||
),
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(1, reductions.single { it.profile.userId == USER_A }.operations.size)
|
||||
assertTrue(reductions.single { it.profile.userId == USER_B }.operations.isEmpty())
|
||||
val prompt = ProfilePromptStore.buildConversationUserPrompt(
|
||||
profiles = USERS.associateWith(::emptyProfile),
|
||||
batch = batch,
|
||||
eligibleUserIds = USERS,
|
||||
)
|
||||
assertTrue(prompt.contains("候选用户别名: U1, U2"))
|
||||
}
|
||||
|
||||
private fun batch() = ConversationProfileBatch(
|
||||
|
||||
@@ -49,6 +49,7 @@ class ProfileHistoryReaderTest {
|
||||
}
|
||||
insert(TARGET, 10, 100, "目标发言一")
|
||||
insert(OTHER, 10, 110, "用于理解语境的回复")
|
||||
insert(OTHER, 10, 110, "同一秒的补充回复")
|
||||
insert(TARGET, 10, 130, "目标发言二")
|
||||
insert(TARGET, 20, 130, "同一秒的另一群发言")
|
||||
insert(OTHER, 20, 140, "后续上下文")
|
||||
@@ -110,6 +111,35 @@ class ProfileHistoryReaderTest {
|
||||
assertTrue(conversation.authoredTextCharsByUser.getValue(TARGET) > 0)
|
||||
assertTrue(conversation.messages.all { it.record.targetId == 10L })
|
||||
assertTrue(conversation.messages.all { it.record.time in 90 until 150 })
|
||||
|
||||
val groupBounds = assertNotNull(reader.findGroupTimeBounds(10))
|
||||
assertEquals(1, groupBounds.botId)
|
||||
assertEquals(100, groupBounds.startTime)
|
||||
assertEquals(201, groupBounds.endTime)
|
||||
val firstGroupBatch = assertNotNull(
|
||||
reader.loadNextConversationBatch(
|
||||
botId = groupBounds.botId,
|
||||
groupId = 10,
|
||||
startTime = groupBounds.startTime,
|
||||
snapshotEndTime = groupBounds.endTime,
|
||||
messageLimit = 2,
|
||||
maxMessageChars = 200,
|
||||
)
|
||||
)
|
||||
assertEquals(111, firstGroupBatch.endTime)
|
||||
assertEquals(listOf(100, 110, 110), firstGroupBatch.messages.map { it.record.time })
|
||||
val secondGroupBatch = assertNotNull(
|
||||
reader.loadNextConversationBatch(
|
||||
botId = groupBounds.botId,
|
||||
groupId = 10,
|
||||
startTime = firstGroupBatch.endTime,
|
||||
snapshotEndTime = groupBounds.endTime,
|
||||
messageLimit = 2,
|
||||
maxMessageChars = 200,
|
||||
)
|
||||
)
|
||||
assertEquals(201, secondGroupBatch.endTime)
|
||||
assertEquals(listOf(130, 200), secondGroupBatch.messages.map { it.record.time })
|
||||
} finally {
|
||||
directory.toFile().deleteRecursively()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package top.jie65535.mirai.profile
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ProfileModelResponseParsingTest {
|
||||
@Test
|
||||
fun ignoresLegacyFieldsAndNormalizesDisplayedEvidenceReferences() {
|
||||
val response = profileResponseJson.decodeFromString<ProfileModelResponse>(
|
||||
"""
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"action": "ADD",
|
||||
"item_id": null,
|
||||
"category": "interest",
|
||||
"content": "关注 Kotlin",
|
||||
"confidence": "low",
|
||||
"evidence_refs": [127, "e:128", "129"]
|
||||
}
|
||||
],
|
||||
"summary": "关注 Kotlin。",
|
||||
"unexpected": true
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertEquals(listOf(127, 128, 129), response.operations.single().evidenceRefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preservesMalformedEvidenceAsAnInvalidReferenceForOperationValidation() {
|
||||
val response = profileResponseJson.decodeFromString<ProfileModelResponse>(
|
||||
"""{"operations":[{"action":"CONFIRM","item_ref":"P1","evidence_refs":["invalid"]}]}"""
|
||||
)
|
||||
|
||||
assertEquals(listOf(0), response.operations.single().evidenceRefs)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UserProfileAnalysisServiceTest {
|
||||
@Test
|
||||
@@ -22,13 +23,14 @@ class UserProfileAnalysisServiceTest {
|
||||
assertEquals(1, model.calls)
|
||||
assertEquals(2, report.analyzedUsers)
|
||||
assertEquals(2, report.appliedOperations)
|
||||
assertEquals(0, report.skippedOperations)
|
||||
assertEquals("日常使用 Kotlin 开发", UserProfileStore.load(USER_A)?.items?.single()?.content)
|
||||
assertEquals("持续关注本地大模型", UserProfileStore.load(USER_B)?.items?.single()?.content)
|
||||
assertEquals(true, UserProfileStore.isConversationProcessed(INPUT_HASH))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsWronglyAttributedEvidenceWithoutPartialCommit() = withProfileStore {
|
||||
fun skipsWronglyAttributedEvidenceAndMarksConversationProcessed() = withProfileStore {
|
||||
val model = FakeConversationProfileModel {
|
||||
result(
|
||||
responseFor(
|
||||
@@ -39,14 +41,14 @@ class UserProfileAnalysisServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
assertFailsWith<IllegalStateException> {
|
||||
analyze(batch(), model, retryMax = 1)
|
||||
}
|
||||
val report = assertNotNull(analyze(batch(), model, retryMax = 1))
|
||||
|
||||
assertEquals(2, model.calls)
|
||||
assertNull(UserProfileStore.load(USER_A))
|
||||
assertNull(UserProfileStore.load(USER_B))
|
||||
assertFalse(UserProfileStore.isConversationProcessed(INPUT_HASH))
|
||||
assertEquals(1, model.calls)
|
||||
assertEquals(0, report.appliedOperations)
|
||||
assertEquals(1, report.skippedOperations)
|
||||
assertNotNull(UserProfileStore.load(USER_A))
|
||||
assertNotNull(UserProfileStore.load(USER_B))
|
||||
assertTrue(UserProfileStore.isConversationProcessed(INPUT_HASH))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package top.jie65535.mirai.profile
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UserProfileCompactorTest {
|
||||
@Test
|
||||
fun mergesRewritesAndDeletesWithinConservativeBounds() {
|
||||
val profile = profile()
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = mapOf("style-item" to supportStats(1)),
|
||||
response = ProfileCompactionResponse(
|
||||
merges = listOf(
|
||||
ProfileCompactionMerge(
|
||||
itemRefs = listOf("P1", "P2"),
|
||||
content = "关注并实际体验国内外大语言模型",
|
||||
)
|
||||
),
|
||||
rewrites = listOf(
|
||||
ProfileCompactionRewrite(
|
||||
itemRef = "P4",
|
||||
content = "从事使用 C# 的上位机开发",
|
||||
)
|
||||
),
|
||||
deletes = listOf(
|
||||
ProfileCompactionDelete(
|
||||
itemRef = "P3",
|
||||
reason = ProfileCompactionDeleteReason.OVER_SPECIFIC,
|
||||
)
|
||||
),
|
||||
summary = "该用户从事上位机开发,并关注大语言模型。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(2, plan.reduction.profile.items.size)
|
||||
assertEquals(profile.cursorTime, plan.reduction.profile.cursorTime)
|
||||
assertEquals(profile.snapshotEndTime, plan.reduction.profile.snapshotEndTime)
|
||||
assertEquals(profile.version + 1, plan.reduction.profile.version)
|
||||
assertEquals(1, plan.mergedGroups)
|
||||
assertEquals(1, plan.rewrittenItems)
|
||||
assertEquals(1, plan.deletedItems)
|
||||
assertEquals(mapOf("model-b" to "model-a"), plan.supportReassignments)
|
||||
assertEquals(4, plan.reduction.operations.size)
|
||||
val merged = plan.reduction.profile.items.single { it.category == ProfileCategory.INTEREST }
|
||||
assertEquals("model-a", merged.id)
|
||||
assertEquals(ProfileConfidence.LOW, merged.confidence)
|
||||
assertEquals("关注并实际体验国内外大语言模型", merged.content)
|
||||
assertFalse(plan.reduction.profile.items.any { it.id == "style-item" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skipsMergingDifferentCategories() {
|
||||
val profile = profile()
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
merges = listOf(
|
||||
ProfileCompactionMerge(
|
||||
itemRefs = listOf("P2", "P3"),
|
||||
content = "关注模型并使用口语化表达",
|
||||
)
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(plan.reduction.operations.isEmpty())
|
||||
assertEquals(profile.version, plan.reduction.profile.version)
|
||||
assertTrue(plan.skippedOperations.single().contains("同类别"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skipsDeletingSupportedOrHighItemsButAllowsLowSupportMedium() {
|
||||
val supported = delete(profile(), "P3", mapOf("style-item" to supportStats(2)))
|
||||
assertTrue(supported.reduction.operations.isEmpty())
|
||||
assertTrue(supported.skippedOperations.single().contains("2 次支持"))
|
||||
|
||||
val high = delete(profile(), "P4", mapOf("fact-item" to supportStats(1)))
|
||||
assertTrue(high.reduction.operations.isEmpty())
|
||||
assertTrue(high.skippedOperations.single().contains("high"))
|
||||
|
||||
val medium = delete(profile(), "P2", mapOf("model-b" to supportStats(1)))
|
||||
assertEquals(1, medium.deletedItems)
|
||||
assertFalse(medium.reduction.profile.items.any { it.id == "model-b" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactionPromptUsesTemporaryReferencesAndSupportCounts() {
|
||||
val profile = profile()
|
||||
val prompt = ProfilePromptStore.buildCompactionUserPrompt(
|
||||
profile,
|
||||
mapOf("model-a" to supportStats(3)),
|
||||
)
|
||||
|
||||
assertTrue(prompt.contains("[P1]"))
|
||||
assertTrue(prompt.contains("supports=3"))
|
||||
assertTrue(prompt.contains("item_range="))
|
||||
profile.items.forEach { assertFalse(prompt.contains(it.id)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun profilePromptsRequireAbsoluteDatesForTimeSensitiveFacts() {
|
||||
assertTrue(ProfilePromptStore.systemPrompt.contains("截至 YYYY-MM-DD"))
|
||||
assertTrue(ProfilePromptStore.conversationSystemPrompt.contains("截至 YYYY-MM-DD"))
|
||||
assertTrue(ProfilePromptStore.compactionSystemPrompt.contains("item_range"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun promptsDefineSummaryAsAWholeProfileSynthesis() {
|
||||
assertTrue(ProfilePromptStore.systemPrompt.contains("不是本批聊天摘要"))
|
||||
assertTrue(ProfilePromptStore.conversationSystemPrompt.contains("不是本批聊天摘要"))
|
||||
assertTrue(ProfilePromptStore.compactionSystemPrompt.contains("全部保留条目"))
|
||||
assertTrue(ProfilePromptStore.compactionSystemPrompt.contains("不得只描述最后编辑的条目"))
|
||||
assertTrue(ProfilePromptStore.compactionSystemPrompt.contains("即使没有条目操作也应重写"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removesRelatedGroupAliasFromRewrittenRelationship() {
|
||||
val relation = item(
|
||||
id = "relation-item",
|
||||
category = ProfileCategory.RELATIONSHIP_NOTE,
|
||||
content = "经常讨论技术方案",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
firstSeenAt = 10,
|
||||
).copy(relatedUserId = 200)
|
||||
val profile = profile().copy(items = listOf(relation))
|
||||
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
rewrites = listOf(
|
||||
ProfileCompactionRewrite("P1", "经常与 R1 讨论技术方案并互相提供建议")
|
||||
),
|
||||
summary = profile.summary,
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals("经常与对方讨论技术方案并互相提供建议", plan.reduction.profile.items.single().content)
|
||||
assertFalse(plan.reduction.profile.items.single().content.contains("R1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresNoopRewriteWithoutChangingVersion() {
|
||||
val profile = profile()
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
rewrites = listOf(
|
||||
ProfileCompactionRewrite("P1", "关注 Qwen、DeepSeek 等模型")
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(plan.reduction.operations.isEmpty())
|
||||
assertEquals(0, plan.rewrittenItems)
|
||||
assertEquals(profile.version, plan.reduction.profile.version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allowsSummaryOnlyCompactionAcrossTheWholeProfile() {
|
||||
val profile = profile().copy(summary = "最近一次只讨论了 C#。")
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
summary = "该用户从事上位机开发,关注大语言模型,表达直接。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v3",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(plan.reduction.operations.isEmpty())
|
||||
assertEquals(profile.version + 1, plan.reduction.profile.version)
|
||||
assertEquals(
|
||||
"该用户从事上位机开发,关注大语言模型,表达直接。",
|
||||
plan.reduction.profile.summary,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processesMoreThanTwelveOperationsOfOneType() {
|
||||
val items = (1..15).map { index ->
|
||||
item(
|
||||
id = "item-$index",
|
||||
category = ProfileCategory.EXPRESSION_STYLE,
|
||||
content = "旧表达方式 $index",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
firstSeenAt = index,
|
||||
)
|
||||
}
|
||||
val profile = profile().copy(items = items)
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
rewrites = items.indices.map { index ->
|
||||
ProfileCompactionRewrite("P${index + 1}", "概括后的表达方式 ${index + 1}")
|
||||
},
|
||||
summary = profile.summary,
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(15, plan.rewrittenItems)
|
||||
assertTrue(plan.skippedOperations.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsOldSummaryWhenNewSummaryContainsTemporaryReference() {
|
||||
val profile = profile()
|
||||
val plan = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = emptyMap(),
|
||||
response = ProfileCompactionResponse(
|
||||
rewrites = listOf(ProfileCompactionRewrite("P1", "关注多个国内外大语言模型")),
|
||||
summary = "P1 已经概括多个模型兴趣。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(profile.summary, plan.reduction.profile.summary)
|
||||
assertTrue(plan.skippedOperations.single().contains("临时画像条目编号"))
|
||||
}
|
||||
|
||||
private fun delete(
|
||||
profile: UserProfileSnapshot,
|
||||
itemRef: String,
|
||||
stats: Map<String, ProfileItemSupportStats>,
|
||||
) = UserProfileCompactor.reduce(
|
||||
current = profile,
|
||||
supportStats = stats,
|
||||
response = ProfileCompactionResponse(
|
||||
deletes = listOf(
|
||||
ProfileCompactionDelete(itemRef, ProfileCompactionDeleteReason.ONE_OFF)
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "compact-v1",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
private fun profile() = UserProfileSnapshot(
|
||||
userId = 100,
|
||||
summary = "关注模型并从事软件开发。",
|
||||
version = 8,
|
||||
cursorTime = 300,
|
||||
snapshotEndTime = 1_000,
|
||||
reliable = true,
|
||||
items = listOf(
|
||||
item("model-a", ProfileCategory.INTEREST, "关注 Qwen、DeepSeek 等模型", ProfileConfidence.LOW, 10),
|
||||
item("model-b", ProfileCategory.INTEREST, "关注开源大模型部署和性能", ProfileConfidence.MEDIUM, 20),
|
||||
item("style-item", ProfileCategory.EXPRESSION_STYLE, "会使用“我去”表示惊讶", ProfileConfidence.LOW, 30),
|
||||
item("fact-item", ProfileCategory.NOTABLE_FACT, "从事上位机开发,使用 C#", ProfileConfidence.HIGH, 40),
|
||||
),
|
||||
)
|
||||
|
||||
private fun item(
|
||||
id: String,
|
||||
category: ProfileCategory,
|
||||
content: String,
|
||||
confidence: ProfileConfidence,
|
||||
firstSeenAt: Int,
|
||||
) = UserProfileItem(
|
||||
id = id,
|
||||
category = category,
|
||||
content = content,
|
||||
confidence = confidence,
|
||||
firstSeenAt = firstSeenAt,
|
||||
lastConfirmedAt = firstSeenAt + 100,
|
||||
)
|
||||
|
||||
private fun supportStats(count: Int) = ProfileItemSupportStats(count, 1, 2)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class UserProfileContextRendererTest {
|
||||
snapshotEndTime = 2,
|
||||
reliable = true,
|
||||
items = listOf(
|
||||
relationship("visible", 200, "经常互相讨论技术方案"),
|
||||
relationship("visible", 200, "经常与 U12 讨论技术方案"),
|
||||
relationship("hidden", 300, "曾共同讨论游戏"),
|
||||
),
|
||||
)
|
||||
@@ -38,8 +38,9 @@ class UserProfileContextRendererTest {
|
||||
|
||||
assertContains(rendered, "小明代号(100)")
|
||||
assertContains(rendered, "好感度+12")
|
||||
assertContains(rendered, "长期认识:长期关注 Kotlin 开发")
|
||||
assertContains(rendered, "与小王:经常互相讨论技术方案")
|
||||
assertContains(rendered, "画像认识:长期关注 Kotlin 开发")
|
||||
assertContains(rendered, "与小王:经常与对方讨论技术方案")
|
||||
assertFalse(rendered.contains("U12"))
|
||||
assertFalse(rendered.contains("小李"))
|
||||
assertFalse(rendered.contains("曾共同讨论游戏"))
|
||||
}
|
||||
@@ -59,7 +60,7 @@ class UserProfileContextRendererTest {
|
||||
assertContains(rendered, "小明(100)")
|
||||
assertContains(rendered, "好感度-8")
|
||||
assertContains(rendered, "主观印象:偶尔喜欢抬杠")
|
||||
assertFalse(rendered.contains("长期认识:"))
|
||||
assertFalse(rendered.contains("画像认识:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,4 +89,5 @@ class UserProfileContextRendererTest {
|
||||
firstSeenAt = 1,
|
||||
lastConfirmedAt = 1,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import top.jie65535.mirai.data.ChatMessageRecord
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UserProfileReducerTest {
|
||||
@@ -43,32 +43,33 @@ class UserProfileReducerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsItemSupportedOnlyByAnotherUser() {
|
||||
fun skipsItemSupportedOnlyByAnotherUser() {
|
||||
val batch = batchOf(message(ref = 1, fromId = OTHER, text = "我平时会写 Kotlin"))
|
||||
|
||||
val failure = assertFailsWith<IllegalArgumentException> {
|
||||
UserProfileReducer.reduce(
|
||||
current = emptyProfile(),
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "从事 Kotlin 开发",
|
||||
confidence = ProfileConfidence.HIGH,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "从事 Kotlin 开发。",
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = emptyProfile(),
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "从事 Kotlin 开发",
|
||||
confidence = ProfileConfidence.HIGH,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
}
|
||||
summary = "从事 Kotlin 开发。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertTrue(failure.message.orEmpty().contains("目标用户"))
|
||||
assertTrue(reduction.operations.isEmpty())
|
||||
assertTrue(reduction.profile.items.isEmpty())
|
||||
assertTrue(reduction.skippedOperations.single().contains("目标用户"))
|
||||
assertEquals(batch.endTime, reduction.profile.cursorTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,12 +121,189 @@ class UserProfileReducerTest {
|
||||
assertEquals(1, reduction.profile.version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolvesPromptLocalItemReferenceWithoutExposingStoredId() {
|
||||
val item = existingItem()
|
||||
val current = emptyProfile().copy(items = listOf(item), reliable = true)
|
||||
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我现在仍然关注 Kotlin"))
|
||||
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = current,
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.CONFIRM,
|
||||
itemRef = "P1",
|
||||
confidence = ProfileConfidence.HIGH,
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "TARGET 仍然关注 Kotlin。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(item.id, reduction.profile.items.single().id)
|
||||
assertEquals(ProfileConfidence.HIGH, reduction.profile.items.single().confidence)
|
||||
assertEquals("该用户仍然关注 Kotlin。", reduction.profile.summary)
|
||||
val prompt = ProfilePromptStore.buildUserPrompt(current, batch)
|
||||
assertTrue(prompt.contains("[P1]"))
|
||||
assertFalse(prompt.contains(item.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsComprehensiveSummaryWhenBatchOnlyConfirmsOneItem() {
|
||||
val interest = existingItem()
|
||||
val work = UserProfileItem(
|
||||
id = "work-item",
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "从事上位机开发",
|
||||
confidence = ProfileConfidence.HIGH,
|
||||
firstSeenAt = 70,
|
||||
lastConfirmedAt = 70,
|
||||
)
|
||||
val current = emptyProfile().copy(
|
||||
summary = "从事上位机开发,并持续关注 Kotlin 生态。",
|
||||
reliable = true,
|
||||
items = listOf(interest, work),
|
||||
)
|
||||
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我还在关注 Kotlin"))
|
||||
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = current,
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.CONFIRM,
|
||||
itemRef = "P1",
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "仍在关注 Kotlin。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(current.summary, reduction.profile.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skipsUnknownPromptLocalItemReference() {
|
||||
val current = emptyProfile().copy(items = listOf(existingItem()), reliable = true)
|
||||
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我仍然关注 Kotlin"))
|
||||
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = current,
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.CONFIRM,
|
||||
itemRef = "P2",
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "关注 Kotlin。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(current.items, reduction.profile.items)
|
||||
assertTrue(reduction.operations.isEmpty())
|
||||
assertTrue(reduction.skippedOperations.single().contains("P2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appliesSafeOperationAndSkipsInvalidEvidenceWithoutRewritingSummary() {
|
||||
val current = emptyProfile().copy(summary = "原摘要")
|
||||
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我平时会写 Kotlin"))
|
||||
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = current,
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.NOTABLE_FACT,
|
||||
content = "日常使用 Kotlin 开发",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
evidenceRefs = listOf(1),
|
||||
),
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.INTEREST,
|
||||
content = "关注本地大模型",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
evidenceRefs = listOf(323),
|
||||
),
|
||||
),
|
||||
summary = "使用 Kotlin 并关注本地大模型。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals(1, reduction.operations.size)
|
||||
assertEquals("日常使用 Kotlin 开发", reduction.profile.items.single().content)
|
||||
assertTrue(reduction.skippedOperations.single().contains("e:323"))
|
||||
assertEquals("原摘要", reduction.profile.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removesTemporaryAliasesFromPersistedRelationshipText() {
|
||||
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "先找到下家再离职"))
|
||||
|
||||
val reduction = UserProfileReducer.reduce(
|
||||
current = emptyProfile(),
|
||||
batch = batch,
|
||||
response = ProfileModelResponse(
|
||||
operations = listOf(
|
||||
ProfileModelOperation(
|
||||
action = ProfileOperationAction.ADD,
|
||||
category = ProfileCategory.RELATIONSHIP_NOTE,
|
||||
content = "TARGET 在 U1 提及离职时会建议 U1 先找到下家",
|
||||
confidence = ProfileConfidence.LOW,
|
||||
relatedUserAlias = "U1",
|
||||
evidenceRefs = listOf(1),
|
||||
)
|
||||
),
|
||||
summary = "TARGET 会给 U1 提供务实建议。",
|
||||
),
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
summaryMaxLength = 500,
|
||||
)
|
||||
|
||||
assertEquals("本人在对方提及离职时会建议对方先找到下家", reduction.profile.items.single().content)
|
||||
assertEquals("该用户会给其他用户提供务实建议。", reduction.profile.summary)
|
||||
assertFalse(reduction.profile.items.single().content.contains("U1"))
|
||||
}
|
||||
|
||||
private fun emptyProfile() = UserProfileSnapshot(
|
||||
userId = TARGET,
|
||||
cursorTime = 100,
|
||||
snapshotEndTime = 1_000,
|
||||
)
|
||||
|
||||
private fun existingItem() = UserProfileItem(
|
||||
id = "b287070d-b7c0-4d50-a18c-fa8348932048",
|
||||
category = ProfileCategory.INTEREST,
|
||||
content = "关注 Kotlin 开发",
|
||||
confidence = ProfileConfidence.MEDIUM,
|
||||
firstSeenAt = 80,
|
||||
lastConfirmedAt = 80,
|
||||
)
|
||||
|
||||
private fun batchOf(vararg messages: ProfilePromptMessage) = ProfileHistoryBatch(
|
||||
userId = TARGET,
|
||||
startTime = 100,
|
||||
|
||||
@@ -136,6 +136,14 @@ class UserProfileStoreTest {
|
||||
}
|
||||
}
|
||||
assertTrue("source" in columns)
|
||||
val tables = connection.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT name FROM sqlite_master WHERE type = 'table'").use { results ->
|
||||
buildSet {
|
||||
while (results.next()) add(results.getString("name"))
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("profile_group_cursor" in tables)
|
||||
val version = connection.createStatement().use { statement ->
|
||||
statement.executeQuery(
|
||||
"SELECT value FROM user_profile_meta WHERE key = 'schema_version'"
|
||||
@@ -144,7 +152,7 @@ class UserProfileStoreTest {
|
||||
results.getString(1)
|
||||
}
|
||||
}
|
||||
assertEquals("2", version)
|
||||
assertEquals("3", version)
|
||||
}
|
||||
} finally {
|
||||
UserProfileStore.close()
|
||||
@@ -152,6 +160,31 @@ class UserProfileStoreTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun persistsGroupAnalysisCursor() {
|
||||
val directory = Files.createTempDirectory("jchatgpt-profile-group-cursor-test-")
|
||||
try {
|
||||
UserProfileStore.init(directory.toFile())
|
||||
val cursor = GroupProfileCursor(
|
||||
botId = 1,
|
||||
groupId = 300,
|
||||
cursorTime = 200,
|
||||
snapshotEndTime = 1_000,
|
||||
updatedAt = 123,
|
||||
)
|
||||
|
||||
UserProfileStore.saveGroupCursor(cursor)
|
||||
assertEquals(cursor, UserProfileStore.loadGroupCursor(1, 300))
|
||||
|
||||
val advanced = cursor.copy(cursorTime = 400, updatedAt = 456)
|
||||
UserProfileStore.saveGroupCursor(advanced)
|
||||
assertEquals(advanced, UserProfileStore.loadGroupCursor(1, 300))
|
||||
} finally {
|
||||
UserProfileStore.close()
|
||||
directory.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun commitsAllConversationProfilesInOneTransactionAndCountsUsageOnce() {
|
||||
val directory = Files.createTempDirectory("jchatgpt-profile-conversation-commit-test-")
|
||||
@@ -198,6 +231,81 @@ class UserProfileStoreTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun commitsCompactionWithoutNewEvidenceAndReassignsExistingSupport() {
|
||||
val directory = Files.createTempDirectory("jchatgpt-profile-compaction-commit-test-")
|
||||
try {
|
||||
UserProfileStore.init(directory.toFile())
|
||||
val batch = batchFor(100, "initial-hash")
|
||||
val first = itemFor("item-1")
|
||||
val second = itemFor("item-2").copy(content = "关注 Kotlin 生态")
|
||||
val initialProfile = UserProfileSnapshot(
|
||||
userId = 100,
|
||||
summary = "关注 Kotlin。",
|
||||
version = 1,
|
||||
cursorTime = 200,
|
||||
snapshotEndTime = 1_000,
|
||||
reliable = true,
|
||||
model = "test-model",
|
||||
promptVersion = "test-prompt",
|
||||
items = listOf(first, second),
|
||||
)
|
||||
UserProfileStore.commit(
|
||||
reduction = ProfileReduction(
|
||||
profile = initialProfile,
|
||||
operations = listOf(
|
||||
applied(first, ProfileOperationAction.ADD, listOf(1)),
|
||||
applied(second, ProfileOperationAction.ADD, listOf(1)),
|
||||
),
|
||||
),
|
||||
batch = batch,
|
||||
usage = ProfileTokenUsage(),
|
||||
)
|
||||
|
||||
val merged = first.copy(content = "关注 Kotlin 及其生态")
|
||||
val compactedProfile = initialProfile.copy(
|
||||
summary = "关注 Kotlin 及其生态。",
|
||||
version = 2,
|
||||
items = listOf(merged),
|
||||
)
|
||||
val plan = ProfileCompactionPlan(
|
||||
reduction = ProfileReduction(
|
||||
profile = compactedProfile,
|
||||
operations = listOf(
|
||||
applied(merged, ProfileOperationAction.UPDATE, emptyList()),
|
||||
applied(second, ProfileOperationAction.DELETE, emptyList()),
|
||||
),
|
||||
),
|
||||
supportReassignments = mapOf(second.id to first.id),
|
||||
mergedGroups = 1,
|
||||
rewrittenItems = 0,
|
||||
deletedItems = 0,
|
||||
)
|
||||
UserProfileStore.commitCompaction(
|
||||
plan = plan,
|
||||
batch = ProfileHistoryBatch(
|
||||
userId = 100,
|
||||
startTime = 150,
|
||||
endTime = 151,
|
||||
messages = emptyList(),
|
||||
aliases = mapOf(100L to "TARGET"),
|
||||
inputHash = "compact-hash",
|
||||
),
|
||||
usage = ProfileTokenUsage(50, 10, 0),
|
||||
)
|
||||
|
||||
val loaded = assertNotNull(UserProfileStore.load(100))
|
||||
assertEquals(200, loaded.cursorTime)
|
||||
assertEquals(listOf(merged), loaded.items)
|
||||
assertEquals(2, UserProfileStore.loadSupportStats(100).getValue(first.id).count)
|
||||
assertTrue(second.id !in UserProfileStore.loadSupportStats(100))
|
||||
assertNotNull(UserProfileStore.lastRevisionAt(100, ProfileRevisionSource.COMPACTION))
|
||||
} finally {
|
||||
UserProfileStore.close()
|
||||
directory.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun batchFor(userId: Long, inputHash: String) = ProfileHistoryBatch(
|
||||
userId = userId,
|
||||
startTime = 100,
|
||||
@@ -231,4 +339,18 @@ class UserProfileStoreTest {
|
||||
firstSeenAt = 150,
|
||||
lastConfirmedAt = 150,
|
||||
)
|
||||
|
||||
private fun applied(
|
||||
item: UserProfileItem,
|
||||
action: ProfileOperationAction,
|
||||
evidenceRefs: List<Int>,
|
||||
) = AppliedProfileOperation(
|
||||
action = action,
|
||||
itemId = item.id,
|
||||
category = item.category,
|
||||
content = item.content,
|
||||
confidence = item.confidence,
|
||||
relatedUserId = item.relatedUserId,
|
||||
evidenceRefs = evidenceRefs,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user