feat(wcs): persist and query execution recovery state
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.wcs.infrastructure.persistence.recovery;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ExecutionRecoveryMigrationTest {
|
||||
|
||||
@Test
|
||||
void packagesExecutionRecoveryTablesAndIdempotencyIndexes() throws Exception {
|
||||
String migration = readSql("V003__wcs_execution_and_recovery.sql");
|
||||
|
||||
assertContains(migration, "wcs_device_command");
|
||||
assertContains(migration, "wcs_device_execution");
|
||||
assertContains(migration, "wcs_command_outbox");
|
||||
assertContains(migration, "wcs_wms_notification_outbox");
|
||||
assertContains(migration, "wcs_device_snapshot");
|
||||
assertContains(migration, "wcs_device_event");
|
||||
assertContains(migration, "wcs_alarm");
|
||||
assertContains(migration, "wcs_recovery_case");
|
||||
assertContains(migration, "wcs_manual_action_audit");
|
||||
assertContains(migration, "wcs_control_lease");
|
||||
assertContains(migration, "uk_wcs_recovery_case_open");
|
||||
assertContains(migration, "WHERE recovery_state = 'OPEN'");
|
||||
assertContains(migration, "uk_wcs_wms_outbox_event");
|
||||
assertContains(migration, "ix_wcs_device_command_order_step");
|
||||
assertContains(migration, "ix_wcs_resource_reservation_resource_state");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesTheExecutionRecoveryVerificationScript() throws Exception {
|
||||
String verification = readSql(
|
||||
"verify_V003__wcs_execution_and_recovery.sql");
|
||||
|
||||
assertContains(verification, "wcs_device_command");
|
||||
assertContains(verification, "uk_wcs_recovery_case_open");
|
||||
assertContains(verification, "uk_wcs_wms_outbox_event");
|
||||
}
|
||||
|
||||
private static void assertContains(String sql, String token) {
|
||||
assertTrue(sql.contains(token), "missing SQL token: " + token);
|
||||
}
|
||||
|
||||
private static String readSql(String filename) throws Exception {
|
||||
Path current = Paths.get(System.getProperty(
|
||||
"maven.multiModuleProjectDirectory", "."))
|
||||
.toAbsolutePath().normalize();
|
||||
while (current != null) {
|
||||
Path file = current.resolve(Paths.get("sql", "wcs", filename));
|
||||
if (Files.isRegularFile(file)) {
|
||||
return new String(Files.readAllBytes(file),
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
throw new java.nio.file.NoSuchFileException(filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.ruoyi.wcs.query;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class WcsOperationsQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void returnsCommandAndRecoveryCorrelationForAnOrder() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
new FakeReadRepository());
|
||||
|
||||
OrderExecutionView view = queries.order("ORDER-1");
|
||||
|
||||
assertEquals("CMD-1", view.getCurrentCommandKey());
|
||||
assertEquals("RC-1", view.getRecoveryCaseId());
|
||||
assertEquals("UNKNOWN", view.getCommandState());
|
||||
assertEquals("VEH-1", view.getDeviceId());
|
||||
assertEquals("ORDER-1", queries.command("CMD-1").getOrderId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesStableDeviceResourceAndRecoveryViews() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
new FakeReadRepository());
|
||||
|
||||
assertEquals("ALARM-1", queries.device("VEH-1").getActiveAlarmCode());
|
||||
assertEquals(77L, queries.resource("NODE:A").getFencingToken());
|
||||
assertEquals("TASK_NUMBER_MISMATCH",
|
||||
queries.recoveryCase("RC-1").getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesRecoveryCasesByExplicitStateWithABoundedLimit() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
new FakeReadRepository());
|
||||
|
||||
List<RecoveryCaseView> page = queries.recoveryCases(
|
||||
"OPEN", 0, 50);
|
||||
|
||||
assertEquals(Arrays.asList("RC-1", "RC-2"), Arrays.asList(
|
||||
page.get(0).getCaseId(), page.get(1).getCaseId()));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> page.add(page.get(0)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> queries.recoveryCases("OPEN", 0, 501));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOrBlankOperationsLookupFailsExplicitly() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
new FakeReadRepository());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> queries.order(" "));
|
||||
assertThrows(DomainRuleViolation.class,
|
||||
() -> queries.order("ORDER-MISSING"));
|
||||
}
|
||||
|
||||
private static final class FakeReadRepository
|
||||
implements WcsOperationsReadRepository {
|
||||
@Override
|
||||
public OrderExecutionView findOrder(String orderId) {
|
||||
if (!"ORDER-1".equals(orderId)) {
|
||||
return null;
|
||||
}
|
||||
return new OrderExecutionView("ORDER-1", "WF-1", "STEP-1",
|
||||
"VEH-1", "CMD-1", "UNKNOWN", "RC-1", Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderExecutionView findCommand(String commandKey) {
|
||||
return "CMD-1".equals(commandKey) ? findOrder("ORDER-1") : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceOperationsView findDevice(String deviceId) {
|
||||
return new DeviceOperationsView("VEH-1", "RGV", "CMD-1",
|
||||
"UNKNOWN", "NODE:A", true, "ALARM-1", "RC-1",
|
||||
Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceOperationsView findResource(String resourceKey) {
|
||||
return new ResourceOperationsView("NODE:A", "OCCUPIED",
|
||||
"ORDER-1", "STEP-1", "VEH-1", 77L, null,
|
||||
Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecoveryCaseView findRecoveryCase(String caseId) {
|
||||
return recovery(caseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecoveryCaseView> findRecoveryCases(String state,
|
||||
int offset,
|
||||
int limit) {
|
||||
return Arrays.asList(recovery("RC-1"), recovery("RC-2"));
|
||||
}
|
||||
|
||||
private static RecoveryCaseView recovery(String caseId) {
|
||||
return new RecoveryCaseView(caseId, "VEH-1", "CMD-1", "OPEN",
|
||||
"TASK_NUMBER_MISMATCH", Instant.EPOCH, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user