From c71c3c32f6381112adb1b9f04c21b324a31e12d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=94=E9=B2=81=E6=AD=A3?= <13780741225@163.com> Date: Thu, 10 Sep 2026 10:40:55 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Mock=20=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=97=B6=E9=92=9F=EF=BC=8C=E6=B6=88=E9=99=A4?= =?UTF-8?q?=20flaky=20=E6=96=AD=E8=A8=80=EF=BC=88ledger=20R17=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MockDataProvider 增加 now: () -> Long = System::currentTimeMillis - metricHistory / workouts 改用 now(),使「确定性」承诺成立 - 测试注入 FIXED_NOW,并补一条时间戳严格递增的断言 --- .../plans/2026-09-10-ring-health-app-m0-m1.md | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md index 439d5a0..ace0db0 100644 --- a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md +++ b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md @@ -1350,10 +1350,17 @@ git add -A && git commit -m "feat: 新增数值格式化工具(含单元测试 - Consumes: `domain/model/*`、`domain/repository/*` - Produces: - `interface AppContainer { healthDataRepository; ringDeviceRepository }`、`class DefaultAppContainer : AppContainer`、`RingHealthApplication.container` - - `class MockDataProvider(seed: Long = DEFAULT_SEED)`,方法:`todaySummary(): HealthSummary`、`metricHistory(metric: MetricType, range: DateRange): List`、`workouts(): List`、`deviceState(): RingDeviceState`、`defaultFunctionSettings(): List` + - `class MockDataProvider(seed: Long = DEFAULT_SEED, now: () -> Long = System::currentTimeMillis)`,方法:`todaySummary(): HealthSummary`、`metricHistory(metric: MetricType, range: DateRange): List`、`workouts(): List`、`deviceState(): RingDeviceState`、`defaultFunctionSettings(): List` - `class MockHealthDataRepository : HealthDataRepository`(每次发射前 `delay(200)`) - `class MockRingDeviceRepository : RingDeviceRepository`(开关状态保存在内存 StateFlow) +> **范围说明(见 ledger Ruling R17)**:`MockDataProvider` 增加一个**可注入的时间源** `now: () -> Long` +> (默认 `System::currentTimeMillis`),`metricHistory` / `workouts` 内一律用 `now()` 而非 +> `System.currentTimeMillis()`。原因:本类的 KDoc 承诺「所有方法都是确定性的(同参数返回同结果)」, +> 但直接读挂钟时 `timestamp` 会随毫秒跳动 —— brief 原先的测试 `assertEquals(first, second)` +> 因此是一条**间歇失败的 flaky 断言**(两次调用跨毫秒即挂)。注入时间源后该承诺才成立, +> 测试也才能传固定时钟、真正验证「可重复」。这是本任务唯一的接口增补。 + - [ ] **Step 1: 写失败的测试** `app/src/test/java/com/placeholder/ringhealth/data/mock/MockDataProviderTest.kt`: @@ -1369,7 +1376,16 @@ import org.junit.Test class MockDataProviderTest { - private val provider = MockDataProvider() + /** + * 固定时钟。Mock 的「确定性」承诺依赖时间源固定,否则 `timestamp` 会随毫秒跳动 + * (ledger Ruling R17)。 + */ + private val provider = MockDataProvider(now = { FIXED_NOW }) + + private companion object { + /** 固定的“当前时间”,让历史数据的时间戳可复现。 */ + const val FIXED_NOW = 1_760_000_000_000L + } @Test fun `今日概览返回固定演示数据`() { @@ -1395,6 +1411,8 @@ class MockDataProviderTest { val second = provider.metricHistory(MetricType.HEART_RATE, DateRange.WEEK) assertEquals(first, second) assertTrue(first.all { it.value > 40.0 && it.value < 180.0 }) + // 时间戳须严格递增(“每点间隔一天”倒推,最早的在最前) + assertTrue(first.zipWithNext().all { (a, b) -> b.timestamp > a.timestamp }) } @Test @@ -1437,10 +1455,17 @@ import com.placeholder.ringhealth.domain.model.WorkoutRecord import kotlin.random.Random /** - * 演示数据生成器。所有方法都是确定性的(同参数返回同结果), + * 演示数据生成器。所有方法都是确定性的(同参数、同时刻返回同结果), * 保证页面展示与截图稳定。M2 接入真机后本类整体弃用。 + * + * @param seed 随机种子,决定历史数据的取值序列。 + * @param now 时间源,默认取系统时钟;测试注入固定值后可得到完全可复现的时间戳 + * (ledger Ruling R17)。 */ -class MockDataProvider(private val seed: Long = DEFAULT_SEED) { +class MockDataProvider( + private val seed: Long = DEFAULT_SEED, + private val now: () -> Long = System::currentTimeMillis, +) { /** 今日概览:固定演示值。血压为 null 以演示“未开启”占位态。 */ fun todaySummary(): HealthSummary = HealthSummary( @@ -1464,13 +1489,13 @@ class MockDataProvider(private val seed: Long = DEFAULT_SEED) { val count = if (range == DateRange.WEEK) 7 else 30 val random = Random(seed + metric.ordinal * 100L + range.ordinal) val (min, max) = valueRange(metric) - val now = System.currentTimeMillis() + val current = now() val dayMillis = 24L * 60 * 60 * 1000 return (0 until count).map { index -> val progress = index.toFloat() / (count - 1).coerceAtLeast(1) val value = min + (max - min) * progress + random.nextDouble(-1.0, 1.0) * (max - min) * 0.15 MetricPoint( - timestamp = now - (count - 1 - index) * dayMillis, + timestamp = current - (count - 1 - index) * dayMillis, value = value.coerceIn(min, max), ) } @@ -1479,14 +1504,14 @@ class MockDataProvider(private val seed: Long = DEFAULT_SEED) { /** 运动记录:固定 5 条演示数据,倒序(最近的在最前)。 */ fun workouts(): List { val random = Random(seed + 7) - val now = System.currentTimeMillis() + val current = now() val dayMillis = 24L * 60 * 60 * 1000 val types = listOf("户外跑步", "健走", "骑行", "自由训练", "户外跑步") return types.mapIndexed { index, type -> WorkoutRecord( id = "workout-$index", type = type, - startTime = now - (index + 1) * dayMillis, + startTime = current - (index + 1) * dayMillis, durationMinutes = 25 + random.nextInt(0, 45), distanceKm = 2.0 + random.nextDouble(0.0, 5.0), calories = 120 + random.nextInt(0, 260),