package com.ruoyi.shadow.kernel; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.ft546.protocol.Ft546DeviceProfile; import com.ruoyi.ft546.protocol.Ft546EncodedCommand; import com.ruoyi.ft546.protocol.Ft546SrgvCommandEncoder; import com.ruoyi.ft546.protocol.Ft546SrgvCommandIntent; import com.ruoyi.shadow.facts.ShadowFactBatch; import com.ruoyi.shadow.facts.ShadowFactBatchValidator; import com.ruoyi.shadow.facts.ShadowMapFact; import com.ruoyi.shadow.facts.ShadowQueueFact; import com.ruoyi.shadow.facts.ShadowVehicleFact; import com.ruoyi.shadow.evidence.ShadowCycleEvidence; import com.ruoyi.shadow.evidence.ShadowCycleEvidenceCollector; import com.ruoyi.shadow.evidence.ShadowEvidenceDigest; import com.ruoyi.shadow.evidence.ShadowEvidenceException; import com.ruoyi.shadow.evidence.ShadowEvidenceRepository; import com.ruoyi.shadow.evidence.ShadowEvidenceStorageHealth; import com.ruoyi.shadow.startup.ShadowStartupReport; import com.ruoyi.wcs.application.ReadOnlyDispatchCycle; import com.ruoyi.wcs.application.ShadowDispatchResult; import com.ruoyi.wcs.application.dispatch.DispatchCoordinator; import com.ruoyi.wcs.application.dispatch.DispatchCycleResult; import com.ruoyi.wcs.application.dispatch.DispatchSnapshot; import com.ruoyi.wcs.application.dispatch.GrantedDispatch; import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy; import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner; import com.ruoyi.wcs.domain.traffic.DeadlockGuard; import com.ruoyi.wcs.domain.traffic.ResourceLedger; import com.ruoyi.wcs.domain.traffic.RoutePlan; import com.ruoyi.wcs.domain.traffic.RoutePlanner; import com.ruoyi.wcs.domain.traffic.WaitForGraph; import com.ruoyi.wcs.port.out.DispatchPolicy; import org.junit.jupiter.api.Test; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class Ft546ShadowCycleTest { @Test void ordinaryGrantUsesTheLegacyMainRegisterSequence() { Ft546EncodedCommand encoded = encode(grant("VEH-2", points("70511", "70512"), parameters("1", null, null))); assertEquals(Arrays.asList("4004", "4000", "4002", "4021"), addresses(encoded)); assertEquals("VEH-2", encoded.getDeviceId()); assertEquals(points(70511, 70512), encoded.getPathPoints()); } @Test void crossFloorGrantAddsOnlyTheLegacyCrossFloorRegisters() { Ft546EncodedCommand encoded = encode(grant("VEH-3", points("70501", "70502"), parameters("8", "8", null))); assertEquals(Arrays.asList("4004", "4000", "4002", "4021", "4020", "4022"), addresses(encoded)); } @Test void twoStageGrantsUseTheMatchingLegacyRegisterSequences() { Ft546EncodedCommand first = encode(grant("VEH-1", points("70501", "70503"), parameters("1", null, "1"))); Ft546EncodedCommand second = encode(grant("VEH-1", points("70503", "70504"), parameters("1", null, "2"))); assertEquals(Arrays.asList("4404", "4422", "4402", "4400"), addresses(first)); assertEquals(Arrays.asList("4404", "4422", "4423", "4402", "4400"), addresses(second)); } @Test void malformedExplicitPhaseParametersFailBeforeAnythingIsRecorded() { RecordingFt546CommandGateway gateway = gateway(); ShadowCommandIntentFactory factory = new ShadowCommandIntentFactory(); GrantedDispatch invalid = grant("VEH-1", points("70501", "70502"), parameters("8", null, null)); assertThrows(IllegalArgumentException.class, () -> gateway.record(factory.create(invalid))); assertEquals(Collections.emptyList(), gateway.getRecorded()); } @Test void recorderKeepsImmutableEncodedEvidenceWithoutSending() { RecordingFt546CommandGateway gateway = gateway(); Ft546SrgvCommandIntent intent = new ShadowCommandIntentFactory().create( grant("VEH-1", points("70501", "70502"), parameters("1", null, null))); Ft546EncodedCommand encoded = gateway.record(intent); assertEquals(Collections.singletonList(encoded), gateway.getRecorded()); assertThrows(UnsupportedOperationException.class, () -> gateway.getRecorded().clear()); } @Test void assemblerUsesOnlyActiveQueueAndIndependentVehicleFacts() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Arrays.asList(queue("shadow_ro.v_wcs_srgv_queue", "ACTIVE", now), queue("shadow_ro.v_wcs_srgv_queue_finish", "FINISH", now)), Collections.singletonList(vehicle(now)), maps()); ShadowDispatchSnapshotAssembler assembler = new ShadowDispatchSnapshotAssembler("FT546", 3L, topology -> new ShadowTrafficState( new ResourceLedger(topology), new WaitForGraph(), 9L, Collections.emptyMap()), Clock.fixed(now, ZoneOffset.UTC)); DispatchSnapshot snapshot = assembler.assemble(batch); assertEquals(1, snapshot.getReadySteps().size()); assertEquals("ACTIVE", snapshot.getReadySteps().get(0).getStepId()); assertEquals("70501", snapshot.getReadySteps().get(0).getSourceNodeId()); assertEquals("70502", snapshot.getReadySteps().get(0).getDestinationNodeId()); assertEquals("30001", snapshot.getReadySteps().get(0) .getCommandParameters().get("parentTaskId")); assertEquals(1, snapshot.getVehicleSnapshots().size()); assertEquals("VEH-9", snapshot.getVehicleSnapshots().get(0) .getVehicleId()); assertEquals(9L, snapshot.getSnapshotVersion()); assertEquals(Arrays.asList("70502"), snapshot.getTopology().neighbors("70501")); } @Test void invalidFactsProduceNoDispatchIntentOrEncodedEvidence() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); ShadowFactBatch invalid = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList( queue("shadow_ro.v_wcs_srgv_queue", "ACTIVE", now)), Collections.emptyList(), maps()); AtomicInteger dispatchCalls = new AtomicInteger(); ReadOnlyDispatchCycle readOnly = new ReadOnlyDispatchCycle( snapshot -> { dispatchCalls.incrementAndGet(); return DispatchCycleResult.retrySnapshotChanged(); }, new ShadowCommandIntentFactory()::create); RecordingFt546CommandGateway gateway = gateway(); Ft546ShadowCycle cycle = new Ft546ShadowCycle( new ShadowFactBatchValidator(Duration.ofSeconds(4), Duration.ofSeconds(30)), batch -> { throw new AssertionError("invalid facts must not assemble"); }, readOnly, gateway); ShadowCycleResult result = cycle.run(invalid, now); assertEquals("INSUFFICIENT_FACTS", result.getStatusCode()); assertEquals("MISSING_VEHICLE_FACTS", result.getReasonCode()); assertEquals(0, dispatchCalls.get()); assertEquals(Collections.emptyList(), result.getIntents()); assertEquals(Collections.emptyList(), result.getEncodedCommands()); assertEquals(Collections.emptyList(), gateway.getRecorded()); } @Test void validFactsRunTheRealKernelThroughVirtualRegisterEvidence() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); AtomicInteger commitCalls = new AtomicInteger(); AtomicInteger generatedIds = new AtomicInteger(); RoutePlanner routePlanner = new AStarRoutePlanner(); DispatchCoordinator coordinator = new DispatchCoordinator( new DefaultVehicleAssignmentPolicy(routePlanner, DispatchPolicy.NONE), new DeadlockGuard(new WaitForGraph(), routePlanner), commit -> { commitCalls.incrementAndGet(); return true; }, () -> "SHADOW-" + generatedIds.incrementAndGet()); ReadOnlyDispatchCycle readOnly = new ReadOnlyDispatchCycle( coordinator::dispatch, new ShadowCommandIntentFactory()::create); ShadowDispatchSnapshotAssembler assembler = new ShadowDispatchSnapshotAssembler("FT546", 3L, topology -> new ShadowTrafficState( new ResourceLedger(topology), new WaitForGraph(), 0L, Collections.emptyMap()), Clock.fixed(now, ZoneOffset.UTC)); RecordingFt546CommandGateway gateway = gateway(); Ft546ShadowCycle cycle = new Ft546ShadowCycle( new ShadowFactBatchValidator(Duration.ofSeconds(4), Duration.ofSeconds(30)), assembler::assemble, readOnly, gateway); ShadowCycleResult result = cycle.run(batch, now); assertEquals("COMPARABLE", result.getStatusCode()); assertEquals(1, result.getDecisions().getGranted().size()); assertEquals("VEH-9", result.getDecisions().getGranted().get(0) .getVehicleId()); assertEquals("ACTIVE", result.getDecisions().getGranted().get(0) .getStepId()); assertEquals(1, commitCalls.get()); assertEquals(1, result.getIntents().size()); assertEquals(1, result.getEncodedCommands().size()); assertEquals("VEH-9", result.getEncodedCommands().get(0) .getDeviceId()); assertEquals(Arrays.asList("4004", "4000", "4002", "4021"), addresses(result.getEncodedCommands().get(0))); assertEquals(1, gateway.getRecorded().size()); } @Test void completeCyclePersistsEvidenceInsideOuterTransactionBeforeRecording() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); RecordingEvidenceRepository evidence = new RecordingEvidenceRepository(); ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap()); RecordingFt546CommandGateway gateway = gateway(); Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); ShadowCycleResult result = cycle.run(batch, now); assertEquals("COMPARABLE", result.getStatusCode()); assertEquals(1, evidence.saved.size()); assertEquals(1, gateway.getRecorded().size()); assertTrue(evidence.dispatchObservedInsideTransaction); assertEquals(ShadowStartupReport.Status.READY, report.getStatus()); assertEquals(now, report.getLastCompletedCycle()); } @Test void evidenceCompletionAndReadyTimeIncludeWorkInsteadOfReusingEntryTime() { Instant now = Instant.parse("2026-09-06T23:59:59Z"); java.util.concurrent.atomic.AtomicReference time = new java.util.concurrent.atomic.AtomicReference(now); Clock clock = new Clock() { public java.time.ZoneId getZone() { return ZoneOffset.UTC; } public Clock withZone(java.time.ZoneId zone) { return this; } public Instant instant() { return time.get(); } }; RecordingEvidenceRepository evidence = new RecordingEvidenceRepository(); java.util.concurrent.atomic.AtomicLong nanos = new java.util.concurrent.atomic.AtomicLong(); ShadowCycleDiagnostics progress = new ShadowCycleDiagnostics(nanos::get); evidence.beforeCommit = () -> { time.set(now.plusSeconds(3)); nanos.set(3000000000L); }; ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap()); Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway(), clock, () -> { time.set(now.plusSeconds(2)); nanos.set(2000000000L); }); ShadowFactBatch batch = ShadowFactBatch.create(now.minusMillis(50), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); assertEquals("COMPARABLE", cycle.run(batch, now, progress).getStatusCode()); assertEquals(now.plusSeconds(2), evidence.saved.get(0).getCompletedAt()); assertEquals(now.plusSeconds(3), report.getLastCompletedCycle()); assertEquals(Long.valueOf(3000), report.getSourceLagMillis()); Map timings = (Map) progress.finish("COMPARABLE", null).get("phaseMillis"); assertEquals(1000L, timings.get("TRANSACTION_COMMIT")); assertEquals(2000L, timings.get("DISPATCH_AND_STATE_SAVE")); } @Test void evidenceFailureRecordsNoCommandAndStopsLaterCalculation() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); RecordingEvidenceRepository evidence = new RecordingEvidenceRepository(); evidence.saveFailure = new ShadowEvidenceException( "EVIDENCE_WRITE_FAILED"); ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap()); RecordingFt546CommandGateway gateway = gateway(); Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); ShadowCycleResult first = cycle.run(batch, now); ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1)); assertEquals("DEGRADED", first.getStatusCode()); assertEquals("EVIDENCE_STORAGE_UNAVAILABLE", first.getReasonCode()); assertEquals("DEGRADED", second.getStatusCode()); assertEquals(1, evidence.transactionCalls.get()); assertEquals(Collections.emptyList(), gateway.getRecorded()); assertEquals(ShadowStartupReport.Status.DEGRADED, report.getStatus()); assertEquals("EVIDENCE_STORAGE_UNAVAILABLE", report.getReasonCode()); } @Test void evidenceCollisionKeepsItsDistinctFailClosedReason() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); RecordingEvidenceRepository evidence = new RecordingEvidenceRepository(); evidence.saveFailure = new ShadowEvidenceException( "EVIDENCE_KEY_COLLISION"); ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap()); RecordingFt546CommandGateway gateway = gateway(); Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); ShadowCycleResult first = cycle.run(batch, now); ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1)); assertEquals("DEGRADED", first.getStatusCode()); assertEquals("EVIDENCE_KEY_COLLISION", first.getReasonCode()); assertEquals("EVIDENCE_KEY_COLLISION", second.getReasonCode()); assertEquals("EVIDENCE_KEY_COLLISION", report.getReasonCode()); assertEquals(1, evidence.transactionCalls.get()); assertEquals(Collections.emptyList(), gateway.getRecorded()); } @Test void unhealthyStorageDegradesBeforeAssemblyAndIsNotRechecked() { Instant now = Instant.parse("2026-08-14T06:00:00Z"); AtomicInteger healthChecks = new AtomicInteger(); AtomicInteger assemblyCalls = new AtomicInteger(); AtomicInteger dispatchCalls = new AtomicInteger(); ShadowEvidenceStorageHealth health = () -> { healthChecks.incrementAndGet(); return ShadowEvidenceStorageHealth.Check.unavailable( "FREE_SPACE_BELOW_MINIMUM"); }; RecordingEvidenceRepository evidence = new RecordingEvidenceRepository(); ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap()); Ft546ShadowCycle cycle = new Ft546ShadowCycle( new ShadowFactBatchValidator(Duration.ofSeconds(4), Duration.ofSeconds(30)), batch -> { assemblyCalls.incrementAndGet(); throw new AssertionError("must not assemble"); }, new ReadOnlyDispatchCycle(snapshot -> { dispatchCalls.incrementAndGet(); return DispatchCycleResult.retrySnapshotChanged(); }, new ShadowCommandIntentFactory()::create), gateway(), health, evidence, collector(evidence.runId, 3L), report); ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now, Collections.singletonList(queueWithoutLegacyAssignment(now)), Collections.singletonList(vehicle(now)), maps()); ShadowCycleResult first = cycle.run(batch, now); ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1)); assertEquals("DEGRADED", first.getStatusCode()); assertEquals("FREE_SPACE_BELOW_MINIMUM", first.getReasonCode()); assertEquals("DEGRADED", second.getStatusCode()); assertEquals(1, healthChecks.get()); assertEquals(0, assemblyCalls.get()); assertEquals(0, dispatchCalls.get()); assertEquals(0, evidence.transactionCalls.get()); } private static Ft546ShadowCycle evidenceCycle( Instant now, RecordingEvidenceRepository evidence, ShadowStartupReport report, RecordingFt546CommandGateway gateway) { return evidenceCycle(now, evidence, report, gateway, Clock.fixed(now, ZoneOffset.UTC), () -> { }); } private static Ft546ShadowCycle evidenceCycle( Instant now, RecordingEvidenceRepository evidence, ShadowStartupReport report, RecordingFt546CommandGateway gateway, Clock clock, Runnable onDispatch) { RoutePlanner routePlanner = new AStarRoutePlanner(); DispatchCoordinator coordinator = new DispatchCoordinator( new DefaultVehicleAssignmentPolicy(routePlanner, DispatchPolicy.NONE), new DeadlockGuard(new WaitForGraph(), routePlanner), commit -> { evidence.dispatchObservedInsideTransaction = evidence.insideTransaction; onDispatch.run(); return true; }, () -> "SHADOW-1"); ReadOnlyDispatchCycle readOnly = new ReadOnlyDispatchCycle( coordinator::dispatch, new ShadowCommandIntentFactory()::create); ShadowDispatchSnapshotAssembler assembler = new ShadowDispatchSnapshotAssembler("FT546", 3L, topology -> new ShadowTrafficState( new ResourceLedger(topology), new WaitForGraph(), 0L, Collections.emptyMap()), Clock.fixed(now, ZoneOffset.UTC)); return new Ft546ShadowCycle( new ShadowFactBatchValidator(Duration.ofSeconds(4), Duration.ofSeconds(30)), assembler::assemble, readOnly, gateway, () -> ShadowEvidenceStorageHealth.Check.available( Long.MAX_VALUE), evidence, collector(evidence.runId, 3L), report, clock); } private static ShadowCycleEvidenceCollector collector( UUID runId, long routeVersion) { return new ShadowCycleEvidenceCollector(runId, routeVersion, new ObjectMapper().findAndRegisterModules()); } private static Ft546EncodedCommand encode(GrantedDispatch grant) { RecordingFt546CommandGateway gateway = gateway(); Ft546SrgvCommandIntent intent = new ShadowCommandIntentFactory().create(grant); return gateway.record(intent); } private static RecordingFt546CommandGateway gateway() { return new RecordingFt546CommandGateway(new Ft546SrgvCommandEncoder(), profile(), capabilities("POINT_TO_POINT_COMMAND", "TWO_STAGE_ATOMIC_TASK")); } private static GrantedDispatch grant(String vehicleId, List routeNodes, Map parameters) { RoutePlan route = new RoutePlan(routeNodes, resourceKeys(routeNodes), 1, 0, 7L); return new GrantedDispatch("PARENT-1", "STEP-1", vehicleId, route, Collections.singletonList("RES-1"), 7L, 11L, parameters); } private static Map parameters(String taskType, String crossFloorTarget, String stage) { Map values = new LinkedHashMap(); values.put("parentTaskId", "PARENT-1"); values.put("phase", stage == null ? "LOADED_TO_PLACE" : "TWO_STAGE_" + stage); values.put("routeVersion", "3"); values.put("taskNo", "30001"); values.put("taskType", taskType); if (crossFloorTarget != null) { values.put("crossFloorTarget", crossFloorTarget); } if (stage != null) { values.put("stage", stage); } return values; } private static List resourceKeys(List nodes) { return Arrays.asList("NODE:" + nodes.get(0), "EDGE:" + nodes.get(0) + ':' + nodes.get(1), "NODE:" + nodes.get(1)); } private static Ft546DeviceProfile profile() { Ft546DeviceProfile profile = new Ft546DeviceProfile(); profile.setDeviceId("SHADOW"); Map registers = new LinkedHashMap(); registers.put("taskNo", "4000"); registers.put("taskType", "4002"); registers.put("path", "4004"); registers.put("trigger", "4021"); registers.put("crossFloorFlag", "4020"); registers.put("crossFloorTarget", "4022"); registers.put("secondTaskNo", "4400"); registers.put("secondTaskType", "4402"); registers.put("secondPath", "4404"); registers.put("secondStageFlag", "4422"); registers.put("secondComplete", "4423"); profile.setRegisters(registers); profile.setCapabilities(capabilities("POINT_TO_POINT_COMMAND", "TWO_STAGE_ATOMIC_TASK")); return profile; } private static Set capabilities(String... values) { return new LinkedHashSet(Arrays.asList(values)); } private static List points(String... values) { return Arrays.asList(values); } private static List points(Integer... values) { return Arrays.asList(values); } private static List addresses(Ft546EncodedCommand encoded) { java.util.ArrayList values = new java.util.ArrayList(); encoded.getWrites().forEach(write -> values.add(write.getAddress())); return values; } private static ShadowQueueFact queue(String table, String guid, Instant createdAt) { return ShadowQueueFact.builder(table, guid) .taskNo("30001") .taskType("2") .priority(2L) .cargoPickSite("70501") .cargoPlaceSite("70502") .startSite("70501") .endSite("70502") .createdAt(createdAt.minusSeconds(10)) .agvId("LEGACY-VEHICLE-MUST-NOT-BE-CANDIDATE") .pathInfo("99999,99998") .turnPath("99997") .default02("1") .build(); } private static ShadowQueueFact queueWithoutLegacyAssignment( Instant createdAt) { return ShadowQueueFact.builder("shadow_ro.v_wcs_srgv_queue", "ACTIVE") .taskNo("30001") .taskType("2") .priority(2L) .cargoPickSite("70501") .cargoPlaceSite("70502") .startSite("70501") .endSite("70502") .createdAt(createdAt.minusSeconds(10)) .default02("1") .build(); } private static ShadowVehicleFact vehicle(Instant capturedAt) { return ShadowVehicleFact.builder("VEH-GUID", "VEH-9") .agvStatus("1") .currentSite("70501") .battery(80) .workMode(5) .alarm(0) .status(1) .errorCode(0) .allowSend(1) .currentFloor(1) .capturedAt(capturedAt) .build(); } private static List maps() { return Arrays.asList( new ShadowMapFact("MAP-1", "70501", 0, 0, 1, 1), new ShadowMapFact("MAP-2", "70502", 1, 0, 1, 1)); } private static final class RecordingEvidenceRepository implements ShadowEvidenceRepository { private final UUID runId = UUID.randomUUID(); private final List saved = new java.util.ArrayList(); private final AtomicInteger transactionCalls = new AtomicInteger(); private boolean insideTransaction; private boolean dispatchObservedInsideTransaction; private RuntimeException saveFailure; private Runnable beforeCommit = () -> { }; @Override public SavedCycle save(ShadowCycleEvidence evidence) { return saveAll(Collections.singletonList(evidence)).get(0); } @Override public List saveAll(List values) { assertTrue(insideTransaction, "evidence must be saved inside the outer transaction"); if (saveFailure != null) { throw saveFailure; } List result = new java.util.ArrayList(); for (ShadowCycleEvidence value : values) { saved.add(value); result.add(new SavedCycle(value.getCycleId(), value.getEvidenceHash(), value.getStatus(), value.getReasonCode())); } return result; } @Override public T inTransaction(Supplier work) { transactionCalls.incrementAndGet(); insideTransaction = true; try { T result = work.get(); beforeCommit.run(); return result; } finally { insideTransaction = false; } } @Override public SavedCycle findCycle(UUID runId, String inputBatchHash, String parentTaskId, String phase, long routeVersion) { return null; } @Override public ShadowEvidenceDigest dailyDigest( java.time.LocalDate date, java.time.ZoneId zoneId) { throw new UnsupportedOperationException(); } @Override public boolean isHealthy() { return true; } } }