ProjectAcceptanceTest.kt
23.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
package ai.lemon.phoneuse
import ai.lemon.phoneuse.core.AgentEvent
import ai.lemon.phoneuse.core.ClipboardEvent
import ai.lemon.phoneuse.core.IntentEvent
import ai.lemon.phoneuse.core.NotificationEvent
import ai.lemon.phoneuse.core.UiEvent
import ai.lemon.phoneuse.core.event.Event
import ai.lemon.phoneuse.core.event.EventType
import ai.lemon.phoneuse.dataextraction.mail.MailConnector
import ai.lemon.phoneuse.dataextraction.notes.NotesConnector
import ai.lemon.phoneuse.dataextraction.telegram.TelegramConnector
import ai.lemon.phoneuse.dataextraction.wechat.WeChatConnector
import ai.lemon.phoneuse.eventbus.EventBus
import ai.lemon.phoneuse.fridabridge.ckn.CknPullCache
import ai.lemon.phoneuse.fridabridge.ckn.CknPullRecord
import ai.lemon.phoneuse.fridabridge.ckn.CknSanitizer
import ai.lemon.phoneuse.fridabridge.ckn.CknUploadRecord
import ai.lemon.phoneuse.fridabridge.ckn.HookStep
import ai.lemon.phoneuse.fridabridge.version.AppVersion
import ai.lemon.phoneuse.fridabridge.version.HookConfigurationStore
import ai.lemon.phoneuse.memorygraph.GraphDatabase
import ai.lemon.phoneuse.memorygraph.GraphEdge
import ai.lemon.phoneuse.memorygraph.GraphNode
import ai.lemon.phoneuse.memorygraph.nlquery.NaturalLanguageQueryParser
import ai.lemon.phoneuse.memorygraph.schema.NodeType
import ai.lemon.phoneuse.memorygraph.schema.RelationType
import ai.lemon.phoneuse.mcpserver.mcp.McpToolRegistry
import ai.lemon.phoneuse.mcpserver.planner.ActionSequencer
import ai.lemon.phoneuse.mcpserver.planner.ActionStep
import ai.lemon.phoneuse.mcpserver.planner.HermesTask
import ai.lemon.phoneuse.mcpserver.planner.TaskDecomposer
import ai.lemon.phoneuse.mcpserver.protocol.ActionType
import ai.lemon.phoneuse.mcpserver.tools.SendMessageTool
import ai.lemon.phoneuse.mcpserver.tools.registerBuiltinTools
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Global acceptance test — validates the completion of all 6 phases of
* Agent Phone Runtime against the original project goals.
*
* Runs on a real Android device (Pixel or emulator). Each test method
* corresponds to one verifiable acceptance criterion from TASKS.md.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class ProjectAcceptanceTest {
private lateinit var context: Context
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
}
// =====================================================================
// Phase 1 — Android Event Bus
// Goal: 采集系统级事件,输出统一 Event Stream
// =====================================================================
@Test
fun p1_eventType_hasAllNineTypes() {
val names = EventType.values().map { it.name }.toSet()
val expected = setOf(
"MESSAGE_RECEIVED", "MESSAGE_SENT", "NOTIFICATION_RECEIVED",
"APP_FOREGROUND", "APP_BACKGROUND", "CLIPBOARD_CHANGED",
"INTENT_RECEIVED", "UI_TREE_CHANGED", "WINDOW_CHANGED"
)
assertEquals("EventType must define exactly 9 event types", expected, names)
}
@Test
fun p1_agentEvent_sealedHierarchy_fourConcreteSubtypes() {
// Verify the four concrete AgentEvent subtypes exist and can be instantiated
val ui: AgentEvent = UiEvent(
timestampMs = 1000L, sourcePackage = "com.tencent.mm",
eventType = "TYPE_VIEW_TEXT_CHANGED", className = "EditText",
text = "hello", contentDescription = null
)
val notification: AgentEvent = NotificationEvent(
timestampMs = 2000L, sourcePackage = "com.tencent.mm",
title = "张三", body = "明天开会推迟", category = "msg", isOngoing = false
)
val clipboard: AgentEvent = ClipboardEvent(
timestampMs = 3000L, sourcePackage = "ai.lemon.phoneuse",
text = "copied text"
)
val intent: AgentEvent = IntentEvent(
timestampMs = 4000L, sourcePackage = "com.tencent.mm",
action = "android.intent.action.MAIN", dataUri = null
)
assertEquals("com.tencent.mm", ui.sourcePackage)
assertEquals("张三", (notification as NotificationEvent).title)
assertEquals("copied text", (clipboard as ClipboardEvent).text)
assertNull((intent as IntentEvent).dataUri)
}
@Test
fun p1_event_serializesToAndFromJson() {
val event = Event(
id = "evt-001",
type = EventType.MESSAGE_RECEIVED,
app = "com.tencent.mm",
timestamp = 1_700_000_000_000L,
payload = mapOf("sender" to "张三", "content" to "你好")
)
val json = Json.encodeToString(event)
assertTrue("JSON must contain event type", json.contains("MESSAGE_RECEIVED"))
assertTrue("JSON must contain app package", json.contains("com.tencent.mm"))
assertTrue("JSON must contain sender", json.contains("张三"))
val decoded = Json.decodeFromString<Event>(json)
assertEquals(event.id, decoded.id)
assertEquals(event.type, decoded.type)
assertEquals(event.app, decoded.app)
}
@Test
fun p1_eventBus_emitAndCollect() = runBlocking {
val results = mutableListOf<AgentEvent>()
val job = launch {
EventBus.events
.take(1)
.collect { results.add(it) }
}
// Yield to let collector subscribe, then emit
delay(50)
val evt = ClipboardEvent(
timestampMs = System.currentTimeMillis(),
sourcePackage = "ai.lemon.phoneuse.test",
text = "acceptance-test-payload"
)
EventBus.emit(evt)
withTimeout(2_000) { job.join() }
assertEquals(1, results.size)
assertEquals("acceptance-test-payload", (results[0] as ClipboardEvent).text)
}
// =====================================================================
// Phase 2 — Data Extraction Engine
// Goal: 绕过截图,直接读取 App 结构化数据
// =====================================================================
@Test
fun p2_wechatConnector_metadataCorrect() {
val connector = WeChatConnector(context.cacheDir)
assertEquals("com.tencent.mm", connector.appPackage)
// isAvailable() must not throw even without root access
val available = connector.isAvailable()
// On a test device without WeChat it returns false — that's fine
assertFalse("isAvailable() should return false without root+WeChat", available)
}
@Test
fun p2_telegramConnector_metadataCorrect() {
val connector = TelegramConnector(context.cacheDir)
assertEquals("org.telegram.messenger", connector.appPackage)
connector.isAvailable() // must not throw
}
@Test
fun p2_mailConnector_instantiates() {
val connector = MailConnector(context.contentResolver, context.cacheDir)
assertNotNull(connector)
connector.isAvailable() // must not throw
}
@Test
fun p2_notesConnector_instantiates() {
val connector = NotesConnector(context.contentResolver, context.cacheDir)
assertNotNull(connector)
connector.isAvailable() // must not throw
}
// =====================================================================
// Phase 3 — Memory Graph
// Goal: 所有 App 数据进入统一 Memory Graph,Agent 查询图谱
// =====================================================================
@Test
fun p3_nodeType_hasAllSevenTypes() {
val names = NodeType.values().map { it.name }.toSet()
assertEquals(
"NodeType must have exactly 7 values",
setOf("PERSON", "CONVERSATION", "MESSAGE", "CALENDAR_EVENT", "TASK", "DOCUMENT", "LOCATION"),
names
)
}
@Test
fun p3_relationType_hasAllSevenRelations() {
val names = RelationType.values().map { it.name }.toSet()
assertEquals(
"RelationType must have exactly 7 values",
setOf("SENT_BY", "PARTICIPATED_IN", "ASSIGNED_TO", "RELATED_TO", "LOCATED_AT", "PART_OF", "CREATED_BY"),
names
)
}
@Test
fun p3_graphDatabase_inMemoryWriteAndRead() = runBlocking {
val db = GraphDatabase.createInMemory(context)
try {
val node = GraphNode(
id = "person-zhangsan",
type = NodeType.PERSON.name,
label = "张三",
appSource = "wechat",
createdAt = 1_700_000_000_000L,
updatedAt = 1_700_000_000_000L
)
db.nodeDao().insertNode(node)
val read = db.nodeDao().findById("person-zhangsan")
assertNotNull("Inserted node must be retrievable", read)
assertEquals("张三", read!!.label)
assertEquals(NodeType.PERSON.name, read.type)
assertEquals("wechat", read.appSource)
} finally {
db.close()
}
}
@Test
fun p3_graphDatabase_personConversationEdgeTraversal() = runBlocking {
val db = GraphDatabase.createInMemory(context)
try {
val person = GraphNode(
id = "p1", type = NodeType.PERSON.name, label = "李四",
appSource = "telegram", createdAt = 1000L, updatedAt = 1000L
)
val conv = GraphNode(
id = "c1", type = NodeType.CONVERSATION.name, label = "与李四的对话",
appSource = "telegram", createdAt = 1000L, updatedAt = 1000L
)
db.nodeDao().insertNodes(listOf(person, conv))
val edge = GraphEdge(
id = "e1",
fromId = "p1",
toId = "c1",
relation = RelationType.PARTICIPATED_IN.name,
createdAt = 1000L
)
db.edgeDao().insertEdge(edge)
val neighbors = db.nodeDao().findNeighbors("p1")
assertEquals(1, neighbors.size)
assertEquals("c1", neighbors[0].id)
} finally {
db.close()
}
}
@Test
fun p3_nlParser_extractsPersonFromChineseQuery() {
val parser = NaturalLanguageQueryParser()
val intent = parser.parse("我和张三上次聊了什么")
assertEquals("张三", intent.extractedPerson)
assertNotNull("Should infer CONVERSATION type from 聊", intent.inferredNodeType)
}
@Test
fun p3_nlParser_confidence_highForRichQuery() {
val parser = NaturalLanguageQueryParser()
val richQuery = parser.parse("张三今天发给我的消息")
val emptyQuery = parser.parse("abc")
assertTrue(
"Rich query confidence must exceed sparse query confidence",
richQuery.confidence > emptyQuery.confidence
)
}
@Test
fun p3_nlParser_timeRange_todayNotNull() {
val parser = NaturalLanguageQueryParser()
val intent = parser.parse("今天所有的消息")
assertNotNull("今天 keyword must produce a time range", intent.extractedTimeRange)
}
// =====================================================================
// Phase 4 — Hermes Runtime Integration
// Goal: 接入 Planner / Executor / Reflection,Agent 能执行跨 App 任务
// =====================================================================
@Test
fun p4_taskDecomposer_parsesFullChineseInstruction() {
val decomposer = TaskDecomposer()
val steps = decomposer.decompose(
HermesTask(intent = "帮我给张三发一条微信说明天开会推迟")
)
assertEquals(1, steps.size)
assertEquals(ActionType.SEND_MESSAGE, steps[0].actionType)
assertEquals("张三", steps[0].params["recipient"])
assertTrue(
"message param must contain 开会推迟",
steps[0].params["message"].toString().contains("开会推迟")
)
}
@Test
fun p4_taskDecomposer_createEventInstruction() {
val decomposer = TaskDecomposer()
val steps = decomposer.decompose(
HermesTask(intent = "明天下午三点创建一个项目讨论会议")
)
assertEquals(1, steps.size)
assertEquals(ActionType.CREATE_EVENT, steps[0].actionType)
assertEquals("calendar", steps[0].targetApp)
}
@Test
fun p4_taskDecomposer_createTaskInstruction() {
val decomposer = TaskDecomposer()
val steps = decomposer.decompose(
HermesTask(intent = "添加待办:周五之前提交报告")
)
assertEquals(1, steps.size)
assertEquals(ActionType.CREATE_TASK, steps[0].actionType)
assertEquals("todo", steps[0].targetApp)
}
@Test
fun p4_taskDecomposer_unknownIntentFallsBackToSearchMemory() {
val decomposer = TaskDecomposer()
val steps = decomposer.decompose(HermesTask(intent = "xyzzy unknown command"))
assertEquals(1, steps.size)
assertEquals(ActionType.SEARCH_MEMORY, steps[0].actionType)
}
@Test
fun p4_actionSequencer_ordersSearchBeforeWrite() {
val sequencer = ActionSequencer()
val steps = listOf(
ActionStep(ActionType.SEND_MESSAGE, null, mapOf("recipient" to "张三"), order = 1),
ActionStep(ActionType.SEARCH_MEMORY, null, mapOf("query" to "张三联系方式"), order = 0)
)
val plan = sequencer.sequence(steps)
val actionTypes = plan.steps.map { it.actionType }
val searchIndex = actionTypes.indexOf(ActionType.SEARCH_MEMORY)
val sendIndex = actionTypes.indexOf(ActionType.SEND_MESSAGE)
assertTrue(
"SEARCH_MEMORY must be sequenced before SEND_MESSAGE",
searchIndex < sendIndex
)
}
// =====================================================================
// Phase 5 — Semantic MCP
// Goal: 对外暴露统一语义 API,Agent 不关心底层 App 实现
// =====================================================================
@Test
fun p5_mcpToolRegistry_registerAndLookup() {
val registry = McpToolRegistry()
registry.register(SendMessageTool)
assertNotNull(registry.get("send_message"))
assertEquals(1, registry.size)
}
@Test
fun p5_mcpToolRegistry_allFiveToolsRegisteredViaContext() {
val registry = McpToolRegistry.withDefaults()
registry.registerBuiltinTools(context)
val toolNames = registry.getAll().map { it.name }.toSet()
assertTrue("send_message must be registered", "send_message" in toolNames)
assertTrue("create_event must be registered", "create_event" in toolNames)
assertTrue("create_task must be registered", "create_task" in toolNames)
assertTrue("read_document must be registered", "read_document" in toolNames)
assertTrue("All tools must be registered", registry.size >= 4)
}
@Test
fun p5_mcpTool_inputSchema_isValidJsonSchemaObject() {
val registry = McpToolRegistry.withDefaults()
registry.registerBuiltinTools(context)
for (tool in registry.getAll()) {
val typeField = tool.inputSchema["type"]
assertNotNull("Tool '${tool.name}' inputSchema must have 'type' field", typeField)
assertEquals(
"Tool '${tool.name}' inputSchema.type must be 'object'",
"object",
typeField!!.toString().trim('"')
)
}
}
@Test
fun p5_sendMessageTool_executesWithValidParams() = runBlocking {
val params = buildJsonObject {
put("to", "张三")
put("content", "明天开会推迟")
}
val result = SendMessageTool.execute(params)
assertNotNull(result)
assertFalse("send_message with valid params must not return isError=true with missing deps",
result.content.isEmpty())
}
@Test
fun p5_mcpToolRegistry_unknownToolReturnsError() = runBlocking {
val registry = McpToolRegistry()
val result = registry.execute("non_existent_tool", buildJsonObject {})
assertTrue("Unknown tool call must set isError=true", result.isError)
}
// =====================================================================
// Phase 6 — Frida Runtime
// Goal: 针对高价值 App 实现深度操作能力 + Collective Knowledge Network
// =====================================================================
@Test
fun p6_hookConfigurationStore_hasFourBuiltinConfigs() {
val store = HookConfigurationStore()
// Verify all four expected configs resolve correctly via public API
val wechat1 = store.getConfig("com.tencent.mm", AppVersion("com.tencent.mm", "8.0.40", 1L, 0L))
val wechat2 = store.getConfig("com.tencent.mm", AppVersion("com.tencent.mm", "8.0.47", 1L, 0L))
val telegram = store.getConfig("org.telegram.messenger", AppVersion("org.telegram.messenger", "10.0.0", 1L, 0L))
val alipay = store.getConfig("com.eg.android.AlipayGphone", AppVersion("com.eg.android.AlipayGphone", "10.5.0", 1L, 0L))
assertEquals(
"Must find 4 distinct configs (WeChat x2, Telegram x1, Alipay x1)",
4, listOfNotNull(wechat1, wechat2, telegram, alipay).size
)
}
@Test
fun p6_hookConfigurationStore_coversThreeTargetApps() {
val store = HookConfigurationStore()
assertNotNull("WeChat must have a config",
store.getConfig("com.tencent.mm", AppVersion("com.tencent.mm", "8.0.47", 1L, 0L)))
assertNotNull("Telegram must have a config",
store.getConfig("org.telegram.messenger", AppVersion("org.telegram.messenger", "10.0.0", 1L, 0L)))
assertNotNull("Alipay must have a config",
store.getConfig("com.eg.android.AlipayGphone", AppVersion("com.eg.android.AlipayGphone", "10.5.0", 1L, 0L)))
}
@Test
fun p6_hookConfigurationStore_wechatVersionRange_twoDistinctRanges() {
val store = HookConfigurationStore()
val legacyConfig = store.getConfig("com.tencent.mm", AppVersion("com.tencent.mm", "8.0.40", 1L, 0L))
val modernConfig = store.getConfig("com.tencent.mm", AppVersion("com.tencent.mm", "8.0.47", 1L, 0L))
assertNotNull("Legacy WeChat (8.0.40) must have a config", legacyConfig)
assertNotNull("Modern WeChat (8.0.47) must have a config", modernConfig)
assertEquals("Legacy config must cap at 8.0.44", "8.0.44", legacyConfig!!.versionRange.maxVersion)
assertEquals("Modern config must start at 8.0.45", "8.0.45", modernConfig!!.versionRange.minVersion)
assertNull("Modern config must have no upper bound", modernConfig.versionRange.maxVersion)
}
@Test
fun p6_cknPullCache_putAndGetRoundtrip() {
val cache = CknPullCache()
val record = CknPullRecord(
app = "com.tencent.mm",
appVersion = "8.0.47",
hookMethod = "com.tencent.mm.sdk.platformtools.ab.sendMessage",
hookScript = "Java.perform(function(){});",
successRate = 0.98,
validatedBy = 143,
pulledAtMs = System.currentTimeMillis()
)
assertNull("Cache must be empty initially", cache.get("com.tencent.mm", "8.0.47"))
cache.put(record)
val fetched = cache.get("com.tencent.mm", "8.0.47")
assertNotNull("Inserted record must be retrievable", fetched)
assertEquals("com.tencent.mm", fetched!!.app)
assertEquals("8.0.47", fetched.appVersion)
assertEquals(143, fetched.validatedBy)
assertEquals(0.98, fetched.successRate, 0.001)
}
@Test
fun p6_cknPullCache_isStale_falseForFreshRecord() {
val cache = CknPullCache()
val record = CknPullRecord(
app = "org.telegram.messenger", appVersion = "10.0.0",
hookMethod = "org.telegram.messenger.SendMessagesHelper.sendMessage",
hookScript = "", successRate = 0.95, validatedBy = 50,
pulledAtMs = System.currentTimeMillis()
)
cache.put(record)
val fetched = cache.get("org.telegram.messenger", "10.0.0")!!
assertFalse("Record pulled now must not be stale", cache.isStale(fetched, CknPullCache.DEFAULT_TTL_MS))
}
@Test
fun p6_cknPullCache_isStale_trueForExpiredRecord() {
val cache = CknPullCache()
val twoDaysAgo = System.currentTimeMillis() - (2 * 24 * 60 * 60 * 1_000L)
val record = CknPullRecord(
app = "com.eg.android.AlipayGphone", appVersion = "10.5.0",
hookMethod = "com.alipay.mobile.billdetail.service.BillQueryService.getBillList",
hookScript = "", successRate = 0.90, validatedBy = 20,
pulledAtMs = twoDaysAgo
)
cache.put(record)
val fetched = cache.get("com.eg.android.AlipayGphone", "10.5.0")!!
assertTrue("Record pulled 2 days ago must be stale", cache.isStale(fetched, CknPullCache.DEFAULT_TTL_MS))
}
@Test
fun p6_cknUploadRecord_constructsWithPrivacySafeFields() {
val record = CknUploadRecord(
intent = "send_wechat_message",
appPackage = "com.tencent.mm",
appVersion = "8.0.47",
executionPath = listOf(
HookStep("frida_hook", "com.tencent.mm.sdk.platformtools.ab.sendMessage", 0.98f)
),
successRate = 0.98f,
validatedByCount = 1,
timestamp = System.currentTimeMillis()
)
assertEquals("send_wechat_message", record.intent)
assertEquals("com.tencent.mm", record.appPackage)
assertEquals(1, record.executionPath.size)
assertEquals("frida_hook", record.executionPath[0].method)
}
@Test
fun p6_cknSanitizer_passesStructuralFields() {
val record = CknUploadRecord(
intent = "send_wechat_message",
appPackage = "com.tencent.mm",
appVersion = "8.0.47",
executionPath = listOf(HookStep("frida_hook", "com.tencent.mm.sdk.platformtools.ab.sendMessage", 0.98f)),
successRate = 0.98f,
validatedByCount = 143,
timestamp = 1_700_000_000_000L
)
val sanitized = CknSanitizer.sanitize(record)
assertEquals("Structural fields must pass through unchanged", "com.tencent.mm", sanitized.appPackage)
assertEquals("8.0.47", sanitized.appVersion)
assertEquals("send_wechat_message", sanitized.intent)
assertEquals(0.98f, sanitized.successRate, 0.001f)
}
@Test
fun p6_cknSanitizer_stripsChinesePhoneNumber() {
val record = CknUploadRecord(
intent = "send_message_to_13812345678", // contains a phone number
appPackage = "com.tencent.mm",
appVersion = "8.0.47",
executionPath = emptyList(),
successRate = 0.98f,
validatedByCount = 1,
timestamp = 1_700_000_000_000L
)
val sanitized = CknSanitizer.sanitize(record)
assertFalse(
"Sanitized intent must not contain raw phone number",
sanitized.intent.contains("13812345678")
)
}
}