feat(wcs): add device command and outbox lifecycle
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package com.ruoyi.wcs.domain.execution;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
import com.ruoyi.wcs.port.out.WmsNotification;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class DeviceCommandTest {
|
||||
|
||||
@Test
|
||||
void sentCommandBecomesUnknownInsteadOfRetryableFailure() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
|
||||
command.markSent("SEND-1", at(1));
|
||||
command.markUnknown("ACK_TIMEOUT", at(6));
|
||||
|
||||
assertEquals(DeviceCommandState.UNKNOWN, command.getState());
|
||||
assertEquals("ACK_TIMEOUT", command.getStatusReason());
|
||||
assertFalse(command.canSendMotionCommandAfterIt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownCommandRequiresMatchingPhysicalEvidenceToSucceed() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
command.markSent("SEND-1", at(1));
|
||||
command.markUnknown("ACK_TIMEOUT", at(2));
|
||||
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> command.succeed("FORGED-DONE", at(3)));
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> command.succeedAfterRecovery(
|
||||
CommandResolutionEvidence.physicalCompletion(
|
||||
"OTHER-COMMAND", "DONE-1"), at(3)));
|
||||
|
||||
command.succeedAfterRecovery(
|
||||
CommandResolutionEvidence.physicalCompletion(
|
||||
"CMD-KEY-1", "DONE-1"), at(3));
|
||||
|
||||
assertEquals(DeviceCommandState.SUCCEEDED, command.getState());
|
||||
assertEquals("DONE-1", command.getCompletionEvidenceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleTransitionTimeCannotPartiallyMutateTheCommand() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
command.markSent("SEND-1", at(5));
|
||||
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> command.acknowledge("STALE-ACK", at(4)));
|
||||
|
||||
assertEquals(DeviceCommandState.SENT, command.getState());
|
||||
assertNull(command.getAcknowledgementEvidenceId());
|
||||
assertEquals(at(5), command.getUpdatedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalCommandCannotBeAcknowledgedAgain() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
command.markSent("SEND-1", at(1));
|
||||
command.acknowledge("ACK-1", at(2));
|
||||
command.succeed("DONE-1", at(3));
|
||||
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> command.acknowledge("ACK-2", at(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyPendingCommandCanFailBeforeSend() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
command.markSent("SEND-1", at(1));
|
||||
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> command.failBeforeSend("SOCKET_CLOSED", at(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateCommandKeyAndAttemptStayStableAcrossOutboxCreation() {
|
||||
DeviceCommand command = pendingCommand();
|
||||
CommandOutboxEntry outbox = CommandOutboxEntry.pending(
|
||||
command.getCommandId(), command.getCommandKey(),
|
||||
command.getPayload(), at(5));
|
||||
|
||||
assertEquals("CMD-KEY-1", outbox.getCommandKey());
|
||||
assertEquals("CMD-1", outbox.getCommandId());
|
||||
assertEquals(at(5), outbox.getNextAttemptAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void wmsOutboxUsesTheBusinessEventAsItsIdempotencyKey() {
|
||||
WmsNotification notification = new WmsNotification(
|
||||
"EVENT-1", "REQ-1", "ORDER-1", at(0),
|
||||
"OrderCompleted", "OK", null,
|
||||
Collections.<String, String>emptyMap());
|
||||
|
||||
WmsNotificationOutboxEntry entry = WmsNotificationOutboxEntry.pending(
|
||||
"EVENT-1", notification, at(7));
|
||||
|
||||
assertEquals("EVENT-1", entry.getEventId());
|
||||
assertEquals("EVENT-1", entry.getNotification().getEventId());
|
||||
assertEquals(at(7), entry.getNextAttemptAt());
|
||||
}
|
||||
|
||||
private static DeviceCommand pendingCommand() {
|
||||
return DeviceCommand.pending("CMD-1", "CMD-KEY-1", "V1",
|
||||
DeviceCommandType.MOVE_ROUTE, "{\"route\":[\"A\",\"B\"]}",
|
||||
1, at(0));
|
||||
}
|
||||
|
||||
private static Instant at(long seconds) {
|
||||
return Instant.EPOCH.plusSeconds(seconds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user