From 692f778120e14b6b803027b4a080ef5df7ea8c1a Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Thu, 30 Jul 2026 13:51:28 +0800 Subject: [PATCH] test: verify shadow traffic invariants --- javaTest/README.md | 29 ++++- .../shadow/ShadowInvariantPropertyTest.java | 108 ++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 javaTest/src/test/java/com/ft546/wcs/shadow/ShadowInvariantPropertyTest.java diff --git a/javaTest/README.md b/javaTest/README.md index 46e6d70..a9afe4d 100644 --- a/javaTest/README.md +++ b/javaTest/README.md @@ -1,6 +1,6 @@ # FT546 WCS 中心调度与交通管制 Demo -这是一个独立的 Java 8 Maven Demo,用于模拟 FT546 WCS 的 LEGACY 中心调度和交通管制行为。 +这是一个独立的 Java 8 Maven Demo。它保留 FT546 WCS 的 LEGACY 调度模拟,并旁路提供 P0-B1 离线 SHADOW 交通决策内核。 ## 安全边界 @@ -8,7 +8,9 @@ - 不启动 Spring,不连接 SQL Server、PLC 或 WMS。 - 不发送真实设备命令。 - 不修改生产 `ruoyi-*` 模块。 -- 不启用 SHADOW 或 ACTIVE 控制模式。 +- SHADOW 仅在离线内存中计算、记录和回放,写端口会直接拒绝调用。 +- 不启用现场 SHADOW、HIL、LIMITED_ACTIVE、ACTIVE 或 PRODUCTION。 +- 不把离线模拟结果作为现场证据。 ## 已覆盖行为 @@ -21,10 +23,26 @@ - 队列 `0/1 → 1/3 → 1/4` 状态流转、完成归档和占用释放。 - 无路可达时返回 HOLD,而不是抛出空指针。 +## P0-B1 离线 SHADOW 覆盖 + +- 带版本、区域确认和基础地图校验和的不可变交通地图。 +- `NODE`、规范化无向 `EDGE` 和显式 `CONFLICT` 资源及容量。 +- `SOFT/COMMIT_GUARD/HARD/UNKNOWN_HARD/RELEASED` 资源保护契约。 +- 区段 Prepare/Commit 状态机及非法跳转拒绝。 +- 带 `plcBootId/heartbeatSeq/snapshotSeq` 的不可变复合快照。 +- 陈旧、序号倒退、PLC 重启、失联和人工模式的 fail-closed 检查。 +- 确定性 `GRANT/HOLD/YIELD`、等待图环拒绝及安全让行点门禁。 +- 匹配反馈逐步释放;重复、跳点、版本错配和 ACK 未知不释放。 +- SHADOW PLC、业务状态和权威预约三类禁写端口。 +- JSON 黄金决策事件、SHA-256 输入哈希和离线回放。 +- 固定种子 `546001` 至 `546010` 的容量、无环和确定性不变量回归。 + +`com.ft546.wcs.demo` 是 LEGACY 演示;新增能力位于 `com.ft546.wcs.shadow`,交通区段状态不会写入 LEGACY 业务队列。 + ## 运行测试 ```bash -mvn test +mvn clean test ``` 测试必须由 Maven Surefire 实际发现并执行;验收标准为测试数大于 0、Failures 为 0、Errors 为 0。 @@ -46,3 +64,8 @@ mvn exec:java - `MOVE`:模拟车辆推进 - `COMPLETE`:队列完成并归档 - `SUMMARY`:最终周期、活动队列、历史和协议尝试次数 +- `SHADOW-SUMMARY`:离线 SHADOW 的 GRANT/HOLD 数量和零写入声明 + +## 未覆盖的生产门槛 + +本项目不包含 SQL Server 事务与 Outbox、真实 PLC Prepare/Commit、CRC 与寄存器布局、提升机物理信号、深位真实执行、硬件在环、现场 SHADOW 样本或区域 ACTIVE 回退。相关能力仍以 `docs/WCS算法优化` 的外部签字和验收门禁为准。 diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/ShadowInvariantPropertyTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/ShadowInvariantPropertyTest.java new file mode 100644 index 0000000..326c10f --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/ShadowInvariantPropertyTest.java @@ -0,0 +1,108 @@ +package com.ft546.wcs.shadow; + +import com.ft546.wcs.shadow.decision.DecisionType; +import com.ft546.wcs.shadow.decision.RollingDispatchPolicy; +import com.ft546.wcs.shadow.decision.ShadowCandidate; +import com.ft546.wcs.shadow.decision.ShadowDecision; +import com.ft546.wcs.shadow.decision.WaitForGraph; +import com.ft546.wcs.shadow.map.EffectiveTrafficMap; +import com.ft546.wcs.shadow.map.TrafficMapCompiler; +import com.ft546.wcs.shadow.resource.ResourceKey; +import com.ft546.wcs.shadow.resource.ResourceLedger; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ShadowInvariantPropertyTest { + + @Test + void fixedSeedsPreserveCapacityAcyclicityAndDeterminism() { + for (long seed = 546001L; seed <= 546010L; seed++) { + verifySeed(seed); + } + } + + private void verifySeed(long seed) { + Random random = new Random(seed); + EffectiveTrafficMap map = map(); + int vehicleCount = 2 + random.nextInt(7); + List candidates = new ArrayList(); + for (int index = 0; index < vehicleCount; index++) { + String site = "N" + random.nextInt(4); + candidates.add(ShadowCandidate.waiting( + "T" + index, "V" + index, false, random.nextInt(5), + random.nextInt(100), random.nextInt(10), + Collections.singleton(ResourceKey.node(site)), null)); + } + + RollingDispatchPolicy policy = new RollingDispatchPolicy(); + List first = + policy.decide(candidates, new ResourceLedger(map.getCapacities()), map); + List second = + policy.decide(candidates, new ResourceLedger(map.getCapacities()), map); + assertEquals(signature(first), signature(second), + "seed=" + seed + " mapVersion=" + map.getVersionId() + " candidates=" + vehicleCount); + + ResourceLedger accepted = new ResourceLedger(map.getCapacities()); + for (ShadowDecision decision : first) { + if (decision.getType() == DecisionType.GRANT) { + ShadowCandidate candidate = find(candidates, decision.getVehicleId()); + assertTrue(accepted.evaluate(candidate.getVehicleId(), candidate.getResources()).isGranted(), + "seed=" + seed + " duplicate grant for " + candidate.getResources()); + } + } + for (String site : Arrays.asList("N0", "N1", "N2", "N3")) { + assertTrue(accepted.protectedCount(ResourceKey.node(site)) <= 1, + "seed=" + seed + " capacity exceeded at " + site); + } + + WaitForGraph graph = new WaitForGraph(); + for (int index = 0; index < vehicleCount; index++) { + String waiter = "V" + index; + String holder = "V" + random.nextInt(vehicleCount); + if (!waiter.equals(holder) + && !graph.wouldCreateCycle(waiter, Collections.singleton(holder))) { + graph.addWait(waiter, Collections.singleton(holder)); + } + assertTrue(graph.isAcyclic(), "seed=" + seed + " wait graph contains a cycle"); + } + } + + private ShadowCandidate find(List candidates, String vehicleId) { + for (ShadowCandidate candidate : candidates) { + if (candidate.getVehicleId().equals(vehicleId)) return candidate; + } + throw new IllegalStateException("candidate not found " + vehicleId); + } + + private List signature(List decisions) { + List result = new ArrayList(); + for (ShadowDecision decision : decisions) { + result.add(decision.getVehicleId() + "|" + decision.getType() + "|" + decision.getReasonCode()); + } + return result; + } + + private EffectiveTrafficMap map() { + LinkedHashSet nodes = + new LinkedHashSet(Arrays.asList("N0", "N1", "N2", "N3")); + LinkedHashSet edges = + new LinkedHashSet(); + edges.add(new TrafficMapCompiler.EdgeDefinition("N0", "N1")); + edges.add(new TrafficMapCompiler.EdgeDefinition("N1", "N2")); + edges.add(new TrafficMapCompiler.EdgeDefinition("N2", "N3")); + return new TrafficMapCompiler().compile(new TrafficMapCompiler.MapDefinition( + "FT546", 2L, "ZONE-PROPERTY", true, "sha", "sha", nodes, edges, + new LinkedHashMap(), Collections.singleton("N0"), + Collections.singleton("N3"))); + } +}