24 KiB
Standard WCS FT546 Adapters Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 用 FT546 Admin 适配器把现有 WMS URL、旧 PLC 寄存器、设备清单和 RuoYi 页面接入标准 ruoyi-wcs,并让标准运行入口取代旧调度线程的业务所有权。
Architecture: ruoyi-admin 下新增 com.ruoyi.project.ft546,保存 WMS、设备协议、配置、运行 wiring 和页面 Controller。现有 PlcSrgvRead/PlcTsjRead/PlcSsjRead/PlcCdzRead 只保留通信与事实采集职责;核心端口实现调用它们,但状态裁决全部回到 ruoyi-wcs。
Tech Stack: Java 8, RuoYi, Spring MVC, Spring scheduling, HslCommunication Modbus, PLC4J, MyBatis, Vue 2, Element UI.
Global Constraints
- 保留
/wcs2Wms/api/receiveOutMydoJob、receiveInMydoJob、cancelMydoJob和tidyVehicleSnapshotURL。 - 保留 FT546 现有 PLC 地址和寄存器语义,不在核心出现
4000/4400/4068等地址。 Ft546LegacySrgvAdapter必须按能力 Profile 限制命令;旧 PLC 不支持命令幂等和 Prepare/Commit。- 旧队列、
default02-default08和mydo_job_wcs只作为旧接口证据,不成为新核心状态。 - 一个设备同一时刻只允许一个运动命令;
UNKNOWN时只读快照。 - 项目配置在启动时校验;有活动命令时禁止切换 Profile。
BaseSRgvThread、TsjThread和CdzThread不再创建、调度或完成标准任务。- UI 只调用 QueryService 和 RecoveryUseCase,不直接编辑核心表。
Task 1: Add FT546 project configuration and capability profiles
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546WcsProperties.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546DeviceProfile.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidator.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityGate.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityEvidence.java - Create:
ruoyi-admin/src/main/resources/ft546/wcs-device-profiles.yml - Create:
sql/wcs/V004__ft546_project_adapter_config.sql - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidatorTest.java
Interfaces:
-
Produces:
Ft546WcsProperties.getRuntimeMode/getDispatchIntervalMs/getCommandPollIntervalMs. -
Produces: device profile fields
deviceId,deviceType,adapterType,ip,port,station,capabilities,registers,profileVersion. -
Persists Admin-owned
project_device_endpoint,project_plc_address,project_protocol_profile,project_protocol_capability_evidence,project_wms_mapping. -
Ft546CapabilityGate.evaluate(profile,evidence): CapabilityGateResultdefaultsTWO_STAGE_ATOMIC_TASKto disabled and permits it only when evidence names the exact profile version, corpus manifest hash, passing test run ID, Git commit, and approval time. -
Step 1: Write failing profile-validation tests
@Test
void rejectsLegacyProfileThatClaimsPrepareCommit() {
Ft546DeviceProfile profile = legacySrgvProfile();
profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
assertThrows(IllegalArgumentException.class, () -> validator.validate(profile));
}
@Test
void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
assertThrows(IllegalArgumentException.class, () ->
validator.validate(profileWithDuplicateAddress("4000")));
}
@Test
void rejectsTwoStageCapabilityWithoutPassingEvidenceForTheExactProfile() {
Ft546DeviceProfile profile = legacySrgvProfileWith("TWO_STAGE_ATOMIC_TASK");
assertFalse(capabilityGate.evaluate(profile, noEvidence()).isPublishable());
assertEquals("TWO_STAGE_EVIDENCE_REQUIRED",
capabilityGate.evaluate(profile, noEvidence()).getReasonCode());
}
@Test
void legacyProfileKeepsUnsupportedCapabilitiesDisabled() {
CapabilityGateResult result = capabilityGate.evaluate(legacySrgvProfile(), noEvidence());
assertFalse(result.isEnabled("COMMAND_RESULT_QUERY"));
assertFalse(result.isEnabled("ORDERED_DEVICE_SNAPSHOT"));
assertFalse(result.isEnabled("CANDIDATE_BUFFER"));
assertFalse(result.isEnabled("PREPARE_COMMIT_SEGMENT"));
assertFalse(result.isEnabled("IDEMPOTENT_COMMAND_KEY"));
assertFalse(result.isEnabled("BOOT_SESSION_ID"));
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test
- Step 3: Implement strongly typed configuration
The checked-in profile contains no credentials and declares:
devices:
- deviceId: "1"
deviceType: SHUTTLE
adapterType: FT546_LEGACY_SRGV
profileVersion: 1
capabilities: [POINT_TO_POINT_COMMAND]
registers:
taskNo: "4000"
taskType: "4002"
path: "4004"
trigger: "4021"
status: "4068"
execution: "4060"
IP, port, station and WMS URLs come from database/environment configuration, not the checked-in YAML.
Persist capability evidence separately from the profile. Publishing or activating a profile fails closed when evidence is absent, expired, belongs to another profile version/hash, or reports any protocol regression failure. Active-command devices cannot change profile or evidence binding.
- Step 4: Run GREEN
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test
- Step 5: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config ruoyi-admin/src/main/resources/ft546 sql/wcs/V004__ft546_project_adapter_config.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config
git commit -m "feat(ft546): add WCS adapter configuration profiles"
Task 2: Replace fixed shuttle readers with a device runtime registry
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistry.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java
Interfaces:
-
Produces:
register(String deviceId, PlcSrgvRead runtime),requireSrgv(String deviceId),allSrgv(). -
Produces: factory
create(DeviceSrgv,Ft546DeviceProfile): PlcSrgvRead. -
Removes new-code dependence on fixed
srgv1Read/srgv2Read/srgv3Readswitches. -
Step 1: Write a failing configurable-count test
@Test
void registersAnyConfiguredNumberOfShuttleRuntimes() {
registry.register("1", runtime("1"));
registry.register("2", runtime("2"));
registry.register("9", runtime("9"));
assertEquals(3, registry.allSrgv().size());
assertSame(runtime("9").getClass(), registry.requireSrgv("9").getClass());
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test
- Step 3: Implement registry-backed startup
ScheduleThread.startAllThread() loads enabled devices, creates readers through the factory, registers them, and starts only fact-collection threads. It must not start SRgv1Thread or any task-creation thread in STANDARD mode.
- Step 4: Run GREEN and verify no three-vehicle switch remains in new paths
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test
Run: rg "case 1|case 2|case 3|srgv1Read|srgv2Read|srgv3Read" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java
Expected: no fixed vehicle selection in the new project package or Standard startup branch.
- Step 5: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
git commit -m "refactor(ft546): register shuttle runtimes from configuration"
Task 3: Implement the legacy shuttle Adapter and snapshot mapper
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvSnapshotMapper.java - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java - Create:
ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json - Create:
ruoyi-admin/src/test/resources/ft546/plc/srgv-two-stage-gate.json
Interfaces:
-
Implements:
DeviceCommandGatewayandDeviceSnapshotGatewayforSHUTTLEdevices. -
Consumes:
Ft546DeviceRuntimeRegistryand validated profile. -
Produces: existing write sequence
4004 -> 4000 -> 4002 -> 4021, plus configured optional fields. -
Step 1: Write failing protocol sequence and UNKNOWN tests
@Test
void encodesPointToPointWritesInTheExistingOrder() {
SendReceipt receipt = adapter.send(pointToPoint("CMD-1", "1", route("A", "B")));
assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
assertEquals(Arrays.asList("4004", "4000", "4002", "4021"), fakeRuntime.writtenAddresses());
}
@Test
void writeAmbiguityReturnsAcceptedUnknownInsteadOfRetryableNotSent() {
fakeRuntime.throwAfterWriting("4000");
assertEquals(SendReceipt.Status.ACCEPTED_UNKNOWN,
adapter.send(pointToPoint("CMD-1", "1", route("A", "B"))).getStatus());
}
@Test
void twoStageCommandIsRejectedUntilTheProfileGateEvidencePasses() {
assertThrows(IllegalStateException.class, () ->
adapterFor(twoStageProfile(), noEvidence()));
assertEquals(0, fakeRuntime.writtenAddresses().size());
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test
- Step 3: Implement profile-limited command encoding
Reject commands other than enabled POINT_TO_POINT_COMMAND and evidence-gated TWO_STAGE_ATOMIC_TASK. The two-stage gate fixture records the exact legacy write order, trigger semantics, disconnect boundaries, expected certainty, profile version, and manifest hash; it does not claim Prepare/Commit semantics. Never send a new command when the core reports an active UNKNOWN command for the device. Map DeviceSrgv connection, mode, status, battery, current site, task number, source, target, floor and load facts into the standard snapshot without inventing device sequence or boot ID.
- Step 4: Run protocol regression GREEN
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test
Expected: JSON sample and register sequence match, and no command completes merely because writes returned success.
- Step 5: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json
git commit -m "feat(ft546): adapt legacy shuttle PLC protocol"
Task 4: Implement lift, conveyor, and charger Adapters
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/lift/Ft546LiftAdapter.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/conveyor/Ft546ConveyorAdapter.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/charger/Ft546ChargerAdapter.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/tsj/PlcTsjRead.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/ssj/PlcSsjRead.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/cdz/PlcCdzRead.java - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546EquipmentAdaptersTest.java
Interfaces:
-
Implements standard
DeviceCommandGateway/DeviceSnapshotGatewaydispatch by device type. -
Maps lift floor/door/port facts, conveyor goods/mode/ready facts, and charger occupancy/charging/fault facts.
-
Step 1: Write failing fact-mapping and no-business-decision tests
@Test
void liftAdapterReportsFactsWithoutReleasingCoreResources() {
DeviceSnapshot snapshot = liftAdapter.readSnapshot("TSJ-1");
assertEquals("3", snapshot.fact("floor"));
assertEquals("OPEN", snapshot.fact("doorState"));
assertFalse(snapshot.getFacts().containsKey("releaseLiftResource"));
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest test
- Step 3: Implement typed protocol translation
Each adapter validates device type and enabled command, performs only configured writes/reads, and emits standard facts. Remove business entry/exit decisions from new calls to TsjThread, SsjThread, and CdzThread; keep those classes unused in Standard mode.
- Step 4: Run GREEN
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest,Ft546LegacySrgvAdapterTest test
- Step 5: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
git commit -m "feat(ft546): adapt lift conveyor and charger protocols"
Task 5: Adapt existing WMS endpoints to standard order use cases
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java
Interfaces:
-
Maps existing
ReceiveJobtoCreateTransportOrderCommand. -
Keeps existing response envelope expected by WMS.
-
Implements
WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceiptby calling the existingRequestLogServicecallback transport. -
Step 1: Write failing endpoint mapping and idempotency tests
@Test
void mapsExistingInboundRequestToCanonicalOrder() {
adapter.receiveInMydoJob(receiveJob("TASK-1", "PORT-1", "SPACE-9"));
CreateTransportOrderCommand command = useCase.lastCreateCommand();
assertEquals("TASK-1", command.getExternalRequestId());
assertEquals(TransportOrderType.INBOUND, command.getOrderType());
assertEquals("PORT-1", command.getSource());
assertEquals("SPACE-9", command.getDestination());
}
@Test
void duplicateWmsRequestReturnsTheSameOrder() {
assertEquals(adapter.receiveOutMydoJob(job).getOrderId(),
adapter.receiveOutMydoJob(job).getOrderId());
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test
- Step 3: Delegate the four preserved endpoints
Wcs2WmsController routes receiveOutMydoJob, receiveInMydoJob, cancelMydoJob, and tidyVehicleSnapshot to Ft546WmsAdapterService. The new path must not insert mydo_job_wcs or wcs_srgv_queue; unrelated legacy endpoints remain untouched.
- Step 4: Implement idempotent completion callback
Map OrderCompleted/Failed/Cancelled to existing callback payloads and use WCS eventId as the callback idempotency key. A callback transport failure leaves Outbox pending and never recreates an order.
- Step 5: Run GREEN
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test
- Step 6: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms
git commit -m "feat(ft546): adapt existing WMS contract to WCS core"
Task 6: Wire the Standard runtime and remove legacy scheduling ownership
Files:
- Create:
ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java - Create:
ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/RuntimeTickResult.java - Create:
ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/SnapshotCollector.java - Create:
ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery/RecoveryGate.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546WcsScheduler.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546AdapterConfiguration.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java - Modify:
ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime/Ft546StandardRuntimeTest.java
Interfaces:
-
Produces:
WcsRuntimeCoordinator.tick(): RuntimeTickResult. -
Scheduler calls snapshot ingest, recovery gate, dispatch, command creation, and Outbox delivery in that order.
-
Standard mode has exactly one business scheduler and one Outbox sender.
-
Consumes:
DispatchSnapshotProvider.load()andDispatchCoordinator.dispatch(DispatchSnapshot)from Plan 2. -
Step 1: Write a failing single-writer test
@Test
void standardModeStartsCoreRuntimeButNotLegacyBusinessThreads() {
runtime.start();
assertTrue(runtime.isCoreSchedulerStarted());
assertFalse(runtime.isBaseSrgvThreadStarted());
assertFalse(runtime.isTsjBusinessThreadStarted());
assertFalse(runtime.isCdzBusinessThreadStarted());
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest test
- Step 3: Implement the runtime tick
public RuntimeTickResult tick() {
snapshotCollector.collectAndAccept();
if (!recoveryGate.allowsDispatch()) {
return RuntimeTickResult.recoveryBlocked();
}
DispatchCycleResult dispatch = dispatchCoordinator.dispatch(dispatchSnapshotProvider.load());
for (GrantedDispatch granted : dispatch.getGranted()) {
executionService.createCommand(granted);
}
OutboxDispatchResult outbox = outboxDispatcher.dispatchDue(100, clock.now());
return RuntimeTickResult.of(dispatch, outbox);
}
ScheduleThread in Standard mode starts PLC fact readers only. Remove standard-mode calls that instantiate or start SRgv1Thread, TsjThread, SsjThread, or CdzThread. Keep legacy code compilable but unreachable from Standard startup.
- Step 4: Run single-writer and reactor tests GREEN
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest,Ft546WmsAdapterServiceTest,Ft546LegacySrgvAdapterTest test
Run: rg "new SRgv1Thread|new TsjThread|new CdzThread" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java
Expected: Standard branch contains no legacy business thread construction.
- Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime
git commit -m "feat(ft546): run standard WCS as the single scheduler"
Task 7: Add operations controllers and Vue monitoring pages
Files:
- Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsOrderController.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsDeviceController.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsTrafficController.java - Create:
ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsRecoveryController.java - Create:
ruoyi-ui/src/api/wcs/order.js - Create:
ruoyi-ui/src/api/wcs/device.js - Create:
ruoyi-ui/src/api/wcs/traffic.js - Create:
ruoyi-ui/src/api/wcs/recovery.js - Create:
ruoyi-ui/src/views/wcs/order/index.vue - Create:
ruoyi-ui/src/views/wcs/device/index.vue - Create:
ruoyi-ui/src/views/wcs/traffic/index.vue - Create:
ruoyi-ui/src/views/wcs/recovery/index.vue - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web/WcsOperationsControllerTest.java
Interfaces:
-
GET
/wcs/orders,/wcs/devices,/wcs/traffic/resources,/wcs/recovery/cases. -
POST
/wcs/orders/{id}/suspend,/resume,/cancel. -
POST
/wcs/recovery/cases/{id}/confirmand/abortwith operator reason/evidence. -
Step 1: Write failing controller authorization and DTO tests
@Test
@WithMockUser(authorities = "wcs:recovery:confirm")
void confirmsRecoveryThroughTheUseCase() throws Exception {
mockMvc.perform(post("/wcs/recovery/cases/RC-1/confirm")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"reason\":\"现场确认车辆位于070501\",\"currentSite\":\"070501\",\"loaded\":false}"))
.andExpect(status().isOk());
verify(recoveryUseCase).confirmFacts(eq("RC-1"), anyString(), any(RecoveryEvidence.class));
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test
- Step 3: Implement controllers and API clients
Controllers return stable QueryService DTOs and use RuoYi permissions. Vue pages provide filters, tables, state tags, detail drawers, pagination and explicit recovery dialogs; they do not include generic CRUD edit/delete for core records.
- Step 4: Implement the traffic view without editing core state
Show resource key, type, capacity, protected count, holder, reservation state and evidence ID. Only Recovery page exposes controlled confirm/abort commands.
- Step 5: Run backend and frontend verification
Run: mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test
Run: cd ruoyi-ui && npm run build:prod
- Step 6: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web ruoyi-ui/src/api/wcs ruoyi-ui/src/views/wcs
git commit -m "feat(ft546): add WCS operations console"
Task 8: Add menus, permissions, and the FT546 end-to-end contract test
Files:
- Create:
sql/wcs/V005__ft546_wcs_menus_and_permissions.sql - Create:
ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java - Create:
ruoyi-admin/src/test/resources/ft546/scenarios/inbound-same-floor.json - Create:
ruoyi-admin/src/test/resources/ft546/scenarios/outbound-cross-floor.json
Interfaces:
-
Permissions:
wcs:order:list,wcs:order:control,wcs:device:list,wcs:traffic:list,wcs:recovery:list,wcs:recovery:confirm. -
End-to-end test uses fake PLC runtimes and fake WMS callback transport through real Spring wiring.
-
Step 1: Write the failing same-floor scenario
@Test
void completesInboundThroughExistingWmsUrlAndLegacyShuttleAdapter() throws Exception {
String orderId = postInboundScenario("inbound-same-floor.json");
runTicksUntilTerminal(orderId, 200);
assertEquals("COMPLETED", operations.order(orderId).getOrderState());
assertEquals(1, callbackTransport.businessEffectCount(orderId));
assertEquals(0, invariantMonitor.totalViolations());
}
- Step 2: Run RED
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test
- Step 3: Add permission SQL and scenario fixtures
Insert one WCS menu group and four child pages using existing RuoYi sys_menu conventions. Use idempotent IF NOT EXISTS checks keyed by permission string.
- Step 4: Make same-floor and cross-floor scenarios GREEN
The cross-floor test must prove source port, lift and target port joint reservation; WMS receives one completion event; no legacy queue row is created.
Run: mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test
- Step 5: Run full Plan 4 verification
Run: mvn -pl ruoyi-admin -am test
Run: cd ruoyi-ui && npm run build:prod
Run: git diff --check
- Step 6: Commit
git add sql/wcs/V005__ft546_wcs_menus_and_permissions.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java ruoyi-admin/src/test/resources/ft546/scenarios
git commit -m "test(ft546): verify standard WCS end to end"