From 486cc80b6ff66e455b776ca488856c0c2ee37ba0 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 7 Sep 2026 13:03:30 +0800 Subject: [PATCH] fix: correct STEP-057B weather runtime state semantics --- ...STEP-057_AndroidHost天气运行时接口基础_v1.md | 10 +++++ ...idHost天气运行时接口基础_Codex完成报告_v3.md | 39 +++++++++++++++++ .../maplibre/android/maps/WeatherRuntime.java | 8 ++-- .../android/maps/WeatherRuntimeStateTest.java | 43 +++++++++++++++++++ 粒子开发记录.md | 6 +++ 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md diff --git a/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md b/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md index 4b3777a2d..44492a365 100644 --- a/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md +++ b/docs/50_Step记录/STEP-057_AndroidHost天气运行时接口基础_v1.md @@ -25,3 +25,13 @@ OFF 会停止 Timeline、clear Host ownership,并在 Render 线程的空 Sourc - timestamp 测试改为 epoch milliseconds(2024-01-01 UTC 起的 64-bit ms),覆盖 exact T0、midpoint、exact T1、上下界 out-of-range 与 clear 后 fail-closed。 验证:output-throttle 下 shader generator、Linux OpenGL `mbgl-core`、`mbgl-render`、`mbgl-test-runner`、相关精确 Source/Style compile 和定向非 Jest tests 均 PASS;`git diff --check` PASS。Android Gradle/APK/ADB/设备/Jest 未运行。 + +## STEP-057B 状态语义修正(2026-09-07) + +状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。 + +基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,本轮只修改 `WeatherRuntime` 与状态测试/记录:普通 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 和空 metadata/payload 保持当前 state;`REJECTED_SOURCE_MODE`、`UNKNOWN` 才进入 ERROR。pending T0、initial window、next snapshot、duplicate 的成功/保持语义不变。 + +`READY + enable(true)`、`WAITING_FOR_FIELDS + enable(true)` 为幂等 no-op;`DISABLED + enable(true)` 成功恢复到 WAITING;ERROR enable 明确返回 REJECTED 并保持 ERROR。clear/disable 成功与失败转换保持原有契约,DISABLED 重复 disable 仍为成功 no-op。 + +`WeatherRuntimeStateTest` 已覆盖普通 rejection 保持 READY/WAITING、真正错误、所有 enable 分支、clear/disable,以及 public flow 的 invalid submit 和 ERROR enable。public flow 测试源码已加入现有 Mockito/JUnit 体系,但本轮未运行 Android Gradle;因此仍需代码审计后统一 Android 验收。 diff --git a/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md new file mode 100644 index 000000000..4aa7e175f --- /dev/null +++ b/docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md @@ -0,0 +1,39 @@ +# STEP-057B WeatherRuntime 状态语义修正 — Codex 完成报告 + +状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。 + +## 实际修改 + +本轮基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,只修改: + +- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java` +- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java` +- STEP-057 Step 记录、`粒子开发记录.md` 与本报告 + +没有修改 renderer、RenderOrchestrator、JNI、shader/UBO、WeatherColor ramp、timestamp Core、Runtime Field Window、Test App UI 或 React Native bridge。 + +## 最终状态语义 + +普通数据质量拒绝 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 以及空 metadata/payload 只表示本次提交失败,保留当前 `READY` 或 `WAITING_FOR_FIELDS`。`ACCEPTED_PENDING_INITIAL_WINDOW` → WAITING,`INITIAL_WINDOW_READY`/`ACCEPTED_NEXT_SNAPSHOT` → READY,`DUPLICATE` 保持 current。只有 `REJECTED_SOURCE_MODE` 和 `UNKNOWN` 进入 ERROR。 + +`READY` 或 `WAITING_FOR_FIELDS` 重复 enable 是 ACCEPTED no-op;`DISABLED` enable 成功恢复 visibility 并进入 WAITING,失败进入 ERROR;ERROR enable 返回 REJECTED 并保持 ERROR。clear 成功 → WAITING、失败保持 current;disable 成功 → DISABLED、失败 → ERROR;DISABLED 重复 disable 为 ACCEPTED no-op。 + +## 测试覆盖 + +`WeatherRuntimeStateTest` 使用现有 JUnit/Mockito,覆盖所有普通 rejection 在 READY/WAITING 下保持状态、SOURCE_MODE/UNKNOWN 错误、enable 分支、clear/disable,以及两条实际 public flow:READY 下 invalid submit 保持 READY,ERROR 下 enable 返回 REJECTED 且不调用 visibility recovery。该源码未单独引入测试框架。 + +## 验证 + +本轮执行 `git diff --check`:PASS。未执行编译或测试命令;因此 public flow 测试尚未运行。明确未执行:Android Gradle、APK build、ADB、设备/虚拟机测试、Jest。此前 STEP-057A 的 Linux/OpenGL 与非 Jest Core 验证不受本轮代码修改影响。 + +## 风险与审计状态 + +已知剩余风险是 Android Java/Kotlin 编译和 Mockito final-class 配置需在后续统一 Android 验收中确认;本轮没有改变 Core field/timeline 生命周期。当前提交具备再次独立代码审计条件,等待 code review 后再进入 Android T1–T13 综合验证。 + +## Git + +- 基线:`dad6876c72f29e2d5671677ee15e13cd25e653a1` +- 功能 commit:待本轮提交后记录 +- Push:待本轮提交后记录 +- Remote branch:`origin/particle/main` +- Final HEAD:待本轮提交后记录 diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java index 275f49cb8..0f46d222a 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java @@ -55,7 +55,7 @@ public final class WeatherRuntime { public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) { requireEnabled(); - if (metadataJson.isEmpty() || payloadBytes.length == 0) { state = State.ERROR; return FieldSubmitResult.REJECTED_ADAPTER; } + if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER; FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes)); state = stateAfterSubmit(state, result); return result; @@ -71,6 +71,7 @@ public final class WeatherRuntime { state = stateAfterDisable(color && particle && clear); return result(color && particle && clear); } + if (state == State.ERROR) return CommandResult.REJECTED; if (state != State.DISABLED) return CommandResult.ACCEPTED; boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible); boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible); @@ -105,13 +106,14 @@ public final class WeatherRuntime { if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS; if (submit == FieldSubmitResult.INITIAL_WINDOW_READY || submit == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT) return State.READY; if (submit == FieldSubmitResult.DUPLICATE) return current; - return State.ERROR; + return submit == FieldSubmitResult.REJECTED_SOURCE_MODE || submit == FieldSubmitResult.UNKNOWN ? State.ERROR : current; } static State stateAfterClear(State current, boolean accepted) { return accepted ? State.WAITING_FOR_FIELDS : current; } static State stateAfterDisable(boolean accepted) { return accepted ? State.DISABLED : State.ERROR; } static State stateAfterEnable(State current, boolean accepted) { if (!accepted) return State.ERROR; - return current == State.DISABLED ? State.WAITING_FOR_FIELDS : current; + if (current == State.DISABLED) return State.WAITING_FOR_FIELDS; + return current; } private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); } } diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java index 9da45e4ed..c379ce80e 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java @@ -1,6 +1,7 @@ package org.maplibre.android.maps; import org.junit.Test; +import org.mockito.Mockito; import static org.junit.Assert.assertEquals; @@ -15,9 +16,51 @@ public class WeatherRuntimeStateTest { assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY)); assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.DUPLICATE)); } + @Test public void ordinarySubmitRejectionsPreserveRuntimeState() { + WeatherRuntime.FieldSubmitResult[] rejected = { + WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, + WeatherRuntime.FieldSubmitResult.REJECTED_INCOMPATIBLE, + WeatherRuntime.FieldSubmitResult.REJECTED_NON_MONOTONIC, + WeatherRuntime.FieldSubmitResult.REJECTED_CAPACITY}; + for (WeatherRuntime.FieldSubmitResult result : rejected) { + assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, result)); + assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, result)); + } + } + @Test public void onlyFacadeContractFailuresEnterError() { + assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.REJECTED_SOURCE_MODE)); + assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.UNKNOWN)); + assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, false)); + assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, true)); + } @Test public void clearAndDisableUseSuccessfulCoreResult() { assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterClear(WeatherRuntime.State.READY, true)); assertEquals(WeatherRuntime.State.DISABLED, WeatherRuntime.stateAfterDisable(true)); assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterDisable(false)); } + + @Test public void publicFlowKeepsReadyOnInvalidSubmission() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + Mockito.when(map.submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class))) + .thenReturn(MapLibreMap.HOST_FIELD_INITIAL_WINDOW_READY) + .thenReturn(MapLibreMap.HOST_FIELD_REJECTED_ADAPTER); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY, runtime.submitWindField("{}", new byte[] {1})); + assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("{}", new byte[] {1})); + assertEquals(WeatherRuntime.State.READY, runtime.getState()); + assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("", new byte[] {1})); + assertEquals(WeatherRuntime.State.READY, runtime.getState()); + } + + @Test public void publicFlowRejectsEnableFromErrorWithoutVisibilityRecovery() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(WeatherRuntime.FieldSubmitResult.UNKNOWN, runtime.submitWindField("{}", new byte[] {1})); + assertEquals(WeatherRuntime.State.ERROR, runtime.getState()); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(true)); + assertEquals(WeatherRuntime.State.ERROR, runtime.getState()); + Mockito.verify(map, Mockito.never()).setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean()); + } } diff --git a/粒子开发记录.md b/粒子开发记录.md index c3fb8b6ac..597e2d8fe 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -463,3 +463,9 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 ` - enable/clear/disable/submit 状态转换已幂等并可恢复,另增加既有 Android JUnit state transition 测试源码;未运行 Gradle。 - hidden unload 改成 RasterParticle/WeatherColor renderer 的 Host-empty-source 显式 opt-in cleanup,移除对所有 hidden RasterArray layer 的全局 prepare/update。 - timestamp 测试改用真实 epoch milliseconds,覆盖 T0、midpoint、T1、上下边界和 clear 后拒绝;WeatherColor capacity 由 Java 通过 JNI 读取 C++ `MaxWeatherColorStops`。 + +## 2026-09-07 — STEP-057B WeatherRuntime 状态语义修正 + +- 普通 field rejection 与 facade/runtime failure 分离:Core 保留有效 timeline 时,READY/WAITING 状态不被坏提交改成 ERROR;空 metadata/payload 同样只拒绝本次提交。 +- `REJECTED_SOURCE_MODE`/`UNKNOWN` 才进入 ERROR;ERROR + enable(true) 返回 REJECTED 并保持 ERROR;READY/WAITING enable 保持幂等,DISABLED enable 成功后等待新 fields。 +- 扩展 `WeatherRuntimeStateTest`,覆盖 static state transition 与 Mockito public control flow;未执行 Android Gradle、设备测试或 Jest。