Files
FT546-WCS/docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md

466 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Standard WCS Simulation Acceptance 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:** 用真实标准内核和 Adapter 契约完成确定性模拟、第二仓配置、FT546 历史回放、故障恢复、性能和长期稳定性验收。
**Architecture:** 快速领域测试保留在 `ruoyi-wcs`,真实历史语料继续由独立 `javaTest` 工程加载,但它改为调用标准 `ruoyi-wcs` 构建产物,不再用自身的演示调度器证明产品完成。所有长测试输出统一报告、JSON 摘要和 SHA-256 manifest。
**Tech Stack:** Java 8, Maven, JUnit 5, existing `javaTest` historical fixtures, SQL Server read-only replay input, deterministic simulated adapters.
## Global Constraints
- 依赖 Plan 4 已完成的标准运行 wiring 和 FT546 Adapter。
- 模拟器必须实现与真实设备相同的 `DeviceCommandGateway` 和 `DeviceSnapshotGateway`。
- 历史车辆、路径、完成时间和状态只用于比较,不作为新调度输入。
- FT546 历史回放允许车辆和路径不同,但业务终点、PLC 语义和安全不变量必须正确。
- 第二套仓库配置不得修改 `ruoyi-wcs` 代码或构建产物。
- 72 小时/100000 任务测试使用固定随机种子并可从失败种子复现。
- 长测试不进入普通 `mvn test`;使用显式 Maven profile 和 JUnit tag。
- 验收报告不得包含数据库密码、PLC IP 密码或 WMS 签名密钥。
---
### Task 1: Build deterministic simulated adapters and fault injection
**Files:**
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulationClock.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulatedDeviceGateway.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulatedDeviceRuntime.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/DeterministicFaultPlan.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/FaultType.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulationResult.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/StandardWcsSimulationTest.java`
**Interfaces:**
- `SimulationClock.advance(Duration)` controls all event time.
- `DeterministicFaultPlan.atTick(long,FaultType,String deviceId)` returns one immutable plan and injects `DISCONNECT`, `COMMAND_ACK_LOST`, `DUPLICATE_EVENT`, `OUT_OF_ORDER_EVENT`, `COMMAND_REJECTED`, `POSITION_MISMATCH`, `LOAD_MISMATCH`, `MANUAL_MODE`, or `WCS_RESTART`.
- `SimulationResult` exposes event digest, metrics, command counts, callback counts, final command/order states, recovery cases, and invariant violations.
- Simulated gateway records standard command envelopes and emits standard events only.
- [ ] **Step 1: Write failing deterministic replay tests**
```java
@Test
void sameSeedProducesTheSameEventStream() {
SimulationResult first = run(20260804L, 500);
SimulationResult second = run(20260804L, 500);
assertEquals(first.getEventDigest(), second.getEventDigest());
assertEquals(first.getMetrics(), second.getMetrics());
}
@Test
void commitTimeoutCreatesUnknownWithoutDuplicateMotion() {
SimulationResult result = run(DeterministicFaultPlan.atTick(
12, FaultType.COMMAND_ACK_LOST, "V1"));
assertEquals(1, result.motionCommandCount("CMD-1"));
assertEquals("UNKNOWN", result.commandState("CMD-1"));
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-wcs -am -Dtest=StandardWcsSimulationTest test`
- [ ] **Step 3: Implement the event-driven simulator**
Process each tick in fixed order: due fault → device physics → event delivery → snapshot ingest → recovery gate → dispatch → command creation → Outbox send → invariant scan. Sort devices, events, tasks and resources by stable ID before each phase.
- [ ] **Step 4: Run GREEN**
Run: `mvn -pl ruoyi-wcs -am -Dtest=StandardWcsSimulationTest test`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation
git commit -m "test(wcs): add deterministic device simulation"
```
### Task 2: Prove core portability with a second warehouse configuration
**Files:**
- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/topology.json`
- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/devices.json`
- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/workflows.json`
- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/policies.json`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/SecondWarehouseConfigurationTest.java`
**Interfaces:**
- Warehouse B has 4 floors, 6 shuttles, 2 lifts, 8 conveyor ports, and 3 chargers.
- Uses the same `TopologyDefinitionProvider`, `WorkflowDefinitionProvider`, `DispatchPolicy`, `PortSelectionPolicy`, and `ChargingPolicy` ports.
- [ ] **Step 1: Write a failing no-core-change acceptance test**
```java
@Test
void runsDifferentWarehouseThroughTheSameCoreArtifact() {
AcceptanceResult result = runner.run("acceptance/warehouse-b", 5000);
assertEquals(200, result.getCompletedOrders());
assertEquals(0, result.getFailedOrders());
assertEquals(0, result.getInvariantViolations());
assertEquals("ruoyi-wcs", result.getCoreArtifactId());
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-wcs -am -Dtest=SecondWarehouseConfigurationTest test`
- [ ] **Step 3: Add the complete synthetic configuration**
Topology includes two independent traffic zones, both lifts, a shared source-floor intersection, capacity-1 chargers, safe wait points, inbound/outbound/relocation/inventory workflows, and policy weights different from FT546.
- [ ] **Step 4: Run GREEN and record the core JAR hash**
Run: `mvn -pl ruoyi-wcs -am package`
Run: `mvn -pl ruoyi-wcs -am -Dtest=SecondWarehouseConfigurationTest test`
Record SHA-256 of `ruoyi-wcs/target/ruoyi-wcs-*.jar` in the test report; the same hash is later recorded for FT546.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-wcs/src/test/resources/acceptance/warehouse-b ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/SecondWarehouseConfigurationTest.java
git commit -m "test(wcs): prove second warehouse portability"
```
### Task 3: Make javaTest replay the real standard WCS core
**Files:**
- Modify: `javaTest/pom.xml`
- Create: `javaTest/src/main/java/com/ft546/wcs/standard/StandardWcsHistoricalReplayAdapter.java`
- Create: `javaTest/src/main/java/com/ft546/wcs/standard/StandardWcsReplayResult.java`
- Create: `javaTest/src/test/java/com/ft546/wcs/standard/StandardWcsHistoricalReplayTest.java`
**Interfaces:**
- Adds dependency `com.ruoyi:ruoyi-wcs:3.8.9` to `javaTest`.
- Consumes existing `HistoricalQueueLoader` and frozen manifest.
- Maps each record to canonical `CreateTransportOrderCommand`; historical `agv_id/path_info/finish_date` remain comparison metadata only.
- [ ] **Step 1: Install the core and write a failing replay test**
Run: `mvn -pl ruoyi-wcs -am install -DskipTests`
Add:
```java
@Test
void completesFrozenHistoryThroughTheStandardCore() {
StandardWcsReplayResult result = adapter.replay(loader.load(classLoader, "shadow/history"));
assertEquals(result.getInputOrders(), result.getCompletedOrders());
assertEquals(0, result.getSafetyViolations());
assertEquals(0, result.getDuplicateBusinessEffects());
}
```
- [ ] **Step 2: Run RED**
Run: `cd javaTest && mvn -Dtest=StandardWcsHistoricalReplayTest test`
Expected: compilation fails because the standard replay adapter does not exist.
- [ ] **Step 3: Implement canonical replay mapping**
Use record task number as `externalRequestId`, map task type to standard order type, resolve physical source/destination from frozen map data, submit through `TransportOrderUseCase`, and run the real `WcsRuntimeCoordinator` against deterministic simulated adapters until terminal or watchdog failure.
- [ ] **Step 4: Produce semantic comparison fields**
For each task record input task number, historical vehicle/path/duration, selected standard vehicle/path/duration, final order state, callback effect count, and violation list. Do not assert identical vehicle or path.
- [ ] **Step 5: Run GREEN with existing history tests**
Run: `cd javaTest && mvn -Dtest=StandardWcsHistoricalReplayTest,HistoricalQueueLoaderTest,HistoricalQueueDatabaseReplayTest test`
- [ ] **Step 6: Commit**
```bash
git add javaTest/pom.xml javaTest/src/main/java/com/ft546/wcs/standard javaTest/src/test/java/com/ft546/wcs/standard
git commit -m "test(javaTest): replay history through standard WCS core"
```
### Task 4: Add PLC sample corpus regression
**Files:**
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/manifest.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-command-cases.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-two-stage-cases.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-snapshot-cases.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/lift-cases.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/conveyor-cases.json`
- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/charger-cases.json`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546PlcCorpusRegressionTest.java`
**Interfaces:**
- Each corpus case contains sanitized input facts, expected standard command/event, expected addresses, and expected certainty `NOT_SENT/ACCEPTED/ACCEPTED_UNKNOWN/REJECTED`.
- Manifest stores row counts and SHA-256 for every sample file.
- The manifest emits `Ft546CapabilityEvidence` only when every required point-to-point case passes and, when `TWO_STAGE_ATOMIC_TASK` is requested, every two-stage case passes for the exact profile version.
- [ ] **Step 1: Write a failing manifest and mapping test**
```java
@ParameterizedTest
@MethodSource("commandCases")
void mapsFrozenCommandCases(PlcCommandCase sample) {
EncodedWrites writes = encoder.encode(sample.getCommand(), sample.getProfile());
assertEquals(sample.getExpectedWrites(), writes.asAddressValuePairs());
}
@Test
void refusesTwoStageCapabilityWhenItsCorpusIsMissingOrFails() {
CapabilityGateResult result = gate.evaluate(twoStageProfile(), corpusWithoutPassingTwoStage());
assertFalse(result.isPublishable());
assertEquals("TWO_STAGE_PROTOCOL_GATE_FAILED", result.getReasonCode());
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546PlcCorpusRegressionTest test`
- [ ] **Step 3: Freeze sanitized production samples**
Export only bounded register values and logical facts; remove IPs, credentials and unrelated data. Calculate canonical UTF-8 hashes and reject corpus load when any hash or row count differs.
- [ ] **Step 4: Run all Adapter contract tests GREEN**
Run: `mvn -pl ruoyi-admin -am -Dtest='Ft546*AdapterTest,Ft546PlcCorpusRegressionTest' test`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-admin/src/test/resources/ft546/plc/corpus ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546PlcCorpusRegressionTest.java
git commit -m "test(ft546): freeze PLC protocol regression corpus"
```
### Task 5: Add the real PLC and hardware-in-the-loop contract gate
**Files:**
- Modify: `ruoyi-admin/pom.xml`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/Ft546PlcHilContractTest.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/HilSafetyGuard.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/HilContractReportWriter.java`
- Create: `docs/wcs/acceptance/ft546-plc-hil-runbook.md`
- Create at runtime: `outputs/wcs-acceptance/ft546-plc-hil-report.json`
**Interfaces:**
- Maven profile `ft546-plc-hil` excludes the suite unless `ft546.hil.enabled=true` and requires an isolated test PLC endpoint, expected profile version, device ID, safe start site, safe target site, and operator approval ID.
- `HilSafetyGuard.verify()` performs read-only connection, AUTO/healthy/idle state, no load, no active command, expected start site, E-stop-chain-ready fact, and exclusive test lease checks before any write.
- The suite always verifies snapshot mapping and point-to-point write/ack/final-fact semantics; it verifies the legacy two-stage path only when the published profile requests `TWO_STAGE_ATOMIC_TASK`.
- [ ] **Step 1: Write failing guard and report tests with a fake PLC runtime**
```java
@Test
void refusesMotionWhenAnySafetyPreconditionIsMissing() {
fakeRuntime.setEmergencyChainReady(false);
assertThrows(HilSafetyException.class, () -> guard.verify());
assertEquals(0, fakeRuntime.writeCount());
}
@Test
void sanitizedReportContainsNoEndpointOrCredential() throws Exception {
Path report = writer.write(passingHilResult());
String json = new String(Files.readAllBytes(report), StandardCharsets.UTF_8);
assertFalse(json.contains("192.168."));
assertFalse(json.toLowerCase(Locale.ROOT).contains("password"));
}
```
- [ ] **Step 2: Run RED on the fake runtime**
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546PlcHilContractTest test`
- [ ] **Step 3: Implement the guarded HIL profile**
The test acquires an exclusive lease, records the initial snapshot, sends exactly one enabled command through `Ft546LegacySrgvAdapter`, waits for the standard completion predicate, returns the rig to its approved safe site, records the final snapshot, and releases the lease. Any timeout becomes `ACCEPTED_UNKNOWN`, stops further motion writes, records the isolated state, and requires the operator runbook before the suite can be rerun.
- [ ] **Step 4: Run against the isolated FT546 test PLC**
```bash
mvn -pl ruoyi-admin -Pft546-plc-hil \
-Dft546.hil.enabled=true \
-Dft546.hil.endpointRef=FT546_HIL_ENDPOINT \
-Dft546.hil.profileVersion=1 \
-Dft546.hil.deviceId=1 \
-Dft546.hil.startSite=HIL_SAFE_A \
-Dft546.hil.targetSite=HIL_SAFE_B \
-Dft546.hil.operatorApprovalId=${HIL_APPROVAL_ID} \
-Dtest=Ft546PlcHilContractTest test
```
Expected: all enabled command contracts pass, the device returns to the approved safe site, and `ft546-plc-hil-report.json` contains the profile hash, test cases, sanitized facts, result, Git commit, operator approval ID, and run time.
- [ ] **Step 5: Commit the suite and runbook**
```bash
git add ruoyi-admin/pom.xml ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil docs/wcs/acceptance/ft546-plc-hil-runbook.md
git commit -m "test(ft546): add guarded PLC hardware contract gate"
```
### Task 6: Verify restart, fault recovery, and manual takeover matrix
**Files:**
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/RecoveryFaultMatrixTest.java`
- Create: `ruoyi-wcs/src/test/resources/acceptance/fault-matrix.json`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/RecoveryMatrixReportWriter.java`
**Interfaces:**
- Matrix includes send-before failure, ACK loss, device offline, PLC restart, position mismatch, load mismatch, lift fault, conveyor mode conflict, WMS callback failure, WCS restart, duplicate restart, operator-confirmed resume, and operator-aborted task.
- Produces one `RecoveryScenarioResult` per scenario with terminal state, isolation scope, manual requirement, released resources, duplicate command count, and duplicate callback count.
- Produces `manual-recovery-audit.json` with operator ID, reason, submitted evidence, pre-state, post-state, affected resources, permission result, and timestamp for the two manual scenarios.
- [ ] **Step 1: Write failing matrix-completeness test**
```java
@Test
void executesEveryRequiredRecoveryScenario() {
RecoveryMatrixResult result = runner.run("acceptance/fault-matrix.json");
assertEquals(13, result.getScenarioCount());
assertEquals(0, result.getBlindRetries());
assertEquals(0, result.getUnsafeReleases());
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-wcs -am -Dtest=RecoveryFaultMatrixTest test`
- [ ] **Step 3: Implement restart checkpoints**
For each scenario stop the runtime after the configured persisted checkpoint, discard all in-memory services, construct a new runtime with the same database snapshot and a new instance token, then run `StartupRecoveryService.recover()` before any dispatch.
- [ ] **Step 4: Run GREEN and write report**
Run: `mvn -pl ruoyi-wcs -am -Dtest=RecoveryFaultMatrixTest test`
Expected reports: `outputs/wcs-acceptance/recovery-matrix.json` with 13 PASS rows and zero blind retry/unsafe release, plus `outputs/wcs-acceptance/manual-recovery-audit.json` with two fully audited manual actions.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance ruoyi-wcs/src/test/resources/acceptance/fault-matrix.json
git commit -m "test(wcs): verify recovery fault matrix"
```
### Task 7: Add performance and long-run profiles
**Files:**
- Modify: `ruoyi-wcs/pom.xml`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/PerformanceBaselineTest.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/LongRunStabilityTest.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceMetrics.java`
**Interfaces:**
- Maven profile `wcs-performance` includes JUnit tag `performance`.
- Maven profile `wcs-long-run` includes JUnit tag `long-run` and properties `wcs.longRun.hours`, `wcs.longRun.minimumTasks`, `wcs.seed`.
- [ ] **Step 1: Write failing baseline assertions**
```java
@Test
@Tag("performance")
void meetsTheApprovedStandardScale() {
AcceptanceMetrics metrics = benchmark.run(scale(50, 10, 300, 20, 2000));
assertTrue(metrics.getDispatchP95Millis() <= 500);
assertTrue(metrics.getEventCommitP95Millis() <= 200);
assertTrue(metrics.getAutomaticRecoveryMillis() <= 60000);
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-wcs -Pwcs-performance -Dtest=PerformanceBaselineTest test`
- [ ] **Step 3: Implement measurement without weakening assertions**
Warm up 1000 cycles, measure at least 10000 cycles, use `System.nanoTime()` only for elapsed time, publish P50/P95/P99 and throughput, and fail when task backlog grows for 100 consecutive cycles.
- [ ] **Step 4: Add explicit long-run entry**
```java
@Test
@Tag("long-run")
void completesTheConfiguredLongRunWithoutInvariantViolations() {
AcceptanceMetrics metrics = longRun.run(hours(), minimumTasks(), seed());
assertTrue(metrics.getCompletedTasks() >= minimumTasks());
assertEquals(0, metrics.getSafetyViolations());
assertEquals(0, metrics.getDuplicateBusinessEffects());
}
```
- [ ] **Step 5: Run short CI and full acceptance modes**
CI smoke: `mvn -pl ruoyi-wcs -Pwcs-long-run -Dwcs.longRun.hours=0.05 -Dwcs.longRun.minimumTasks=1000 -Dwcs.seed=20260804 test`
Full acceptance: `mvn -pl ruoyi-wcs -Pwcs-long-run -Dwcs.longRun.hours=72 -Dwcs.longRun.minimumTasks=100000 -Dwcs.seed=20260804 test`
- [ ] **Step 6: Commit**
```bash
git add ruoyi-wcs/pom.xml ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance
git commit -m "test(wcs): add performance and long run acceptance"
```
### Task 8: Generate the final acceptance evidence bundle
**Files:**
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceReportWriter.java`
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceReportWriterTest.java`
- Create: `docs/wcs/acceptance/README.md`
- Create: `docs/wcs/acceptance/standard-wcs-acceptance-index.md`
- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/summary.json`
- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/report.md`
- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/manifest.sha256`
**Interfaces:**
- `summary.json` contains Git commit, core JAR hash, FT546 config hash, Warehouse B config hash, test counts, replay counts, performance metrics, recovery matrix, safety counts, start/end time, and verdict.
- `report.md` contains dedicated sections for structured-log field coverage, required metric coverage, manual takeover audit, PLC corpus capability evidence, and the latest guarded HIL report.
- Manifest covers every file except itself.
- [ ] **Step 1: Write a failing evidence-completeness test**
```java
@Test
void writesACompleteTamperEvidentEvidenceBundle() throws Exception {
Path output = writer.write(passingAcceptanceResult());
assertTrue(Files.exists(output.resolve("summary.json")));
assertTrue(Files.exists(output.resolve("report.md")));
assertTrue(Files.exists(output.resolve("manifest.sha256")));
assertTrue(manifestVerifier.verify(output));
}
```
- [ ] **Step 2: Run RED**
Run: `mvn -pl ruoyi-wcs -am -Dtest=AcceptanceReportWriterTest test`
- [ ] **Step 3: Implement canonical reports and verdict rules**
Verdict is PASS only when both warehouse configurations use the same core JAR hash, all required tests ran, historical replay has no safety violations, fault/manual matrix passes, required logs and metrics are observed, PLC capability evidence matches the published profile, the guarded HIL report passes every enabled FT546 command, performance thresholds pass, long-run minimum task count is reached, and every absolute safety count is zero.
- [ ] **Step 4: Run the complete standard verification**
```bash
mvn -pl ruoyi-wcs,ruoyi-admin -am test
mvn -pl ruoyi-wcs -Pwcs-performance test
cd javaTest && mvn clean test
cd ../ruoyi-ui && npm run build:prod
```
Then run the approved full long-run command and generate the evidence bundle.
- [ ] **Step 5: Verify repository and evidence integrity**
Run: `git diff --check`
Run the manifest verifier against the generated directory. Expected: all hashes match and the acceptance index links the exact run ID and Git commit.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance docs/wcs/acceptance
git commit -m "docs(wcs): record standard product acceptance"
```