Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions 00_项目执行总览.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

当前整体状态:`IN_PROGRESS`

当前唯一阶段:`03 FakeTicket 场次票档与订单边界(READY)`
当前唯一阶段:`05 Agent 任务配置目标模式与权限(READY)`

## 目标

Expand Down Expand Up @@ -53,9 +53,9 @@
| 00R | [规范迁移与现状审计](docs/stages/v2/00R_规范迁移与现状审计.md) | `VERIFIED` | 2026-08-12 完成 v2 规范迁移与静态一致性检查 |
| 01 | [工程与历史实现基线复验](docs/stages/v2/01_工程与历史实现基线复验.md) | `VERIFIED` | 2026-08-13:双模块测试、Lint、APK、签名门禁及 vivo Android 16 启动/旋转验证通过 |
| 02 | [FakeTicket 真实布局页面骨架](docs/stages/v2/02_FakeTicket真实布局页面骨架.md) | `VERIFIED` | 2026-08-13:四页 canonical 流程、返回、旋转、稳定 ID 与 vivo Android 16 真机证据通过 |
| 03 | [FakeTicket 场次票档与订单边界](docs/stages/v2/03_FakeTicket场次票档与订单边界.md) | `READY` | 当前唯一可开始阶段;精确场次、票档顺序、数量、总额、支付页停止 |
| 04 | [FakeTicket 开售排队与异常场景](docs/stages/v2/04_FakeTicket开售排队与异常场景.md) | `REVALIDATION_REQUIRED` | 排队成功/失败/超时及风险停止 fixture |
| 05 | [Agent 任务配置目标模式与权限](docs/stages/v2/05_Agent任务配置目标模式与权限.md) | `REVALIDATION_REQUIRED` | 完整标题、城市、场次、票档、金额和逐次确认 |
| 03 | [FakeTicket 场次票档与订单边界](docs/stages/v2/03_FakeTicket场次票档与订单边界.md) | `VERIFIED` | 2026-08-13:三场七票档、数量/实名/金额边界、订单摘要与支付停止页在 vivo Android 16 通过 |
| 04 | [FakeTicket 开售排队与异常场景](docs/stages/v2/04_FakeTicket开售排队与异常场景.md) | `VERIFIED` | 2026-08-13:开售/库存、排队三结果、八类异常、Debug 注入门禁及 vivo Android 16 真机 fixture 通过 |
| 05 | [Agent 任务配置目标模式与权限](docs/stages/v2/05_Agent任务配置目标模式与权限.md) | `READY` | 前置阶段已验证;完整标题、城市、场次、票档、金额和逐次确认待复验 |
| 06 | [目标身份与 Accessibility 观测](docs/stages/v2/06_目标身份与Accessibility观测.md) | `REVALIDATION_REQUIRED` | 目标配置、包名/版本/签名与节点快照 |
| 07 | [截图 OCR 与证据归一化](docs/stages/v2/07_截图OCR与证据归一化.md) | `REVALIDATION_REQUIRED` | 同帧 OCR、隐私和资源生命周期 |
| 08 | [元素定位与页面状态](docs/stages/v2/08_元素定位与页面状态.md) | `REVALIDATION_REQUIRED` | 文本、区域、关系和置信度 |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/images/test-results/stage-v2-03/evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 阶段 03 真机证据

本目录只保存 FakeTicketApp 的脱敏 fixture 页面,不包含真实账号、实名资料、票务平台页面、OCR 原文或完整节点树。

- 设备:vivo V2453A
- Android:16(API 36)
- 流程:三亚预约选择 -> 模拟订单摘要 -> 支付停止页
- 固定选择:2026-12-19 19:30、980 元票档、1 张、观演人 A fixture
- 金额:票款 980 元 + 服务费 10 元 = 合计 990 元
- 安全结论:支付停止页无支付动作;支付只能由用户本人处理
- 设备环境:截图右侧可见 vivo 系统 `com.vivo.upslide` 小窗控件,仅覆盖空白区域,未修改用户系统设置

文件:

- `01-reservation-selected.png`
- `02-order-summary.png`
- `03-payment-stop.png`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/images/test-results/stage-v2-04/evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 阶段 04 真机证据

本目录只保存 FakeTicketApp 的脱敏 fixture 页面,不包含真实账号、实名资料、真实票务平台页面、OCR 原文或完整节点树。

- 设备:vivo V2453A
- Android:16(API 36)
- 屏幕:1260x2800,560 dpi
- 流程:Debug 场景注入 -> 排队等待 -> 排队超时 -> 验证码安全停止
- 真机自动化:阶段 04 用例 3/3 通过;证据用例 1/1 通过
- 安全结论:排队结果由本地 fixture 确定,不连接网络;验证码页只提供重置,不提供绕过或继续动作
- 设备环境:截图右侧可见 vivo 系统悬浮控件,仅覆盖空白区域,未修改设备设置

文件与 SHA-256:

- `01-queue-waiting.png`:`82d0be35e6f17181077e413ee2d55a0ad00f71fea6d44c4ba7132951c168bce4`
- `02-queue-timeout.png`:`a3289e3739bca3c3365c953839eabcb70d13dd613c790e9173ed49929ca20142`
- `03-captcha-stop.png`:`f1847508cfe354a779b94899013857de1d7ced830a2c25d9d82e55f6371b2c3d`
30 changes: 29 additions & 1 deletion docs/stages/v2/03_FakeTicket场次票档与订单边界.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# 阶段 03:FakeTicket 场次票档与订单边界

- 状态:`READY`
- 状态:`VERIFIED`
- 负责人:Codex
- 开始时间:2026-08-13
- 完成时间:2026-08-13
- 前置:阶段 02 `VERIFIED`

## 目标
Expand All @@ -14,3 +17,28 @@
## 门禁

测试先行覆盖精确日期/时间、首选和有序后备票档、禁用确定按钮、必要费用、总额超限、实名缺失/歧义和支付页无支付动作。JVM、UI、Lint、assemble 与真机流程通过。

## 实施结果

- 三个可预约场次使用精确日期、时间和稳定 ID;七个票档按 `580 -> 780 -> 980 -> 1280 -> 1680 -> 1980 -> 2380` 固定排序。
- 票档策略支持首选和有序后备;数量固定为 1 至 2,实名 fixture 数量必须与票数一致。
- 模拟订单明确计算票款和每张 10 元服务费;测试总额上限为 4000 元,超过时禁止确认。
- 预约页、订单摘要和支付停止页共享同一订单草稿;支付页删除支付动作,仅允许返回复核。

## 验证与证据

- TDD 红灯:`ReservationOrderPolicyTest` 首次因策略和 fixture 缺失编译失败;补最小领域实现后通过。
- TDD 红灯:`ReservationFlowViewModelTest` 首次因状态层缺失编译失败;补最小 `StateFlow` ViewModel 后通过。
- 真机首次失败:离屏高价票档不在节点树;增加稳定列表 ID 并滚动到节点。
- 真机第二次失败:滚到列表尾后目标场次离屏;每个选择动作前滚动到对应稳定 ID。
- 真机第三次失败:摘要文案重复“观演人”;收敛 fixture 显示名职责后通过。
- `./gradlew :fake-ticket-app:testDebugUnitTest :fake-ticket-app:lintDebug :fake-ticket-app:assembleDebug`:退出码 0;JVM 84 个测试零失败,Lint 和 APK 构建成功。
- `ANDROID_SERIAL=10AF5C1FCL001TZ ./gradlew :fake-ticket-app:connectedDebugAndroidTest`:退出码 0;vivo V2453A、Android 16/API 36,9 个测试零失败。
- 支付安全负向:主导航无支付回调,支付停止页无 `ACTION_PAY` 节点;完整真机用例断言支付动作数量为 0。
- 视觉证据:`docs/images/test-results/stage-v2-03/`;三张图只含脱敏 FakeTicket fixture。右侧 vivo 系统小窗仅覆盖空白区域,未修改设备设置。

## 限制与下一门禁

- 本阶段只验证 vivo V2453A Android 16/API 36,不外推到其他系统、ROM 或分辨率。
- 本阶段未操作 AndroidAgent 或真实大麦;真实目标仍须等待阶段 15 及后续逐级验收。
- 阶段 04 已在总览中成为下一次唯一 `READY`;本次不开始阶段 04。
39 changes: 38 additions & 1 deletion docs/stages/v2/04_FakeTicket开售排队与异常场景.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# 阶段 04:FakeTicket 开售排队与异常场景

- 状态:`REVALIDATION_REQUIRED`
- 状态:`VERIFIED`
- 负责人:Codex
- 开始时间:2026-08-13
- 完成时间:2026-08-13
- 前置:阶段 03 `VERIFIED`

## 目标
Expand All @@ -14,3 +17,37 @@
## 门禁

模型和 Compose 测试覆盖每个转换与重置;release 不暴露场景注入;排队 fixture 不依赖固定不可控网络。所有测试、Lint、assemble 和真机人工注入通过。

## 实现结果

- `FakeSaleClock`、七票档库存与场景控制器提供未开售、已开售、售罄及统一固定初态重置。
- 排队 fixture 覆盖等待到成功、失败、超时三个确定性结果;失败/超时暴露 `page_queue_failed`,所有状态暴露 `text_queue_status`。
- 独立页面覆盖网络错误、登录失效、风控、验证码和未知页;阻断场景使用带 `dialog_blocking` 与 `action_dismiss_dialog` 的真实 Compose 弹窗。
- Debug 构建允许从测试 Intent 注入已知枚举;Release 构建策略拒绝全部场景注入,未知值在所有构建中均拒绝。
- 全部 fixture 使用本地内存状态,不连接网络;每个场景均可经 `action_reset_scenario` 回到首页固定初态。

## 验证结果

工作目录均为仓库根目录。

- `./gradlew :fake-ticket-app:testDebugUnitTest :fake-ticket-app:testReleaseUnitTest :fake-ticket-app:lintDebug :fake-ticket-app:lintRelease :fake-ticket-app:assembleDebug :fake-ticket-app:assembleRelease`:退出码 0;Debug/Release 各 93 条单元测试通过,两种构建的 Lint 与 APK assemble 通过。
- `ANDROID_SERIAL=emulator-5554 ./gradlew :fake-ticket-app:connectedDebugAndroidTest`:退出码 0;Pixel 6 AVD、Android 14,12/12 条 instrumented test 通过,包含横竖屏恢复与阶段 04 三条 Compose fixture 测试。
- `ANDROID_SERIAL=10AF5C1FCL001TZ ./gradlew :fake-ticket-app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.yanbo.faketicket.FakeTicketScenarioFixtureTest`:退出码 0;vivo V2453A、Android 16/API 36,3/3 条阶段 04 真机测试通过。
- 显式安装 app/test APK 后执行 `adb ... shell am instrument ... FakeTicketScenarioFixtureTest#capturesSelectedStage04Evidence`:退出码 0,`OK (1 test)`;验证 Debug 手工场景注入并保留选定截图。
- Release 负向验证由 `ScenarioBuildPolicyTest` 在 `testReleaseUnitTest` 中执行:`BuildConfig.DEBUG=false` 时 `CAPTCHA` 等已知值仍返回 `null`。

## 失败修复

- vivo 首次安装在熄屏锁定状态返回 `INSTALL_FAILED_ABORTED: User rejected permissions`;解锁设备并由用户允许 USB 安装后,相同 APK 安装成功,随后真机用例通过。未绕过 Android 安全确认。
- 初版阻断场景只是普通页面,Compose 契约测试因 `dialog_blocking` 不存在失败;改为真实 `AlertDialog` 后关闭与重置用例通过。
- 首次全量模拟器回归有两条历史横屏用例触发 `performMeasureAndLayout called during measure layout`;日志确认恢复竖屏时系统 rotation 已变化但 Compose 仍在布局,在测试恢复 helper 加入 UI 空闲等待后 discovery 7/7、全量 12/12 通过。

## 证据与限制

- 脱敏截图与哈希见 [阶段证据](../../images/test-results/stage-v2-04/evidence.md)。
- 本阶段只验收 FakeTicketApp fixture,不包含 AndroidAgent 自动化链路,也没有执行真实大麦识别或动作。
- 真机结果仅代表 vivo V2453A、Android 16/API 36、1260x2800;Android 10 至 15 的兼容性仍待后续矩阵阶段验证。

## 下一阶段门禁

阶段 04 已 `VERIFIED`。阶段 05 可标为唯一 `READY`,但本任务在此停止,不自动开始阶段 05。
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,6 @@ class FakeTicketDiscoveryFlowTest {
ShellTestDevice.run("settings put system accelerometer_rotation $originalAuto")
ShellTestDevice.run("settings put system user_rotation $originalRotation")
ShellTestDevice.waitForRotation(originalRotation.toInt())
ShellTestDevice.waitForUiStable()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.yanbo.faketicket

import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yanbo.faketicket.support.ShellTestDevice
import com.yanbo.faketicket.ui.components.FakeTicketTestTags
import com.yanbo.faketicket.ui.discovery.DiscoveryContent
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FakeTicketReservationOrderFlowTest {
@get:Rule
val composeRule = createEmptyComposeRule()

@Before
fun launch() = ShellTestDevice.launchFakeTicket()

@Test
fun exactReservationCreatesSummaryAndStopsWithoutPaymentAction() {
openReservationSelect()

listOf(
"12月18日 周五 19:30",
"12月19日 周六 19:30",
"12月20日 周日 19:30",
).forEach { composeRule.onNodeWithText(it).assertExists() }
listOf(580, 780, 980, 1280, 1680, 1980, 2380).forEach { price ->
composeRule.onNodeWithTag(FakeTicketTestTags.LIST_RESERVATION_OPTIONS)
.performScrollToNode(hasTestTag(FakeTicketTestTags.tier("tier-$price")))
composeRule.onNodeWithText("¥$price").assertExists()
}

composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_CONFIRM_RESERVATION)
.assertIsNotEnabled()
composeRule.onNodeWithTag(FakeTicketTestTags.LIST_RESERVATION_OPTIONS)
.performScrollToNode(hasTestTag(FakeTicketTestTags.SESSION_2026_12_19_1930))
composeRule.onNodeWithTag(FakeTicketTestTags.SESSION_2026_12_19_1930).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.LIST_RESERVATION_OPTIONS)
.performScrollToNode(hasTestTag(FakeTicketTestTags.TIER_980))
composeRule.onNodeWithTag(FakeTicketTestTags.TIER_980).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_CONFIRM_RESERVATION)
.assertIsNotEnabled()
composeRule.onNodeWithTag(FakeTicketTestTags.LIST_RESERVATION_OPTIONS)
.performScrollToNode(hasTestTag(FakeTicketTestTags.ATTENDEE_FIXTURE_A))
composeRule.onNodeWithTag(FakeTicketTestTags.ATTENDEE_FIXTURE_A).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_CONFIRM_RESERVATION)
.assertIsEnabled()
val screenshots = mutableListOf(
ShellTestDevice.capturePrivatePng(
fileName = "01-reservation-selected.png",
privateDirectoryName = "stage-v2-03",
),
)
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_CONFIRM_RESERVATION)
.performClick()

composeRule.onNodeWithTag(FakeTicketTestTags.PAGE_ORDER_CONFIRM).assertIsDisplayed()
composeRule.onNodeWithText("12月19日 周六 19:30").assertExists()
composeRule.onNodeWithText("票款 ¥980").assertExists()
composeRule.onNodeWithText("服务费 ¥10").assertExists()
composeRule.onNodeWithText("合计 ¥990").assertExists()
composeRule.onNodeWithText("观演人 A").assertExists()
screenshots += ShellTestDevice.capturePrivatePng(
fileName = "02-order-summary.png",
privateDirectoryName = "stage-v2-03",
)
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_SUBMIT_ORDER).performClick()

composeRule.onNodeWithTag(FakeTicketTestTags.PAGE_PAYMENT).assertIsDisplayed()
composeRule.onNodeWithText("支付请由用户本人处理").assertIsDisplayed()
composeRule.onAllNodesWithTag(FakeTicketTestTags.ACTION_PAY).assertCountEquals(0)
screenshots += ShellTestDevice.capturePrivatePng(
fileName = "03-payment-stop.png",
privateDirectoryName = "stage-v2-03",
)
ShellTestDevice.exportEvidence(screenshots, directoryName = "stage-v2-03-selected")
}

private fun openReservationSelect() {
composeRule.onNodeWithTag(FakeTicketTestTags.INPUT_HOME_SEARCH)
.performTextInput(DiscoveryContent.performanceTitle)
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_HOME_SEARCH).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.PAGE_SEARCH_RESULTS).assertIsDisplayed()
ShellTestDevice.waitForKeyboardHidden()
composeRule.onNode(
hasTestTag(FakeTicketTestTags.CARD_TARGET_PERFORMANCE) and
hasAnyDescendant(hasTestTag(FakeTicketTestTags.CITY_SANYA)),
).assertExists()
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_CITY_RESERVE).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.ACTION_RESERVE_OR_BUY).performClick()
composeRule.onNodeWithTag(FakeTicketTestTags.PAGE_RESERVATION_SELECT).assertIsDisplayed()
}
}
Loading