1035 lines
50 KiB
Markdown
1035 lines
50 KiB
Markdown
# WMS-Native Tidy Recovery 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:** Replace WMS restart recovery's WCS `tidyTaskState` dependency with transactional decisions based only on WMS workflow, job, inventory, cargo-space, and lock evidence.
|
|
|
|
**Architecture:** `WmsAlgorithmMapper` returns one immutable `TidyRecoveryEvidence` projection for each non-terminal tidy workflow. `TidyWorkflowRecoveryService` applies the approved decision table inside the WMS transaction manager and delegates a locally proven archived completion to `TidyWorkflowTransaction` with callback stage `RECOVERY_FINISHED`; no recovery path performs HTTP or repeats inventory movement.
|
|
|
|
**Tech Stack:** Java 17 records, Spring Boot transactions, MyBatis XML, SQL Server, JUnit 5, AssertJ, Mockito, PowerShell 7/Pester, Node.js built-in HTTP simulator.
|
|
|
|
## Global Constraints
|
|
|
|
- A tidy move remains a normal WMS job with `in_or_out='28'`; normal completion continues through `alg.usp_complete_move_job` and `jobFinish`.
|
|
- WMS-local state is the only completion and restart-recovery authority; do not add or retain a WCS task-state API.
|
|
- Keep `tidy-wcs-snapshot-url` and `WcsVehicleSnapshotClient` unchanged because manual capacity still needs vehicle availability.
|
|
- Never call `alg.usp_complete_move_job` during recovery of an already archived job.
|
|
- A workflow in `PAUSED_RECOVERY` is never changed automatically and its active locks are never released automatically.
|
|
- Database/query exceptions propagate out of the transactional recovery method; they are not translated into recovery reasons.
|
|
- Preserve normal `MOVE_FINISHED` and `OUTBOUND_FINISHED` callback behavior and duplicate-callback idempotency.
|
|
- Do not change outbound-relocation recovery or unrelated WMS/WCS endpoints.
|
|
- The WMS repository has extensive user changes. Stage and commit only the exact files listed by each task; never reset, unstage, or overwrite unrelated work.
|
|
- `D:/workSpace/PROJECT_SPACE/FT546` has no usable root Git metadata. Replay and handoff files outside `FT546MWS/FT546MWS` are verified but are not committed unless that repository is restored by the user.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Create `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java`: immutable database-shaped recovery projection only.
|
|
- Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java`: expose the local-evidence query.
|
|
- Modify `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml`: select the first incomplete step and its active/finished job, stock, cargo-space, and lock evidence.
|
|
- Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java`: implement the approved decision table without HTTP.
|
|
- Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java`: remove the special `UNKNOWN` callback branch while retaining the shared idempotent advancement path.
|
|
- Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java`: obsolete WCS state DTO.
|
|
- Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java`: obsolete HTTP adapter.
|
|
- Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java`: obsolete WCS-oriented recovery projection.
|
|
- Modify `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java`: remove only task-state URL/timeout properties.
|
|
- Modify `ruoyi-admin/src/main/resources/application.yml`: remove only task-state configuration.
|
|
- Modify focused JUnit tests under `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms`: prove SQL shape, every recovery decision, callback idempotency, and dependency removal.
|
|
- Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/Invoke-Ft546TidyBatchReplay.ps1`: replace the WCS UNKNOWN scenario with repairable-finish and missing-local-job cases.
|
|
- Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/simulators/ft546-wcs-simulator.mjs`: remove task-state simulator state and endpoints.
|
|
- Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/tests/Invoke-Ft546TidyBatchReplay.Tests.ps1`: enforce the new replay contract.
|
|
- Modify historical design/plan notes, the root handoff, and `tools/history-replay/README.md`: explain supersession and point to new evidence.
|
|
|
|
### Task 1: WMS Local Recovery Evidence Projection
|
|
|
|
**Files:**
|
|
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java`
|
|
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java`
|
|
- Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml`
|
|
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java`
|
|
|
|
**Interfaces:**
|
|
- Produces: `List<TidyRecoveryEvidence> selectTidyRecoveryEvidence(String warehouseId)`.
|
|
- Produces: nullable `stepId`/`stepNo` when every step is complete; all count fields are non-null `int` values.
|
|
- Produces: job fields from the deterministic ID `TDY-<workflowId>-<stepId>` even when the row disagrees with the step tuple.
|
|
|
|
- [ ] **Step 1: Write failing SQL contract tests**
|
|
|
|
Add these tests to `WmsAlgorithmMapperWorkflowSqlTest`:
|
|
|
|
```java
|
|
@Test
|
|
void tidyRecoveryProjectionMapsWorkflowJobInventoryAndLockEvidence() throws IOException {
|
|
String mapping = statement("resultmap", "tidyRecoveryEvidenceMap");
|
|
|
|
assertThat(mapping)
|
|
.contains("type=\"com.ruoyi.algorithm.wms.tidy.tidyrecoveryevidence\"")
|
|
.contains("column=\"workflowid\"")
|
|
.contains("column=\"stepid\"")
|
|
.contains("column=\"activejobid\"")
|
|
.contains("column=\"finishedjobid\"")
|
|
.contains("column=\"sourcestockrows\"")
|
|
.contains("column=\"targetstockrows\"")
|
|
.contains("column=\"activelockcount\"");
|
|
}
|
|
|
|
@Test
|
|
void tidyRecoveryQueryUsesOnlyWmsLocalAuthoritativeTables() throws IOException {
|
|
String query = statement("select", "selectTidyRecoveryEvidence");
|
|
|
|
assertThat(query)
|
|
.contains("from alg.tidy_workflow workflow")
|
|
.contains("outer apply")
|
|
.contains("from alg.tidy_step step")
|
|
.contains("left join dbo.mydo_job active_job")
|
|
.contains("left join dbo.mydo_job_finish finished_job")
|
|
.contains("from dbo.bas_stock")
|
|
.contains("left join dbo.bas_cargo_space source_space")
|
|
.contains("left join dbo.bas_cargo_space target_space")
|
|
.contains("from alg.tidy_space_lock")
|
|
.contains("concat('tdy-', workflow.workflow_id, '-', active_step.step_id)")
|
|
.doesNotContain("wcs_task_id");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the focused test and confirm the red state**
|
|
|
|
Run from `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`:
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am "-Dtest=WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
```
|
|
|
|
Expected: FAIL because `tidyRecoveryEvidenceMap` and `selectTidyRecoveryEvidence` do not exist.
|
|
|
|
- [ ] **Step 3: Add the immutable projection and mapper signature**
|
|
|
|
Create `TidyRecoveryEvidence.java` exactly as a data-only record:
|
|
|
|
```java
|
|
package com.ruoyi.algorithm.wms.tidy;
|
|
|
|
public record TidyRecoveryEvidence(
|
|
String workflowId,
|
|
TidyOrigin origin,
|
|
String workflowStatus,
|
|
int activeLockCount,
|
|
String stepId,
|
|
Integer stepNo,
|
|
String stepStatus,
|
|
String sourceSpaceId,
|
|
String targetSpaceId,
|
|
String trayCode,
|
|
int activeTidyJobCount,
|
|
String activeJobId,
|
|
String activeJobStatus,
|
|
String activeJobInOrOut,
|
|
String activeJobTrayCode,
|
|
String activeJobSourceSpaceId,
|
|
String activeJobTargetSpaceId,
|
|
String finishedJobId,
|
|
String finishedJobStatus,
|
|
String finishedJobInOrOut,
|
|
String finishedJobTrayCode,
|
|
String finishedJobSourceSpaceId,
|
|
String finishedJobTargetSpaceId,
|
|
int sourceStockRows,
|
|
int targetStockRows,
|
|
String sourceSpaceStatus,
|
|
String sourceSpaceTrayCode,
|
|
String targetSpaceStatus,
|
|
String targetSpaceTrayCode) { }
|
|
```
|
|
|
|
Import it in `WmsAlgorithmMapper` and add:
|
|
|
|
```java
|
|
List<TidyRecoveryEvidence> selectTidyRecoveryEvidence(@Param("warehouseId") String warehouseId);
|
|
```
|
|
|
|
- [ ] **Step 4: Implement the database-shaped result map and local-evidence SQL**
|
|
|
|
Replace the old recovery result map/query only after adding this new query, so the project remains compilable between tasks. The SQL must use `OUTER APPLY` so a workflow whose steps are all complete is still returned:
|
|
|
|
```xml
|
|
<resultMap id="tidyRecoveryEvidenceMap" type="com.ruoyi.algorithm.wms.tidy.TidyRecoveryEvidence">
|
|
<constructor>
|
|
<arg column="workflowId" javaType="java.lang.String" />
|
|
<arg column="origin" javaType="com.ruoyi.algorithm.wms.tidy.TidyOrigin" />
|
|
<arg column="workflowStatus" javaType="java.lang.String" />
|
|
<arg column="activeLockCount" javaType="_int" />
|
|
<arg column="stepId" javaType="java.lang.String" />
|
|
<arg column="stepNo" javaType="java.lang.Integer" />
|
|
<arg column="stepStatus" javaType="java.lang.String" />
|
|
<arg column="sourceSpaceId" javaType="java.lang.String" />
|
|
<arg column="targetSpaceId" javaType="java.lang.String" />
|
|
<arg column="trayCode" javaType="java.lang.String" />
|
|
<arg column="activeTidyJobCount" javaType="_int" />
|
|
<arg column="activeJobId" javaType="java.lang.String" />
|
|
<arg column="activeJobStatus" javaType="java.lang.String" />
|
|
<arg column="activeJobInOrOut" javaType="java.lang.String" />
|
|
<arg column="activeJobTrayCode" javaType="java.lang.String" />
|
|
<arg column="activeJobSourceSpaceId" javaType="java.lang.String" />
|
|
<arg column="activeJobTargetSpaceId" javaType="java.lang.String" />
|
|
<arg column="finishedJobId" javaType="java.lang.String" />
|
|
<arg column="finishedJobStatus" javaType="java.lang.String" />
|
|
<arg column="finishedJobInOrOut" javaType="java.lang.String" />
|
|
<arg column="finishedJobTrayCode" javaType="java.lang.String" />
|
|
<arg column="finishedJobSourceSpaceId" javaType="java.lang.String" />
|
|
<arg column="finishedJobTargetSpaceId" javaType="java.lang.String" />
|
|
<arg column="sourceStockRows" javaType="_int" />
|
|
<arg column="targetStockRows" javaType="_int" />
|
|
<arg column="sourceSpaceStatus" javaType="java.lang.String" />
|
|
<arg column="sourceSpaceTrayCode" javaType="java.lang.String" />
|
|
<arg column="targetSpaceStatus" javaType="java.lang.String" />
|
|
<arg column="targetSpaceTrayCode" javaType="java.lang.String" />
|
|
</constructor>
|
|
</resultMap>
|
|
|
|
<select id="selectTidyRecoveryEvidence" resultMap="tidyRecoveryEvidenceMap">
|
|
select workflow.workflow_id as workflowId,
|
|
workflow.origin as origin,
|
|
workflow.status as workflowStatus,
|
|
(select count(*) from alg.tidy_space_lock held
|
|
where held.workflow_id = workflow.workflow_id and held.status = 'ACTIVE') as activeLockCount,
|
|
active_step.step_id as stepId,
|
|
active_step.step_no as stepNo,
|
|
active_step.status as stepStatus,
|
|
active_step.source_space_id as sourceSpaceId,
|
|
active_step.target_space_id as targetSpaceId,
|
|
active_step.tray_code as trayCode,
|
|
(select count(*) from dbo.mydo_job workflow_job
|
|
where workflow_job.job_id like concat('TDY-', workflow.workflow_id, '-%')) as activeTidyJobCount,
|
|
active_job.job_id as activeJobId,
|
|
active_job.status as activeJobStatus,
|
|
active_job.in_or_out as activeJobInOrOut,
|
|
active_job.tray_code as activeJobTrayCode,
|
|
active_job.cargo_space_id as activeJobSourceSpaceId,
|
|
active_job.target_cargo_space_id as activeJobTargetSpaceId,
|
|
finished_job.job_id as finishedJobId,
|
|
finished_job.status as finishedJobStatus,
|
|
finished_job.in_or_out as finishedJobInOrOut,
|
|
finished_job.tray_code as finishedJobTrayCode,
|
|
finished_job.cargo_space_id as finishedJobSourceSpaceId,
|
|
finished_job.target_cargo_space_id as finishedJobTargetSpaceId,
|
|
(select count(*) from dbo.bas_stock source_stock
|
|
where source_stock.cargo_space_id = active_step.source_space_id
|
|
and source_stock.tray_code = active_step.tray_code) as sourceStockRows,
|
|
(select count(*) from dbo.bas_stock target_stock
|
|
where target_stock.cargo_space_id = active_step.target_space_id
|
|
and target_stock.tray_code = active_step.tray_code) as targetStockRows,
|
|
source_space.status as sourceSpaceStatus,
|
|
source_space.tray_code as sourceSpaceTrayCode,
|
|
target_space.status as targetSpaceStatus,
|
|
target_space.tray_code as targetSpaceTrayCode
|
|
from alg.tidy_workflow workflow
|
|
outer apply (
|
|
select top (1) step.step_id, step.step_no, step.status,
|
|
step.source_space_id, step.target_space_id, step.tray_code
|
|
from alg.tidy_step step
|
|
where step.workflow_id = workflow.workflow_id
|
|
and step.status in ('PENDING', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY')
|
|
order by step.step_no
|
|
) active_step
|
|
left join dbo.mydo_job active_job
|
|
on active_job.job_id = concat('TDY-', workflow.workflow_id, '-', active_step.step_id)
|
|
left join dbo.mydo_job_finish finished_job
|
|
on finished_job.job_id = concat('TDY-', workflow.workflow_id, '-', active_step.step_id)
|
|
left join dbo.bas_cargo_space source_space
|
|
on source_space.cargo_space_id = active_step.source_space_id
|
|
left join dbo.bas_cargo_space target_space
|
|
on target_space.cargo_space_id = active_step.target_space_id
|
|
where workflow.warehouse_id = #{warehouseId}
|
|
and workflow.status in (
|
|
'CLAIMED', 'WAITING_DISPATCH', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY'
|
|
)
|
|
</select>
|
|
```
|
|
|
|
- [ ] **Step 5: Run the mapper test and commit the projection**
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am "-Dtest=WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java
|
|
git commit --only -m "feat: project local tidy recovery evidence" ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java
|
|
```
|
|
|
|
Expected: the focused test passes and the commit contains only the four listed files.
|
|
|
|
### Task 2: Transactional Recovery Decision Service
|
|
|
|
**Files:**
|
|
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java`
|
|
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `List<TidyRecoveryEvidence> selectTidyRecoveryEvidence(String warehouseId)` from Task 1.
|
|
- Consumes: `boolean TidyWorkflowTransaction.onJobFinished(String jobId, String callbackStage)` with `callbackStage="RECOVERY_FINISHED"`.
|
|
- Produces: `int reconcile(String warehouseId)`, counting workflows changed by completion repair, terminal cleanup, or pause; no-op and duplicate repairs return zero.
|
|
|
|
- [ ] **Step 1: Replace WCS-oriented unit tests with the local decision table**
|
|
|
|
Use a test builder that creates a matching active job in the valid pre-move state by default. The test class must include these assertions:
|
|
|
|
```java
|
|
private WmsAlgorithmMapper mapper;
|
|
private TidyWorkflowTransaction transaction;
|
|
private TidyWorkflowRecoveryService service;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
mapper = mock(WmsAlgorithmMapper.class);
|
|
transaction = mock(TidyWorkflowTransaction.class);
|
|
service = new TidyWorkflowRecoveryService(mapper, transaction);
|
|
}
|
|
|
|
@Test
|
|
void matchingActiveJobWithPreMoveEvidenceRemainsRunning() {
|
|
TidyRecoveryEvidence evidence = matching().build();
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence));
|
|
|
|
assertThat(service.reconcile("01")).isZero();
|
|
|
|
verifyNoInteractions(transaction);
|
|
verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString());
|
|
verify(mapper, never()).releaseTidyResources(anyString());
|
|
}
|
|
|
|
@Test
|
|
void matchingArchivedFinishAdvancesOnceWithoutMovingInventoryAgain() {
|
|
TidyRecoveryEvidence evidence = matching().finished().build();
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence));
|
|
when(transaction.onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED"))
|
|
.thenReturn(true, false);
|
|
|
|
assertThat(service.reconcile("01")).isEqualTo(1);
|
|
assertThat(service.reconcile("01")).isZero();
|
|
|
|
verify(transaction, times(2)).onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED");
|
|
verify(mapper, never()).releaseTidyResources(evidence.workflowId());
|
|
}
|
|
|
|
@Test
|
|
void completedStepsWithoutAnActiveTidyJobFinishWorkflowAndReleaseLocks() {
|
|
TidyRecoveryEvidence evidence = matching().allStepsCompleted().build();
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence));
|
|
|
|
assertThat(service.reconcile("01")).isEqualTo(1);
|
|
|
|
verify(mapper).completeTidyWorkflow(evidence.workflowId());
|
|
verify(mapper).releaseTidyResources(evidence.workflowId());
|
|
}
|
|
|
|
@ParameterizedTest(name = "{0}")
|
|
@MethodSource("pauseCases")
|
|
void inconsistentLocalEvidencePausesWithoutReleasingLocks(
|
|
String label, TidyRecoveryEvidence evidence, String expectedReason) {
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence));
|
|
|
|
assertThat(service.reconcile("01")).isEqualTo(1);
|
|
|
|
verify(mapper).pauseTidyWorkflow(evidence.workflowId(), expectedReason);
|
|
verify(mapper).pauseTidyStep(evidence.workflowId(), evidence.stepNo(), expectedReason);
|
|
verify(mapper, never()).releaseTidyResources(anyString());
|
|
verifyNoInteractions(transaction);
|
|
}
|
|
|
|
private static Stream<Arguments> pauseCases() {
|
|
return Stream.of(
|
|
Arguments.of("invalid active inventory", matching().sourceStockRows(0).build(),
|
|
"RECOVERY_ACTIVE_EVIDENCE_INVALID"),
|
|
Arguments.of("missing active and finish jobs", matching().missingJobs().build(),
|
|
"RECOVERY_JOB_MISSING"),
|
|
Arguments.of("active job type mismatch", matching().activeJobInOrOut("27").build(),
|
|
"RECOVERY_JOB_MISMATCH"),
|
|
Arguments.of("active job tray mismatch", matching().activeJobTrayCode("OTHER").build(),
|
|
"RECOVERY_JOB_MISMATCH"),
|
|
Arguments.of("active job source mismatch", matching().activeJobSourceSpaceId("OTHER").build(),
|
|
"RECOVERY_JOB_MISMATCH"),
|
|
Arguments.of("finish status is not terminal", matching().finished().finishedJobStatus("3").build(),
|
|
"RECOVERY_FINISH_EVIDENCE_INVALID"),
|
|
Arguments.of("finish job target mismatch", matching().finished().finishedJobTargetSpaceId("OTHER").build(),
|
|
"RECOVERY_JOB_MISMATCH"),
|
|
Arguments.of("finish inventory is not terminal", matching().finished().sourceStockRows(1).build(),
|
|
"RECOVERY_FINISH_EVIDENCE_INVALID"),
|
|
Arguments.of("active and finish rows coexist", matching().withBothJobs().build(),
|
|
"RECOVERY_FINISH_EVIDENCE_INVALID"));
|
|
}
|
|
|
|
@Test
|
|
void alreadyPausedWorkflowIsLeftUntouched() {
|
|
TidyRecoveryEvidence evidence = matching().workflowStatus("PAUSED_RECOVERY").build();
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence));
|
|
|
|
assertThat(service.reconcile("01")).isZero();
|
|
|
|
verifyNoInteractions(transaction);
|
|
verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString());
|
|
}
|
|
|
|
@Test
|
|
void queryFailurePropagatesWithoutWritingRecoveryState() {
|
|
when(mapper.selectTidyRecoveryEvidence("01")).thenThrow(new IllegalStateException("db unavailable"));
|
|
|
|
assertThatThrownBy(() -> service.reconcile("01"))
|
|
.isInstanceOf(IllegalStateException.class)
|
|
.hasMessage("db unavailable");
|
|
|
|
verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString());
|
|
verifyNoInteractions(transaction);
|
|
}
|
|
```
|
|
|
|
Define the referenced builder in the same test class. Its defaults represent a valid pre-move state, while its named mutations create each exact decision-table row:
|
|
|
|
```java
|
|
private static EvidenceBuilder matching() {
|
|
return new EvidenceBuilder();
|
|
}
|
|
|
|
private static final class EvidenceBuilder {
|
|
private String workflowId = "WF-1";
|
|
private TidyOrigin origin = TidyOrigin.MANUAL_TIDY;
|
|
private String workflowStatus = "RUNNING";
|
|
private int activeLockCount = 2;
|
|
private String stepId = "WF-1-1";
|
|
private Integer stepNo = 1;
|
|
private String stepStatus = "PENDING";
|
|
private String sourceSpaceId = "SOURCE";
|
|
private String targetSpaceId = "TARGET";
|
|
private String trayCode = "TRAY-1";
|
|
private int activeTidyJobCount = 1;
|
|
private String activeJobId = "TDY-WF-1-WF-1-1";
|
|
private String activeJobStatus = "1";
|
|
private String activeJobInOrOut = "28";
|
|
private String activeJobTrayCode = "TRAY-1";
|
|
private String activeJobSourceSpaceId = "SOURCE";
|
|
private String activeJobTargetSpaceId = "TARGET";
|
|
private String finishedJobId;
|
|
private String finishedJobStatus;
|
|
private String finishedJobInOrOut;
|
|
private String finishedJobTrayCode;
|
|
private String finishedJobSourceSpaceId;
|
|
private String finishedJobTargetSpaceId;
|
|
private int sourceStockRows = 1;
|
|
private int targetStockRows;
|
|
private String sourceSpaceStatus = "2";
|
|
private String sourceSpaceTrayCode = "TRAY-1";
|
|
private String targetSpaceStatus = "1";
|
|
private String targetSpaceTrayCode = "";
|
|
|
|
EvidenceBuilder workflowStatus(String value) {
|
|
workflowStatus = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder activeJobInOrOut(String value) {
|
|
activeJobInOrOut = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder activeJobTrayCode(String value) {
|
|
activeJobTrayCode = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder activeJobSourceSpaceId(String value) {
|
|
activeJobSourceSpaceId = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder sourceStockRows(int value) {
|
|
sourceStockRows = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder finishedJobStatus(String value) {
|
|
finishedJobStatus = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder finishedJobTargetSpaceId(String value) {
|
|
finishedJobTargetSpaceId = value;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder finished() {
|
|
activeTidyJobCount = 0;
|
|
activeJobId = null;
|
|
activeJobStatus = null;
|
|
activeJobInOrOut = null;
|
|
activeJobTrayCode = null;
|
|
activeJobSourceSpaceId = null;
|
|
activeJobTargetSpaceId = null;
|
|
finishedJobId = "TDY-WF-1-WF-1-1";
|
|
finishedJobStatus = "4";
|
|
finishedJobInOrOut = "28";
|
|
finishedJobTrayCode = trayCode;
|
|
finishedJobSourceSpaceId = sourceSpaceId;
|
|
finishedJobTargetSpaceId = targetSpaceId;
|
|
sourceStockRows = 0;
|
|
targetStockRows = 1;
|
|
sourceSpaceStatus = "1";
|
|
sourceSpaceTrayCode = "";
|
|
targetSpaceStatus = "2";
|
|
targetSpaceTrayCode = trayCode;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder missingJobs() {
|
|
activeTidyJobCount = 0;
|
|
activeJobId = null;
|
|
activeJobStatus = null;
|
|
activeJobInOrOut = null;
|
|
activeJobTrayCode = null;
|
|
activeJobSourceSpaceId = null;
|
|
activeJobTargetSpaceId = null;
|
|
finishedJobId = null;
|
|
finishedJobStatus = null;
|
|
finishedJobInOrOut = null;
|
|
finishedJobTrayCode = null;
|
|
finishedJobSourceSpaceId = null;
|
|
finishedJobTargetSpaceId = null;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder withBothJobs() {
|
|
finishedJobId = activeJobId;
|
|
finishedJobStatus = "4";
|
|
finishedJobInOrOut = "28";
|
|
finishedJobTrayCode = trayCode;
|
|
finishedJobSourceSpaceId = sourceSpaceId;
|
|
finishedJobTargetSpaceId = targetSpaceId;
|
|
return this;
|
|
}
|
|
|
|
EvidenceBuilder allStepsCompleted() {
|
|
missingJobs();
|
|
stepId = null;
|
|
stepNo = null;
|
|
stepStatus = null;
|
|
sourceSpaceId = null;
|
|
targetSpaceId = null;
|
|
trayCode = null;
|
|
sourceStockRows = 0;
|
|
targetStockRows = 0;
|
|
sourceSpaceStatus = null;
|
|
sourceSpaceTrayCode = null;
|
|
targetSpaceStatus = null;
|
|
targetSpaceTrayCode = null;
|
|
return this;
|
|
}
|
|
|
|
TidyRecoveryEvidence build() {
|
|
return new TidyRecoveryEvidence(
|
|
workflowId, origin, workflowStatus, activeLockCount,
|
|
stepId, stepNo, stepStatus, sourceSpaceId, targetSpaceId, trayCode,
|
|
activeTidyJobCount,
|
|
activeJobId, activeJobStatus, activeJobInOrOut, activeJobTrayCode,
|
|
activeJobSourceSpaceId, activeJobTargetSpaceId,
|
|
finishedJobId, finishedJobStatus, finishedJobInOrOut, finishedJobTrayCode,
|
|
finishedJobSourceSpaceId, finishedJobTargetSpaceId,
|
|
sourceStockRows, targetStockRows,
|
|
sourceSpaceStatus, sourceSpaceTrayCode, targetSpaceStatus, targetSpaceTrayCode);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the recovery test and confirm the red state**
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
```
|
|
|
|
Expected: compilation or test failure because the service still requires `WcsTaskStateClient` and calls `selectActiveTidyWorkflows`.
|
|
|
|
- [ ] **Step 3: Implement the minimal transactional decision service**
|
|
|
|
Replace `TidyWorkflowRecoveryService` with this decision structure; keep all comparison helpers in this service, not in the projection:
|
|
|
|
```java
|
|
@Service
|
|
public class TidyWorkflowRecoveryService {
|
|
private static final Set<String> ACTIVE_JOB_STATUSES = Set.of("1", "2", "3");
|
|
|
|
private final WmsAlgorithmMapper mapper;
|
|
private final TidyWorkflowTransaction transaction;
|
|
|
|
public TidyWorkflowRecoveryService(WmsAlgorithmMapper mapper,
|
|
TidyWorkflowTransaction transaction) {
|
|
this.mapper = mapper;
|
|
this.transaction = transaction;
|
|
}
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
public int reconcile(String warehouseId) {
|
|
int changed = 0;
|
|
for (TidyRecoveryEvidence evidence : mapper.selectTidyRecoveryEvidence(warehouseId)) {
|
|
if ("PAUSED_RECOVERY".equals(evidence.workflowStatus())) {
|
|
continue;
|
|
}
|
|
if (evidence.stepId() == null) {
|
|
if (evidence.activeTidyJobCount() == 0) {
|
|
mapper.completeTidyWorkflow(evidence.workflowId());
|
|
mapper.releaseTidyResources(evidence.workflowId());
|
|
changed++;
|
|
} else {
|
|
changed += pause(evidence, "RECOVERY_JOB_MISMATCH");
|
|
}
|
|
continue;
|
|
}
|
|
if (evidence.activeJobId() != null && evidence.finishedJobId() != null) {
|
|
changed += pause(evidence, "RECOVERY_FINISH_EVIDENCE_INVALID");
|
|
continue;
|
|
}
|
|
if (evidence.activeJobId() != null) {
|
|
if (!matchesStep(evidence, false)) {
|
|
changed += pause(evidence, "RECOVERY_JOB_MISMATCH");
|
|
} else if (ACTIVE_JOB_STATUSES.contains(evidence.activeJobStatus())
|
|
&& isPreMoveState(evidence)) {
|
|
continue;
|
|
} else {
|
|
changed += pause(evidence, "RECOVERY_ACTIVE_EVIDENCE_INVALID");
|
|
}
|
|
continue;
|
|
}
|
|
if (evidence.finishedJobId() != null) {
|
|
if (!matchesStep(evidence, true)) {
|
|
changed += pause(evidence, "RECOVERY_JOB_MISMATCH");
|
|
} else if ("4".equals(evidence.finishedJobStatus())
|
|
&& isTerminalMoveState(evidence)) {
|
|
if (transaction.onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED")) {
|
|
changed++;
|
|
}
|
|
} else {
|
|
changed += pause(evidence, "RECOVERY_FINISH_EVIDENCE_INVALID");
|
|
}
|
|
continue;
|
|
}
|
|
changed += pause(evidence, "RECOVERY_JOB_MISSING");
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
private boolean matchesStep(TidyRecoveryEvidence evidence, boolean finished) {
|
|
return "28".equals(finished ? evidence.finishedJobInOrOut() : evidence.activeJobInOrOut())
|
|
&& Objects.equals(evidence.trayCode(), finished
|
|
? evidence.finishedJobTrayCode() : evidence.activeJobTrayCode())
|
|
&& Objects.equals(evidence.sourceSpaceId(), finished
|
|
? evidence.finishedJobSourceSpaceId() : evidence.activeJobSourceSpaceId())
|
|
&& Objects.equals(evidence.targetSpaceId(), finished
|
|
? evidence.finishedJobTargetSpaceId() : evidence.activeJobTargetSpaceId());
|
|
}
|
|
|
|
private boolean isPreMoveState(TidyRecoveryEvidence evidence) {
|
|
return evidence.sourceStockRows() > 0
|
|
&& evidence.targetStockRows() == 0
|
|
&& "2".equals(evidence.sourceSpaceStatus())
|
|
&& Objects.equals(evidence.trayCode(), trimToNull(evidence.sourceSpaceTrayCode()))
|
|
&& "1".equals(evidence.targetSpaceStatus())
|
|
&& trimToNull(evidence.targetSpaceTrayCode()) == null;
|
|
}
|
|
|
|
private boolean isTerminalMoveState(TidyRecoveryEvidence evidence) {
|
|
return evidence.sourceStockRows() == 0
|
|
&& evidence.targetStockRows() > 0
|
|
&& "1".equals(evidence.sourceSpaceStatus())
|
|
&& trimToNull(evidence.sourceSpaceTrayCode()) == null
|
|
&& "2".equals(evidence.targetSpaceStatus())
|
|
&& Objects.equals(evidence.trayCode(), trimToNull(evidence.targetSpaceTrayCode()));
|
|
}
|
|
|
|
private int pause(TidyRecoveryEvidence evidence, String reason) {
|
|
mapper.pauseTidyWorkflow(evidence.workflowId(), reason);
|
|
if (evidence.stepNo() != null) {
|
|
mapper.pauseTidyStep(evidence.workflowId(), evidence.stepNo(), reason);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
private String trimToNull(String value) {
|
|
if (value == null || value.trim().isEmpty()) return null;
|
|
return value.trim();
|
|
}
|
|
}
|
|
```
|
|
|
|
Add imports for `java.util.Objects` and `java.util.Set`. Do not catch mapper exceptions and do not call `releaseTidyResources` from `pause`.
|
|
|
|
- [ ] **Step 4: Run service, transaction, and mapper tests**
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
```
|
|
|
|
Expected: PASS; Mockito proves no lock release on every paused path and no inventory procedure is reachable from the service.
|
|
|
|
- [ ] **Step 5: Commit the recovery decision**
|
|
|
|
```powershell
|
|
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java
|
|
git commit --only -m "feat: recover tidy workflows from WMS evidence" ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java
|
|
```
|
|
|
|
### Task 3: Remove WCS Task-State and UNKNOWN Callback Dependencies
|
|
|
|
**Files:**
|
|
- Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java`
|
|
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java`
|
|
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java`
|
|
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java`
|
|
- Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml`
|
|
- Modify: `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java`
|
|
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
|
- Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java`
|
|
- Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java`
|
|
- Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java`
|
|
|
|
**Interfaces:**
|
|
- Retains: `tidyWcsSnapshotUrl` and its getter/setter.
|
|
- Removes: task-state URL, timeout, DTO, HTTP client, old mapper projection/query, and the `UNKNOWN` callback branch.
|
|
- Retains: `RECOVERY_FINISHED` on the same callback-event idempotency key used by normal completion.
|
|
|
|
- [ ] **Step 1: Add a failing absence test and a positive recovery-stage test**
|
|
|
|
Create `WcsTaskStateDependencyRemovalTest`:
|
|
|
|
```java
|
|
class WcsTaskStateDependencyRemovalTest {
|
|
@Test
|
|
void taskStateClassesPropertiesAndConfigurationAreAbsent() throws Exception {
|
|
assertThatThrownBy(() -> Class.forName(
|
|
"com.ruoyi.algorithm.wms.tidy.WcsTaskStateClient"))
|
|
.isInstanceOf(ClassNotFoundException.class);
|
|
assertThatThrownBy(() -> Class.forName(
|
|
"com.ruoyi.algorithm.wms.tidy.WcsTaskState"))
|
|
.isInstanceOf(ClassNotFoundException.class);
|
|
|
|
assertThat(Arrays.stream(AlgorithmProperties.class.getMethods())
|
|
.map(Method::getName))
|
|
.noneMatch(name -> name.contains("TidyWcsTaskState"));
|
|
|
|
try (InputStream stream = getClass().getResourceAsStream("/application.yml")) {
|
|
assertThat(stream).isNotNull();
|
|
String yaml = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
|
assertThat(yaml).doesNotContain("tidy-wcs-task-state");
|
|
assertThat(yaml).contains("tidy-wcs-snapshot-url");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Replace the old `unknownCallbackPausesWorkflowAndKeepsResources` transaction test with:
|
|
|
|
```java
|
|
@Test
|
|
void recoveryFinishedUsesTheNormalIdempotentAdvancementPath() {
|
|
WmsAlgorithmMapper mapper = mock(WmsAlgorithmMapper.class);
|
|
ExecutionAdmissionService admission = mock(ExecutionAdmissionService.class);
|
|
TidyWorkflowTransaction tx = new TidyWorkflowTransaction(mapper, admission);
|
|
when(mapper.selectTidyWorkflowIdByJob("JOB-1")).thenReturn("WF-1");
|
|
when(mapper.selectTidyStepIdByJob("JOB-1")).thenReturn("STEP-1");
|
|
when(mapper.insertTidyCallbackEventIfAbsent(any(), eq("WF-1"), eq("STEP-1"),
|
|
eq("JOB-1"), eq("RECOVERY_FINISHED"), eq("SUCCESS"))).thenReturn(1);
|
|
when(mapper.selectNextTidyStepId("WF-1")).thenReturn(null);
|
|
|
|
assertThat(tx.onJobFinished("JOB-1", "RECOVERY_FINISHED")).isTrue();
|
|
|
|
verify(mapper).completeTidyStep("STEP-1");
|
|
verify(mapper).completeTidyWorkflow("WF-1");
|
|
verify(mapper).releaseTidyResources("WF-1");
|
|
verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString());
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run clean focused tests and confirm the absence test is red**
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am clean "-Dtest=WcsTaskStateDependencyRemovalTest,TidyWorkflowTransactionTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
```
|
|
|
|
Expected: FAIL because task-state classes, properties, and YAML keys still exist.
|
|
|
|
- [ ] **Step 3: Delete the dependency and obsolete recovery projection**
|
|
|
|
Delete the three listed Java files. Remove these members from `AlgorithmProperties` and nothing related to the snapshot URL:
|
|
|
|
```java
|
|
private String tidyWcsTaskStateUrl;
|
|
private int tidyWcsTaskStateTimeoutMs;
|
|
public String getTidyWcsTaskStateUrl();
|
|
public void setTidyWcsTaskStateUrl(String value);
|
|
public int getTidyWcsTaskStateTimeoutMs();
|
|
public void setTidyWcsTaskStateTimeoutMs(int value);
|
|
```
|
|
|
|
Remove these two YAML entries:
|
|
|
|
```yaml
|
|
tidy-wcs-task-state-url: ${FORTUCKY_TIDY_WCS_TASK_STATE_URL:http://localhost:8081/wcs2Wms/api/tidyTaskState}
|
|
tidy-wcs-task-state-timeout-ms: ${FORTUCKY_TIDY_WCS_TASK_STATE_TIMEOUT_MS:3000}
|
|
```
|
|
|
|
Remove `selectActiveTidyWorkflows`, its `TidyWorkflowState` import/signature, `tidyWorkflowStateMap`, and the old XML query. In `TidyWorkflowTransaction`, remove only:
|
|
|
|
```java
|
|
if ("UNKNOWN".equals(callbackStage)) {
|
|
mapper.pauseTidyWorkflow(workflowId, "PAUSED_RECOVERY");
|
|
return true;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Verify dependency removal and callback regressions**
|
|
|
|
```powershell
|
|
mvn -pl ruoyi-admin -am clean "-Dtest=WcsTaskStateDependencyRemovalTest,TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,ToWmsServiceImplWorkflowCallbackTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
rg -n "WcsTaskState|tidyWcsTaskState|tidy-wcs-task-state|FORTUCKY_TIDY_WCS_TASK_STATE|UNKNOWN.*callbackStage" fortucky-algorithm/src/main ruoyi-admin/src/main
|
|
```
|
|
|
|
Expected: Maven PASS. The search prints no matches. A separate search for `tidy-wcs-snapshot-url` still finds the retained snapshot configuration.
|
|
|
|
- [ ] **Step 5: Commit only WMS dependency-removal files**
|
|
|
|
```powershell
|
|
git add fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java
|
|
git add -u ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java
|
|
git commit --only -m "refactor: remove WCS tidy task-state recovery" fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java
|
|
```
|
|
|
|
### Task 4: Replace UNKNOWN Replay with WMS-Local Recovery Cases
|
|
|
|
**Files:**
|
|
- Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/Invoke-Ft546TidyBatchReplay.ps1`
|
|
- Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/simulators/ft546-wcs-simulator.mjs`
|
|
- Test: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/tests/Invoke-Ft546TidyBatchReplay.Tests.ps1`
|
|
|
|
**Interfaces:**
|
|
- Produces replay event `TIDY_RECOVERY_FINISHED` for archived completion repaired after restart.
|
|
- Produces replay event `TIDY_RECOVERY_MISSING_JOB` for a run-scoped deleted active job paused with retained locks.
|
|
- Produces two `tidy-reconciliation.csv` rows with phases `RECOVERY_FINISHED` and `RECOVERY_MISSING_JOB`.
|
|
- Removes simulator endpoints `/sim/task-state` and `/sim/unknown-task` and environment variable `FORTUCKY_TIDY_WCS_TASK_STATE_URL`.
|
|
|
|
- [ ] **Step 1: Change Pester expectations first**
|
|
|
|
In the automatic/manual control test, replace the old positive task-state assertions with:
|
|
|
|
```powershell
|
|
$source | Should Match 'TIDY_RECOVERY_FINISHED'
|
|
$source | Should Match 'TIDY_RECOVERY_MISSING_JOB'
|
|
$source | Should Match 'RECOVERY_FINISHED'
|
|
$source | Should Match 'RECOVERY_JOB_MISSING'
|
|
$source | Should Match 'usp_complete_move_job'
|
|
$source | Should Match 'DELETE FROM dbo\.mydo_job'
|
|
$source | Should Not Match 'FORTUCKY_TIDY_WCS_TASK_STATE_URL'
|
|
$source | Should Not Match "Invoke-Simulator '/sim/unknown-task'"
|
|
|
|
$simulator = Get-Content -LiteralPath $simulatorPath -Raw
|
|
$simulator | Should Not Match '/sim/task-state'
|
|
$simulator | Should Not Match '/sim/unknown-task'
|
|
```
|
|
|
|
Add a contract assertion that the runner inspects `/sim/events` and requires zero task-state paths after both recovery cases.
|
|
|
|
- [ ] **Step 2: Run Pester and confirm the red state**
|
|
|
|
Run from `D:/workSpace/PROJECT_SPACE/FT546`:
|
|
|
|
```powershell
|
|
Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed
|
|
```
|
|
|
|
Expected: FAIL on the old environment variable, simulator endpoints, and missing local-recovery events.
|
|
|
|
- [ ] **Step 3: Remove task-state behavior from the simulator and WMS process environment**
|
|
|
|
Delete `unknownNextTask`, both task-state routes, and the reset assignment from `ft546-wcs-simulator.mjs`. Keep vehicle snapshot, receive-job, completion, event, and reset routes unchanged. Remove only this environment entry from `Start-WmsProcess`:
|
|
|
|
```powershell
|
|
FORTUCKY_TIDY_WCS_TASK_STATE_URL = "http://127.0.0.1:$SimulatorPort/sim/task-state"
|
|
```
|
|
|
|
- [ ] **Step 4: Add run-scoped SQL helpers for the two crash boundaries**
|
|
|
|
Add these helpers beside `Complete-TidyWorkflow`:
|
|
|
|
```powershell
|
|
function Complete-TidyJobWithoutCallback([string]$JobId) {
|
|
$jobLiteral = ConvertTo-SqlLiteral $JobId
|
|
$result = @(Invoke-Query @"
|
|
DECLARE @result int;
|
|
EXEC alg.usp_complete_move_job @v_job_id=$jobLiteral, @v_result=@result OUTPUT;
|
|
SELECT @result AS result_code;
|
|
"@)
|
|
if ($result.Count -ne 1 -or [int]$result[0].result_code -ne 10) {
|
|
throw "Crash-boundary tidy completion failed for $JobId"
|
|
}
|
|
}
|
|
|
|
function Remove-ActiveTidyJob([string]$JobId) {
|
|
$deleted = [int](Invoke-NonQuery "DELETE FROM dbo.mydo_job WHERE job_id=$(ConvertTo-SqlLiteral $JobId) AND in_or_out='28'")
|
|
if ($deleted -ne 1) {
|
|
throw "Expected one active tidy job to be removed for $JobId; deleted=$deleted"
|
|
}
|
|
}
|
|
```
|
|
|
|
These mutations run only after the verified backup. They must be recorded with `Add-Event` and are cleaned by the existing `finally` restore.
|
|
|
|
- [ ] **Step 5: Implement repairable archived-completion recovery**
|
|
|
|
Replace the old UNKNOWN block with this ordered flow:
|
|
|
|
1. Start WMS with a fast automatic scan and wait for a fresh `AUTO_TIDY` workflow and its active `TDY-%` job.
|
|
2. Stop WMS.
|
|
3. Capture the tray/source/target and current matching `move_msg` count.
|
|
4. Call `Complete-TidyJobWithoutCallback`; assert the active row is gone, one status `4` finish row exists, the tidy step is still non-complete, stock is at target, source is empty, and target holds the tray.
|
|
5. Capture the post-procedure `move_msg` count, restart WMS with automatic scan quiet and reconciliation at 1000 ms, then wait for the step/workflow to advance.
|
|
6. Assert exactly one `RECOVERY_FINISHED` callback event, no active deterministic job, unchanged post-procedure `move_msg` count, and zero active locks when the workflow is terminal.
|
|
7. Write `TIDY_RECOVERY_FINISHED` and a passing `RECOVERY_FINISHED` reconciliation row.
|
|
|
|
Use these acceptance predicates in the runner:
|
|
|
|
```powershell
|
|
$repairPass = [string]$repairAfter.status -eq 'COMPLETED' -and
|
|
[int]$repairAfter.active_jobs -eq 0 -and
|
|
[int]$repairAfter.finish_jobs -eq 1 -and
|
|
[int]$repairAfter.recovery_events -eq 1 -and
|
|
[int]$repairAfter.active_locks -eq 0 -and
|
|
[int]$repairAfter.move_messages -eq $moveMessagesAfterProcedure
|
|
```
|
|
|
|
- [ ] **Step 6: Implement missing-local-job recovery**
|
|
|
|
1. Start WMS with a fast automatic scan and wait for another fresh workflow/job, excluding the normal and repaired workflow IDs.
|
|
2. Stop WMS and call `Remove-ActiveTidyJob`.
|
|
3. Restart WMS with automatic scan quiet and reconciliation at 1000 ms.
|
|
4. Wait for workflow status `PAUSED_RECOVERY`, workflow `status_reason='RECOVERY_JOB_MISSING'`, at least one paused step, and active lock count greater than zero.
|
|
5. Assert no replacement deterministic job and no `RECOVERY_FINISHED` event.
|
|
6. Write `TIDY_RECOVERY_MISSING_JOB` and a passing `RECOVERY_MISSING_JOB` reconciliation row.
|
|
|
|
Use these acceptance predicates:
|
|
|
|
```powershell
|
|
$missingPass = [string]$missingAfter.status -eq 'PAUSED_RECOVERY' -and
|
|
[string]$missingAfter.status_reason -eq 'RECOVERY_JOB_MISSING' -and
|
|
[int]$missingAfter.paused_steps -ge 1 -and
|
|
[int]$missingAfter.active_locks -gt 0 -and
|
|
[int]$missingAfter.active_jobs -eq 0 -and
|
|
[int]$missingAfter.recovery_events -eq 0
|
|
```
|
|
|
|
Finally query simulator events and fail the run unless both task-state paths are absent:
|
|
|
|
```powershell
|
|
$simulatorEvents = @((Invoke-Simulator '/sim/events').data)
|
|
$taskStateRequests = @($simulatorEvents | Where-Object {
|
|
$_.path -in @('/sim/task-state', '/sim/unknown-task')
|
|
}).Count
|
|
if ($taskStateRequests -ne 0) {
|
|
throw "Unexpected WCS task-state requests observed: $taskStateRequests"
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Run Pester to green**
|
|
|
|
```powershell
|
|
Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed
|
|
```
|
|
|
|
Expected: all contract tests pass; the exact total may increase from the historical 25 as the two new contracts are added.
|
|
|
|
No Git commit is attempted for this task because `D:/workSpace/PROJECT_SPACE/FT546` is not a Git repository.
|
|
|
|
### Task 5: Documentation, Full Regression, and Final Replay Evidence
|
|
|
|
**Files:**
|
|
- Modify: `docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md`
|
|
- Modify: `docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md`
|
|
- Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/README.md`
|
|
- Modify: `D:/workSpace/PROJECT_SPACE/FT546/docs/FT546-WMS-history-replay-handoff-2026-07-22.md`
|
|
- Generate: `D:/workSpace/PROJECT_SPACE/FT546/outputs/ft546-tidy-batch-replay-<new-run-id>/report.md`
|
|
|
|
**Interfaces:**
|
|
- Historical documents remain intact except for a dated supersession note linking to the approved 2026-07-23 design and this implementation plan.
|
|
- The handoff reports the new run ID, recovery evidence, unit/Pester totals, final JAR hash, and restoration result.
|
|
|
|
- [ ] **Step 1: Add supersession notes before changing historical claims**
|
|
|
|
At the WCS recovery sections in both 2026-07-22 documents, add:
|
|
|
|
```markdown
|
|
> Superseded on 2026-07-23: WCS task-state lookup and the UNKNOWN-task replay case are replaced by WMS-native evidence recovery. See `docs/superpowers/specs/2026-07-23-wms-native-tidy-recovery-design.md` and `docs/superpowers/plans/2026-07-23-wms-native-tidy-recovery.md`. Vehicle snapshot behavior is not superseded.
|
|
```
|
|
|
|
Do not rewrite the historical task list or pretend its old acceptance evidence used the new design.
|
|
|
|
- [ ] **Step 2: Run focused and full WMS verification**
|
|
|
|
From `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`:
|
|
|
|
```powershell
|
|
$env:MAVEN_OPTS='-Xms64m -Xmx384m -XX:MaxMetaspaceSize=192m -XX:ReservedCodeCacheSize=64m -XX:+UseSerialGC'
|
|
mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,WcsTaskStateDependencyRemovalTest,WmsAlgorithmMapperWorkflowSqlTest,ToWmsServiceImplWorkflowCallbackTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
|
|
mvn -q "-DargLine=-Xms32m -Xmx128m -XX:MaxMetaspaceSize=96m -XX:ReservedCodeCacheSize=32m -XX:+UseSerialGC" test
|
|
mvn -DskipTests package
|
|
```
|
|
|
|
Expected: all focused tests pass, the root reactor has zero failure/error/skipped tests, and packaging exits 0.
|
|
|
|
- [ ] **Step 3: Run replay contract tests and read-only preflight**
|
|
|
|
From `D:/workSpace/PROJECT_SPACE/FT546`:
|
|
|
|
```powershell
|
|
Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed
|
|
pwsh -File .\tools\history-replay\Invoke-Ft546TidyBatchReplay.ps1 -WmsBaseUrl http://127.0.0.1:18082 -SimulatorPort 18081 -PreflightOnly
|
|
```
|
|
|
|
Expected: Pester passes and preflight creates a new output directory without changing the database.
|
|
|
|
- [ ] **Step 4: Run the destructive replay under the existing verified-backup guard**
|
|
|
|
Confirm ports 18081/18082 are unused and do not stop the user's WMS on 18080. Then run:
|
|
|
|
```powershell
|
|
pwsh -File .\tools\history-replay\Invoke-Ft546TidyBatchReplay.ps1 -WmsBaseUrl http://127.0.0.1:18082 -SimulatorPort 18081 -ConfirmReplay
|
|
```
|
|
|
|
Do not use `-KeepFailedState`. If backup creation, restore, or baseline verification fails, stop further writes and preserve the run output for DBA review.
|
|
|
|
- [ ] **Step 5: Verify the new run artifacts rather than trusting the exit code**
|
|
|
|
Inspect the newest run directory and require all of the following:
|
|
|
|
```text
|
|
summary.completed = true
|
|
summary.restoration.passed = true
|
|
RESTORE event mismatches = []
|
|
TIDY_RECOVERY_FINISHED pass = true
|
|
TIDY_RECOVERY_MISSING_JOB pass = true
|
|
RECOVERY_FINISHED callback event count = 1 for the repair workflow
|
|
missing-job workflow status_reason = RECOVERY_JOB_MISSING
|
|
missing-job active lock count > 0 before restore
|
|
simulator task-state request count = 0
|
|
```
|
|
|
|
Also confirm `tidy-reconciliation.csv` contains the unchanged automatic/manual rows plus both new recovery rows and that ports 18081/18082 have no listener after cleanup.
|
|
|
|
- [ ] **Step 6: Update the replay README and handoff with measured evidence**
|
|
|
|
Replace the handoff's WCS UNKNOWN claim with the two WMS-local cases. Record exact output paths, test totals, final JAR size/hash, backup path, restoration result, and any remaining limitations. Do not reuse the 2026-07-23 04:29 evidence as proof of the new recovery implementation.
|
|
|
|
- [ ] **Step 7: Commit only WMS repository documentation and perform final checks**
|
|
|
|
From `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`:
|
|
|
|
```powershell
|
|
git add docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md
|
|
git commit --only -m "docs: supersede WCS tidy task-state recovery" docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md
|
|
git diff --check 590eea6..HEAD
|
|
git status --short
|
|
```
|
|
|
|
Expected: `git diff --check` prints nothing. `git status --short` may still show the user's pre-existing changes, but none were reset, unstaged, or included in these focused commits.
|