Files
FT546-WCS/ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/kernel/Ft546ShadowCycleTest.java

645 lines
29 KiB
Java

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<Ft546SrgvCommandIntent> readOnly =
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(
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<Ft546SrgvCommandIntent> readOnly =
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(
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<Instant> time =
new java.util.concurrent.atomic.AtomicReference<Instant>(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<Ft546SrgvCommandIntent>(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<Ft546SrgvCommandIntent> readOnly =
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(
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<String> routeNodes,
Map<String, String> 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<String, String> parameters(String taskType,
String crossFloorTarget,
String stage) {
Map<String, String> values = new LinkedHashMap<String, String>();
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<String> resourceKeys(List<String> 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<String, String> registers = new LinkedHashMap<String, String>();
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<String> capabilities(String... values) {
return new LinkedHashSet<String>(Arrays.asList(values));
}
private static List<String> points(String... values) {
return Arrays.asList(values);
}
private static List<Integer> points(Integer... values) {
return Arrays.asList(values);
}
private static List<String> addresses(Ft546EncodedCommand encoded) {
java.util.ArrayList<String> values = new java.util.ArrayList<String>();
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<ShadowMapFact> 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<ShadowCycleEvidence> saved =
new java.util.ArrayList<ShadowCycleEvidence>();
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<SavedCycle> saveAll(List<ShadowCycleEvidence> values) {
assertTrue(insideTransaction,
"evidence must be saved inside the outer transaction");
if (saveFailure != null) {
throw saveFailure;
}
List<SavedCycle> result = new java.util.ArrayList<SavedCycle>();
for (ShadowCycleEvidence value : values) {
saved.add(value);
result.add(new SavedCycle(value.getCycleId(),
value.getEvidenceHash(), value.getStatus(),
value.getReasonCode()));
}
return result;
}
@Override
public <T> T inTransaction(Supplier<T> 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;
}
}
}