fix(#152): separate address save and final proof

This commit is contained in:
QiuSW
2026-08-29 14:51:54 +08:00
parent e188fec3aa
commit 7fddd189b4
3 changed files with 50 additions and 22 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "cn.ilapage.goauto.agent"
minSdk = 23
targetSdk = 34
versionCode = 36
versionName = "0.9.23"
versionCode = 37
versionName = "0.9.24"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -73,7 +73,7 @@ class PurchaseLiveAutomation(
val snapshot = driver.capture()
pageProblem(snapshot)
if (snapshot.packageName != PDD_PACKAGE) fail("PURCHASE_CONFIRMATION_LOST", "最终提交前页面已经变化,禁止创建订单")
if (!hasSavedAddressEvidence(snapshot, address.expectedAddress, address.suffix)) {
if (!hasFinalSavedAddressEvidence(snapshot, address.expectedAddress, address.suffix)) {
fail("PURCHASE_ADDRESS_UPDATE_FAILED", "收货地址保存后复核失败,未创建订单")
}
val submit = finalSubmitTargets(snapshot)
@@ -171,12 +171,12 @@ class PurchaseLiveAutomation(
val save = uniqueClickable(stable, stable.nodes.filter { it.visible && it.enabled && it.label == "保存" })
if (save.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存按钮不唯一,未创建订单")
click(save.single(), "保存地址")
val savedEvidence = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单") { snapshot ->
hasSavedAddressEvidence(snapshot, expected, suffix)
}
if (finalSubmitTargets(savedEvidence).size != 1) {
val savedEvidence = waitForStableSavedAddress(expected, suffix)
if (!hasFinalSavedAddressEvidence(savedEvidence, expected, suffix)) {
if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单")
waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { finalSubmitTargets(it).size == 1 }
waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") {
hasFinalSavedAddressEvidence(it, expected, suffix)
}
}
return ShippingAddressProof(expected, suffix)
}
@@ -184,11 +184,6 @@ class PurchaseLiveAutomation(
private fun hasSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean {
if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false
val visible = snapshot.nodes.filter { it.visible }
val addressContext = visible.count { normalizeAddressText(it.label) == "收货地址" } == 1 ||
(mergedDirect(visible.filter { it.enabled && MASKED_PHONE.containsMatchIn(it.label) }).size == 1 &&
finalSubmitTargets(snapshot).size == 1)
if (!addressContext) return false
val normalizedExpected = normalizeAddressText(expected)
val fullMatches = mergedDirect(visible.filter {
normalizeAddressText(it.label).contains(normalizedExpected) && hasExactTaskSuffix(it.label, suffix)
@@ -200,6 +195,25 @@ class PurchaseLiveAutomation(
return suffixMatches.size == 1
}
private fun hasFinalSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean =
hasSavedAddressEvidence(snapshot, expected, suffix) && finalSubmitTargets(snapshot).size == 1
private fun waitForStableSavedAddress(expected: String, suffix: String): UiSnapshot {
var consecutiveMatches = 0
repeat(50) {
val snapshot = driver.capture()
pageProblem(snapshot)
if (hasSavedAddressEvidence(snapshot, expected, suffix)) {
consecutiveMatches++
if (consecutiveMatches >= 2) return snapshot
} else {
consecutiveMatches = 0
}
pause(200)
}
fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单")
}
private fun normalizeAddressText(value: String): String = value.filterNot(Char::isWhitespace)
private fun hasExactTaskSuffix(value: String, suffix: String): Boolean =
@@ -51,6 +51,19 @@ class PurchaseLiveAutomationTest {
assertEquals(0, driver.submitClicks)
}
@Test
fun `saved address transition without legacy title or phone is accepted before final confirmation`() {
val driver = LiveDriver(savedTransitionWithoutLegacyContext = true)
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg21")
val final = automation.finalConfirmation(input().copy(addressSuffix = "_cg21"), address)
assertEquals("_cg21", final.addressSuffix)
assertEquals(1, driver.backCount)
assertEquals(0, driver.submitClicks)
}
@Test
fun `edit field containing suffix cannot impersonate post save evidence`() {
val driver = LiveDriver(saveStaysInEdit = true)
@@ -64,24 +77,20 @@ class PurchaseLiveAutomationTest {
@Test
fun `missing confirmation suffix fails before order creation`() {
val driver = LiveDriver(hideConfirmationSuffix = true)
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg19")
val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg19"), address) }
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg19") }
.exceptionOrNull() as PurchaseLiveException
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code)
assertEquals(0, driver.submitClicks)
}
@Test
fun `duplicate independent confirmation suffixes fail before order creation`() {
val driver = LiveDriver(splitConfirmationAddress = true, duplicateConfirmationSuffix = true)
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg20")
val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg20"), address) }
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg20") }
.exceptionOrNull() as PurchaseLiveException
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code)
assertEquals(0, driver.submitClicks)
}
@@ -166,6 +175,7 @@ class PurchaseLiveAutomationTest {
private val hideConfirmationSuffix: Boolean = false,
private val duplicateConfirmationSuffix: Boolean = false,
private val saveStaysInEdit: Boolean = false,
private val savedTransitionWithoutLegacyContext: Boolean = false,
) : PurchaseUiDriver {
private var page = "confirmation"
private var addressVisible = !addressClipped
@@ -196,6 +206,10 @@ class PurchaseLiveAutomationTest {
node("editor-address", address, className = "android.widget.EditText", bounds = NodeBounds(180, 390, 900, 480)),
node("save-parent", "", clickable = true), node("save", "保存", parentPath = "save-parent"),
))
"saved-transition" -> snapshot(listOf(
node("transition-title", "选择收货信息"),
node("address-suffix", address.substring(address.lastIndexOf("_cg"))),
))
"order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true)))
"chooser" -> UiSnapshot(if (trustedChooser) "android" else "example.untrusted", "com.android.internal.app.ChooserActivity", listOf(
node("chooser-title", "选择要使用的应用"), node("wechat-1", "微信"), node("wechat-2", "微信分身"),
@@ -236,7 +250,7 @@ class PurchaseLiveAutomationTest {
when (target.label) {
"138****5678" -> page = "panel"
"修改" -> page = "edit"
"保存" -> if (!saveStaysInEdit) page = "panel"
"保存" -> if (!saveStaysInEdit) page = if (savedTransitionWithoutLegacyContext) "saved-transition" else "panel"
"提交订单" -> { submitClicks++; page = if (chooserAfterSubmit) "chooser" else "order" }
}
return FreshActionResult.SUCCESS