Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

zengy 3 viikkoa sitten
vanhempi
commit
df73cd6186

+ 4 - 1
docs/cockpit/README.md

@@ -199,7 +199,10 @@ mvn -pl zksy-admin -am test -Dtest='Cockpit*Test'
 - 本机真实登录态 HTTP 验收通过:登录成功;未认证请求返回 401 `UNAUTHORIZED`;4 个非法参数样例返回 400 `INVALID_REQUEST`;同一 `snapshotAt` 的 5 页地图请求总数稳定且点位不重复;点位详情和同一设备的 archive / metrics / events / work-orders / detail-aggregate / freshness 均返回 200 且 `readOnly=true`;列出的概览、基础设施、监测、事件和契约自检端点均返回 200 且只读。
 - 本轮真实登录态补充验收:16 个端点非法参数样例全部返回 400 `INVALID_REQUEST`;以真实 `deviceCode` `860347065467627` 依次请求 archive、metrics、events、work-orders、detail-aggregate、freshness,均返回 200、`readOnly=true` 并回显同一 canonical `deviceCode`。
 - 第 4 轮 `detail-aggregate` 的 MockMvc 故障注入证明:单条时间线失败保留其余成功时间线和档案/指标;返回其他工单的顶层或内层日志编码会被拒绝,聚合仍以 200 `PARTIAL` 返回未受影响分区。这是契约测试证据,未在真实服务上制造业务故障。
-- **验收边界**:上述 200/readOnly 证明当前本机实例的认证与响应契约,不等于 #63 全部切片的代表性真实数据、空/脏数据及故障场景均已验收。仓库保留 `Empty*Adapter` 作为 fallback;对应接口另有 `@Primary Business*Adapter`,不能仅凭空实现类仍存在就判断运行时未接线。目标部署实例仍需验收,issue 保持开启。
+- 第 4 轮真实 HTTP 复核(重启加载 `f33b9bf`):首屏无 token 返回 200 并签发 `snapshotAt=+08:00`、`total=15`;使用该 token 请求第 2 页返回 200、同 `snapshotAt`、同 `total`;第 2 页缺 token 和使用旧未签发 token 均返回 400(契约类别由 MockMvc 固化为 `SNAPSHOT_EXPIRED`)。
+- 第 7 轮补充 Redis GET/SET 运行时异常测试:适配器将缓存连接故障包装为 `CockpitReadException`,控制器契约映射为 `500 SERVER_ERROR`,不误报为 `SNAPSHOT_EXPIRED`;定向与全量回归均通过。
+- 第 8 轮本机受控性能烟测:同一真实 `snapshotAt` 连续认证请求 30 次,30/30 成功,平均 24.10ms、P95 30.60ms、最大 34.35ms;本次 live 数据样例 `total=7`。该结果仅代表本机进程和当前数据量,不替代生产规模压测。
+- **验收边界**:上述 200/readOnly 证明当前本机实例的认证与响应契约,不等于 #63 全部切片的代表性真实数据、空/脏数据及故障场景均已验收。静态装配盘点显示 23 个 `Empty*Adapter` 均有对应的 `@Primary Business*Adapter`(另有 1 个共享事件事实适配器);`Empty*Adapter` 作为 fallback 保留,不能仅凭类仍存在判断运行时未接线。目标部署实例仍需验收,issue 保持开启。
 - WebSocket `/ws/deviceStatus` 真实浏览器验收通过:发送 `cockpitSubscribe(scope=all)` 后收到 `cockpitSubscribed`,响应带 `serverUpdatedAt`;所有抽查时间戳为 `+08:00`。
 - 目标部署地址 `192.168.110.30:8301` TCP 不可达,因此没有把它当作本轮 HTTP 证据;生产部署实例、性能压测、银河麒麟与 Docker 实机验证仍未执行。
 

+ 32 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMapSnapshotAdapterTest.java

@@ -7,6 +7,7 @@ import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.service.MonitorPointService;
 import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
+import com.zksy.cockpit.foundation.CockpitReadException;
 import com.zksy.cockpit.foundation.CockpitSnapshotExpiredException;
 import com.zksy.cockpit.map.BusinessCockpitMapSnapshotAdapter;
 import com.zksy.cockpit.map.CockpitMapSnapshot;
@@ -416,6 +417,37 @@ class BusinessCockpitMapSnapshotAdapterTest {
         verifyNoInteractions(monitorPointService, equipmentBaseService, equipmentTypeService);
     }
 
+    @Test
+    void redisGetFailureIsClassifiedAsReadFailure() {
+        RedisTemplate<Object, Object> sharedRedis = mock(RedisTemplate.class);
+        ValueOperations<Object, Object> values = mock(ValueOperations.class);
+        when(sharedRedis.opsForValue()).thenReturn(values);
+        when(values.get(any())).thenThrow(new IllegalStateException("redis unavailable"));
+        BusinessCockpitMapSnapshotAdapter sharedAdapter = new BusinessCockpitMapSnapshotAdapter(
+                monitorPointService, equipmentBaseService, equipmentTypeService, deviceFactResolver,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC), sharedRedis);
+
+        assertThrows(CockpitReadException.class, () -> sharedAdapter.load(null));
+    }
+
+    @Test
+    void redisSetFailureIsClassifiedAsReadFailure() {
+        RedisTemplate<Object, Object> sharedRedis = mock(RedisTemplate.class);
+        ValueOperations<Object, Object> values = mock(ValueOperations.class);
+        when(sharedRedis.opsForValue()).thenReturn(values);
+        when(values.get(any())).thenReturn(null);
+        when(values.setIfAbsent(any(), any(), anyLong(), eq(java.util.concurrent.TimeUnit.SECONDS)))
+                .thenThrow(new IllegalStateException("redis unavailable"));
+        when(monitorPointService.list()).thenReturn(Collections.emptyList());
+        when(equipmentBaseService.list()).thenReturn(Collections.emptyList());
+        when(equipmentTypeService.list()).thenReturn(Collections.emptyList());
+        BusinessCockpitMapSnapshotAdapter sharedAdapter = new BusinessCockpitMapSnapshotAdapter(
+                monitorPointService, equipmentBaseService, equipmentTypeService, deviceFactResolver,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC), sharedRedis);
+
+        assertThrows(CockpitReadException.class, () -> sharedAdapter.load(null));
+    }
+
     @Test
     void expiredSnapshotAtIsRejectedInsteadOfLoadingCurrentRelations() {
         when(monitorPointService.list()).thenReturn(Collections.emptyList());