feat(wcs): add device command and outbox lifecycle

This commit is contained in:
zgw
2026-08-11 10:06:10 +08:00
parent 74f2de59cd
commit 0152fb1a81
11 changed files with 560 additions and 0 deletions

View File

@@ -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);
}
}