package com.battery.business.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import com.battery.device.api.DeviceAdapter; import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore; import com.battery.device.api.model.ChannelIdentity; import com.battery.device.api.model.DetectedChannel; import com.battery.device.api.registry.DeviceAdapterRegistry; import com.battery.business.mapper.TDetectionCabinetMapper; import com.battery.business.mapper.TTestMapper; class DeviceDiscoveryPersistenceServiceTest { /** * 来源不可达时,既有候选保留来源身份但旧快照被撤下、在线转为离线,登记设备同步离线; * 来源恢复后下一次成功发现重新带回实时事实。 */ @Test void failedDiscoveryInvalidatesSuccessfulSnapshotUntilSourceReturns() { InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore(); TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class); DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class); DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class); ChannelIdentity identity = new ChannelIdentity("neware76", 21, 36, 1, 2); java.time.Instant observedAt = java.time.Instant.parse("2026-09-28T01:54:56Z"); com.battery.device.api.model.RealtimeSnapshot snapshot = new com.battery.device.api.model.RealtimeSnapshot( identity, com.battery.device.api.model.DeviceWorkStatus.WORKING, 3, 7, "1", null, new java.math.BigDecimal("1.2"), new java.math.BigDecimal("3.3"), new java.math.BigDecimal("0.5"), new java.math.BigDecimal("1.1"), 60L, 30L, null, observedAt, java.util.Map.of()); when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "26.1.2.3", true))); when(adapter.inquire(any())).thenReturn(java.util.Map.of(identity, snapshot)); when(registry.execute(eq("neware76"), any())).thenAnswer(invocation -> { @SuppressWarnings("unchecked") Function operation = (Function) invocation.getArgument(1); return operation.apply(adapter); }); DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(store, cabinetMapper); service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource( "neware76", 8L, "新威 7.6 那台", 60000, true)); var before = store.snapshot(java.time.Instant.now()).cabinets().get(0); org.junit.jupiter.api.Assertions.assertNotNull(before.channels().get(0).latestSnapshot()); org.junit.jupiter.api.Assertions.assertTrue(before.online()); service.markUnavailable("neware76"); var failed = store.snapshot(java.time.Instant.now()).cabinets().get(0); assertEquals(before.deviceCode(), failed.deviceCode()); org.junit.jupiter.api.Assertions.assertFalse(failed.online()); org.junit.jupiter.api.Assertions.assertNull(failed.channels().get(0).latestSnapshot()); verify(cabinetMapper).markRegisteredCabinetsOffline("neware76"); } @Test void discoveryPersistsCandidatesWithoutReceivingTest() { InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore(); TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class); TTestMapper testMapper = Mockito.mock(TTestMapper.class); DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class); DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class); ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 1, 1); when(testMapper.hasReceivingTestForAdapter("lab-bts")).thenReturn(0); when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "127.0.0.1", true))); when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation -> { @SuppressWarnings("unchecked") Function operation = (Function) invocation.getArgument(1); return operation.apply(adapter); }); DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(candidateStore, cabinetMapper, testMapper); var result = service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource("lab-bts", 7L, "一号 BTS", 1000)); assertEquals(1, result.cabinetCount()); assertEquals(1, candidateStore.snapshot(java.time.Instant.now()).cabinets().size()); verify(adapter).discoverChannels(); } /** * 直连厂商存储的来源:通道登记是长期事实,候选不按时间过期, * 每条候选附带最近一次采样行的设备时间,供操作员判断哪几台真的在产出数据。 */ @Test void vendorSourceCandidatesCarryLastDataTimeAndDoNotExpireByTime() { InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore(); TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class); TTestMapper testMapper = Mockito.mock(TTestMapper.class); DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class); DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class); ChannelIdentity identity = new ChannelIdentity("neware76", 21, 36, 1, 2); java.time.Instant lastDataAt = java.time.Instant.parse("2026-09-28T01:54:56Z"); when(testMapper.hasReceivingTestForAdapter("neware76")).thenReturn(0); when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "26.1.2.3", true, com.battery.device.api.model.DetectedDeviceMetadata.empty(), lastDataAt))); when(registry.execute(eq("neware76"), any())).thenAnswer(invocation -> { @SuppressWarnings("unchecked") Function operation = (Function) invocation.getArgument(1); return operation.apply(adapter); }); DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(candidateStore, cabinetMapper, testMapper); service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource( "neware76", 8L, "新威 7.6 那台", 60000, true)); var candidate = candidateStore.snapshot(java.time.Instant.now()).cabinets().get(0); assertEquals(lastDataAt, candidate.lastDataAt()); assertEquals(lastDataAt, candidate.channels().get(0).lastDataAt()); // 一年后仍然是候选:候选只按"已登记且未删除"排除,不按时间过期。 assertEquals(1, candidateStore.snapshot(java.time.Instant.now().plusSeconds(365L * 24 * 3600)) .cabinets().size()); } }