feat(javaTest): visualize source-aligned task phases
This commit is contained in:
@@ -91,17 +91,71 @@ def verify_map_interactions(page):
|
||||
page.locator("#show-move-trace").check()
|
||||
|
||||
|
||||
def verify_historical_mode(page):
|
||||
"""Run 240 tasks and inspect one real cross-floor dispatch without drawing a fake edge."""
|
||||
def select_task_with_phase(page, phase_name):
|
||||
"""Select the first visible business task that exposes the requested backend phase."""
|
||||
rows = page.locator("#history-task-body tr")
|
||||
for index in range(rows.count()):
|
||||
rows.nth(index).click()
|
||||
if page.locator("#phase-list [data-phase='%s']" % phase_name).count():
|
||||
return
|
||||
raise AssertionError("no task exposed business phase " + phase_name)
|
||||
|
||||
|
||||
def assert_no_cross_floor_route_segments(page):
|
||||
"""Verify that transfer jobs use port markers rather than fake map edges."""
|
||||
cross_floor_segments = page.evaluate("""
|
||||
async () => {
|
||||
const bootstrap = await fetch('/api/bootstrap').then(response => response.json());
|
||||
const floors = Object.fromEntries(
|
||||
bootstrap.sites.map(site => [site.siteId, Number(site.floor)]));
|
||||
return Array.from(document.querySelectorAll(
|
||||
'#dynamic-route-layer .route-segment title'))
|
||||
.map(title => title.textContent.split(' → '))
|
||||
.filter(parts => parts.length === 2
|
||||
&& floors[parts[0]] !== undefined
|
||||
&& floors[parts[1]] !== undefined
|
||||
&& floors[parts[0]] !== floors[parts[1]]);
|
||||
}
|
||||
""")
|
||||
require(not cross_floor_segments,
|
||||
"normal route layer drew cross-floor edges: " + str(cross_floor_segments))
|
||||
|
||||
|
||||
def verify_historical_mode(page, switch_raw=True):
|
||||
"""Run business replay, inspect phase/transfer paths, then verify RAW compatibility."""
|
||||
page.locator(".mode-tab[data-mode='history']").click()
|
||||
require(page.locator("#simulation-mode-business").is_checked(),
|
||||
"business replay must be the default historical mode")
|
||||
page.locator("#run-history").click()
|
||||
expect(page.locator("#history-run-state")).to_have_text(
|
||||
re.compile(r"^最近运行"), timeout=180_000
|
||||
)
|
||||
require("240" in page.locator("#task-filter-count").inner_text(),
|
||||
"historical result did not expose all 240 tasks")
|
||||
require(page.locator("#history-task-body tr").count() == 240,
|
||||
"historical task table row count is not 240")
|
||||
require(page.locator("#raw-snapshot-count").inner_text() == "240",
|
||||
"input audit did not expose 240 raw snapshots")
|
||||
require(page.locator("#business-task-count").inner_text() == "76",
|
||||
"input audit did not expose 76 business parent tasks")
|
||||
require("76" in page.locator("#task-filter-count").inner_text(),
|
||||
"business replay did not expose 76 parent tasks")
|
||||
require(page.locator("#history-task-body tr").count() == 76,
|
||||
"business replay table row count is not 76")
|
||||
|
||||
select_task_with_phase(page, "EMPTY_RETURN_TO_TARGET_FRONT")
|
||||
require(page.locator("#phase-list [data-phase]").count() >= 2,
|
||||
"business task did not expose its multi-stage route evidence")
|
||||
require(page.locator("#business-completed-at").inner_text() != "--",
|
||||
"business completion time is missing")
|
||||
require("车辆已释放" in page.locator("#vehicle-released-at").inner_text(),
|
||||
"vehicle release evidence is incomplete")
|
||||
selected_phase = page.locator(
|
||||
"#phase-list [data-phase='EMPTY_RETURN_TO_TARGET_FRONT']"
|
||||
)
|
||||
selected_phase.click()
|
||||
require(page.locator("#dynamic-route-layer .business-phase").count() > 0,
|
||||
"selected business phase did not render a same-floor route")
|
||||
require(page.locator(".map-floor-layer:not([hidden]) .map-edge").count() > 20,
|
||||
"phase selection replaced the authoritative full map")
|
||||
assert_no_cross_floor_route_segments(page)
|
||||
|
||||
transfer_rows = page.locator(
|
||||
"#history-task-body tr[data-assignment-mode='FLOOR_TRANSFER']"
|
||||
)
|
||||
@@ -132,22 +186,21 @@ def verify_historical_mode(page):
|
||||
).inner_text() == "换层完成",
|
||||
"transfer completion event did not receive a business label")
|
||||
|
||||
cross_floor_segments = page.evaluate("""
|
||||
async () => {
|
||||
const bootstrap = await fetch('/api/bootstrap').then(response => response.json());
|
||||
const floors = Object.fromEntries(
|
||||
bootstrap.sites.map(site => [site.siteId, Number(site.floor)]));
|
||||
return Array.from(document.querySelectorAll(
|
||||
'#dynamic-route-layer .route-segment title'))
|
||||
.map(title => title.textContent.split(' → '))
|
||||
.filter(parts => parts.length === 2
|
||||
&& floors[parts[0]] !== undefined
|
||||
&& floors[parts[1]] !== undefined
|
||||
&& floors[parts[0]] !== floors[parts[1]]);
|
||||
}
|
||||
""")
|
||||
require(not cross_floor_segments,
|
||||
"normal route layer drew cross-floor edges: " + str(cross_floor_segments))
|
||||
assert_no_cross_floor_route_segments(page)
|
||||
|
||||
if not switch_raw:
|
||||
return
|
||||
page.locator("label[for='simulation-mode-raw']").click()
|
||||
expect(page.locator("#task-filter-count")).to_have_text("240 条", timeout=180_000)
|
||||
require(page.locator("#history-task-body tr").count() == 240,
|
||||
"RAW compatibility replay did not expose all 240 snapshots")
|
||||
require(page.locator("#replay-mode-readout").inner_text() == "RAW 快照",
|
||||
"replay mode readout did not switch to RAW")
|
||||
require(not page.locator("#show-historical").is_disabled(),
|
||||
"RAW mode did not restore the historical path layer")
|
||||
page.locator("#history-task-body tr").first.click()
|
||||
require(page.locator("#route-difference").inner_text() != "共同 -- / 新增 -- / 历史 --",
|
||||
"RAW task did not render historical/new path difference evidence")
|
||||
|
||||
|
||||
def verify_viewport(browser, viewport, filename, run_history):
|
||||
@@ -166,7 +219,7 @@ def verify_viewport(browser, viewport, filename, run_history):
|
||||
run_point_to_point(page)
|
||||
verify_map_interactions(page)
|
||||
if run_history:
|
||||
verify_historical_mode(page)
|
||||
verify_historical_mode(page, switch_raw=viewport["width"] > 580)
|
||||
require(not horizontal_overflow(page), "page has horizontal overflow after simulation")
|
||||
page.screenshot(path=os.path.join(OUTPUT_DIR, filename), full_page=True)
|
||||
require(not errors, "browser console/page errors: " + " | ".join(errors))
|
||||
@@ -186,7 +239,7 @@ def main():
|
||||
launch_options["executable_path"] = cached_candidates[0]
|
||||
browser = playwright.chromium.launch(**launch_options)
|
||||
verify_viewport(browser, {"width": 1440, "height": 1000}, "desktop.png", True)
|
||||
verify_viewport(browser, {"width": 390, "height": 844}, "mobile.png", False)
|
||||
verify_viewport(browser, {"width": 390, "height": 844}, "mobile.png", True)
|
||||
browser.close()
|
||||
print("visualizer browser QA passed: desktop=1440x1000 mobile=390x844")
|
||||
|
||||
|
||||
@@ -23,9 +23,12 @@ public final class HistoricalDispatchVisualizerApplication {
|
||||
HistoricalQueueDataset dataset = classLoader.getResource("shadow/history/manifest.json") != null
|
||||
? loader.load(classLoader, "shadow/history")
|
||||
: loader.load(Paths.get("src/test/resources/shadow/history"));
|
||||
HistoricalSimulationInput input = new HistoricalSimulationInputFactory().create(dataset);
|
||||
HistoricalSimulationInputFactory factory = new HistoricalSimulationInputFactory();
|
||||
HistoricalSimulationInput businessInput = factory.create(dataset);
|
||||
HistoricalSimulationInput rawInput = factory.createRawSnapshots(dataset);
|
||||
HistoricalDispatchVisualizerServer server =
|
||||
new HistoricalDispatchVisualizerServer(port, dataset, input);
|
||||
new HistoricalDispatchVisualizerServer(
|
||||
port, dataset, businessInput, rawInput);
|
||||
CountDownLatch shutdown = new CountDownLatch(1);
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
server.stop();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationExecutionMode;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
@@ -35,25 +36,35 @@ public final class HistoricalDispatchVisualizerServer {
|
||||
private final HttpServer httpServer;
|
||||
private final ExecutorService executor;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HistoricalSimulationInput input;
|
||||
private final HistoricalSimulationInput businessInput;
|
||||
private final HistoricalSimulationInput rawInput;
|
||||
private final PointToPointSimulationService pointService;
|
||||
private final HistoricalWorkloadSimulationService historicalService;
|
||||
private final HistoricalWorkloadSimulationService businessHistoricalService;
|
||||
private final HistoricalWorkloadSimulationService rawHistoricalService;
|
||||
private final SimulationVisualizationMapper mapper;
|
||||
private final AtomicBoolean simulationBusy = new AtomicBoolean(false);
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
public HistoricalDispatchVisualizerServer(
|
||||
int port, HistoricalQueueDataset dataset, HistoricalSimulationInput input)
|
||||
int port, HistoricalQueueDataset dataset,
|
||||
HistoricalSimulationInput businessInput, HistoricalSimulationInput rawInput)
|
||||
throws IOException {
|
||||
if (port < 0 || port > 65_535) {
|
||||
throw new IllegalArgumentException("port must be between 0 and 65535");
|
||||
}
|
||||
if (dataset == null || input == null) {
|
||||
throw new IllegalArgumentException("dataset and input are required");
|
||||
if (dataset == null || businessInput == null || rawInput == null) {
|
||||
throw new IllegalArgumentException("dataset, business input and raw input are required");
|
||||
}
|
||||
this.input = input;
|
||||
this.pointService = new PointToPointSimulationService(input);
|
||||
this.historicalService = new HistoricalWorkloadSimulationService(input);
|
||||
if (businessInput.getExecutionMode()
|
||||
!= SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS
|
||||
|| rawInput.getExecutionMode() != SimulationExecutionMode.RAW_SNAPSHOT) {
|
||||
throw new IllegalArgumentException("historical inputs do not match their replay modes");
|
||||
}
|
||||
this.businessInput = businessInput;
|
||||
this.rawInput = rawInput;
|
||||
this.pointService = new PointToPointSimulationService(businessInput);
|
||||
this.businessHistoricalService = new HistoricalWorkloadSimulationService(businessInput);
|
||||
this.rawHistoricalService = new HistoricalWorkloadSimulationService(rawInput);
|
||||
this.mapper = new SimulationVisualizationMapper(dataset);
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.httpServer = HttpServer.create(new InetSocketAddress(
|
||||
@@ -131,14 +142,14 @@ public final class HistoricalDispatchVisualizerServer {
|
||||
Map<String, Object> health = new LinkedHashMap<String, Object>();
|
||||
health.put("status", "UP");
|
||||
health.put("mode", "OFFLINE_IN_MEMORY_SIMULATION");
|
||||
health.put("mapLoaded", !input.getGridMap().getSites().isEmpty());
|
||||
health.put("mapLoaded", !businessInput.getGridMap().getSites().isEmpty());
|
||||
health.put("simulationBusy", simulationBusy.get());
|
||||
writeJson(exchange, 200, health);
|
||||
return;
|
||||
}
|
||||
if ("/api/bootstrap".equals(path)) {
|
||||
requireMethod(method, "GET");
|
||||
writeJson(exchange, 200, mapper.bootstrap(input));
|
||||
writeJson(exchange, 200, mapper.bootstrap(businessInput));
|
||||
return;
|
||||
}
|
||||
if ("/api/simulations/point-to-point".equals(path)) {
|
||||
@@ -147,15 +158,19 @@ public final class HistoricalDispatchVisualizerServer {
|
||||
PointToPointSimulationRequest request = objectMapper.readValue(
|
||||
body, PointToPointSimulationRequest.class);
|
||||
writeJson(exchange, 200, runSimulation(() ->
|
||||
mapper.result(input, pointService.run(request))));
|
||||
mapper.result(businessInput, pointService.run(request))));
|
||||
return;
|
||||
}
|
||||
if ("/api/simulations/historical".equals(path)) {
|
||||
requireMethod(method, "POST");
|
||||
readBody(exchange);
|
||||
HistoricalSimulationRequest request = objectMapper.readValue(
|
||||
readBody(exchange), HistoricalSimulationRequest.class);
|
||||
writeJson(exchange, 200, runSimulation(() -> {
|
||||
HistoricalDispatchSimulationResult result = historicalService.run();
|
||||
return mapper.result(input, result);
|
||||
boolean raw = request.getMode() == SimulationExecutionMode.RAW_SNAPSHOT;
|
||||
HistoricalSimulationInput selectedInput = raw ? rawInput : businessInput;
|
||||
HistoricalDispatchSimulationResult result = raw
|
||||
? rawHistoricalService.run() : businessHistoricalService.run();
|
||||
return mapper.result(selectedInput, result);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ft546.wcs.shadow.history.visualizer;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationExecutionMode;
|
||||
|
||||
/**
|
||||
* 选择历史回放的审计口径。
|
||||
*
|
||||
* <p>业务模式把重复队列快照归并为真实父任务并执行二段式阶段;RAW 模式保留原先
|
||||
* 240 行逐条运行的兼容结果。空请求默认进入业务模式,避免页面误把快照数当成业务任务数。
|
||||
*/
|
||||
public final class HistoricalSimulationRequest {
|
||||
@JsonProperty("mode")
|
||||
private String mode;
|
||||
|
||||
/** 供 Jackson 反序列化请求体使用。 */
|
||||
public HistoricalSimulationRequest() {
|
||||
}
|
||||
|
||||
/** 返回经过校验的回放口径;未传值时默认按生产源码语义回放。 */
|
||||
public SimulationExecutionMode getMode() {
|
||||
String normalized = mode == null || mode.trim().isEmpty()
|
||||
? SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS.name()
|
||||
: mode.trim().toUpperCase();
|
||||
try {
|
||||
return SimulationExecutionMode.valueOf(normalized);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"INVALID_EXECUTION_MODE: mode must be SOURCE_ALIGNED_BUSINESS or RAW_SNAPSHOT",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationConfig;
|
||||
|
||||
/** 重新运行由 240 条历史快照组装出的业务父任务,并保留既定的确定性 PLC 故障注入。 */
|
||||
/** 按调用方选择的不可变输入运行历史回放,并保留既定的确定性 PLC 故障注入。 */
|
||||
public final class HistoricalWorkloadSimulationService {
|
||||
private final HistoricalSimulationInput input;
|
||||
|
||||
@@ -17,6 +17,7 @@ public final class HistoricalWorkloadSimulationService {
|
||||
}
|
||||
|
||||
public HistoricalDispatchSimulationResult run() {
|
||||
// 每种模式都对自身任务序列稳定注入故障,保证业务/RAW 对比可重复而不共享运行状态。
|
||||
DeterministicFaultPlan.Builder faults = DeterministicFaultPlan.builder();
|
||||
for (int i = 0; i < input.getTasks().size(); i += 37) {
|
||||
faults.plcFailure(input.getTasks().get(i).getSimulationTaskId(), 1,
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalTaskSnapshotEvidence;
|
||||
import com.ft546.wcs.shadow.history.simulation.FloorTransferJob;
|
||||
import com.ft546.wcs.shadow.history.simulation.RoutePlanningEvidence;
|
||||
import com.ft546.wcs.shadow.history.simulation.RerouteDecisionEvidence;
|
||||
@@ -16,6 +17,7 @@ import com.ft546.wcs.shadow.history.simulation.SimulationMetrics;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationTaskStatus;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationTickEvent;
|
||||
import com.ft546.wcs.shadow.history.simulation.TaskPhaseEvidence;
|
||||
import com.ft546.wcs.demo.traffic.RouteCandidateEvaluation;
|
||||
import com.ft546.wcs.demo.traffic.RoutePlanDecision;
|
||||
import com.ft546.wcs.demo.traffic.RouteQuality;
|
||||
@@ -121,6 +123,7 @@ public final class SimulationVisualizationMapper {
|
||||
payload.put("plannerVersion", PLANNER_VERSION);
|
||||
payload.put("mapSha256", dataset.getFullMapSha256());
|
||||
payload.put("pathInfoUsedForPlanning", false);
|
||||
payload.put("executionMode", result.getExecutionMode().name());
|
||||
payload.put("summary", summary(result, definitions, input.getGridMap()));
|
||||
payload.put("tasks", tasks);
|
||||
payload.put("events", events);
|
||||
@@ -135,6 +138,13 @@ public final class SimulationVisualizationMapper {
|
||||
GridMap gridMap) {
|
||||
Map<String, Object> value = new LinkedHashMap<String, Object>();
|
||||
value.put("inputCount", result.getTaskResults().size());
|
||||
value.put("rawSnapshotCount", result.getInputAudit().getRawSnapshotCount());
|
||||
value.put("uniqueQueueCount", result.getInputAudit().getUniqueQueueCount());
|
||||
value.put("duplicateSnapshotCount", result.getInputAudit().getDuplicateSnapshotCount());
|
||||
value.put("businessTaskCount", result.getInputAudit().getBusinessTaskCount());
|
||||
value.put("operationalQueueCount", result.getInputAudit().getOperationalQueueCount());
|
||||
value.put("incompleteVehicleReleaseCount",
|
||||
result.getIncompleteVehicleReleaseCount());
|
||||
value.put("acceleration", result.getAcceleration());
|
||||
value.put("completedCount", result.getCompletedCount());
|
||||
value.put("waitingCount", result.getWaitingCount());
|
||||
@@ -199,6 +209,8 @@ public final class SimulationVisualizationMapper {
|
||||
value.put("guid", record.getGuid());
|
||||
value.put("taskType", definition == null
|
||||
? record.getTaskType() : definition.getTaskType().name());
|
||||
value.put("businessType", definition == null
|
||||
? record.getTaskType() : definition.getTaskType().name());
|
||||
value.put("priority", record.getPriority());
|
||||
value.put("businessStartSite", record.getStartSite());
|
||||
value.put("businessEndSite", record.getEndSite());
|
||||
@@ -206,6 +218,10 @@ public final class SimulationVisualizationMapper {
|
||||
? record.getStartSite() : definition.getPhysicalSource());
|
||||
value.put("physicalEndSite", definition == null
|
||||
? record.getEndSite() : definition.getPhysicalTarget());
|
||||
value.put("sourceFrontSite", definition == null
|
||||
? null : definition.getSourceFrontSite());
|
||||
value.put("targetFrontSite", definition == null
|
||||
? null : definition.getTargetFrontSite());
|
||||
value.put("historicalVehicleId", record.getAgvId());
|
||||
value.put("simulatedVehicleId", task.getSimulatedVehicleId());
|
||||
value.put("assignmentMode", task.getAssignmentMode() == null
|
||||
@@ -224,6 +240,13 @@ public final class SimulationVisualizationMapper {
|
||||
value.put("arrivalMillis", task.getArrivalMillis());
|
||||
value.put("firstAssignedMillis", task.getFirstAssignedMillis());
|
||||
value.put("completedMillis", task.getCompletedMillis());
|
||||
value.put("businessCompletedMillis", task.getBusinessCompletedMillis());
|
||||
value.put("vehicleReleasedMillis", task.getVehicleReleasedMillis());
|
||||
value.put("vehicleReleaseStatus", task.getVehicleReleaseStatus().name());
|
||||
value.put("cargoState", task.getCargoState().name());
|
||||
value.put("skippedStageReasons", task.getSkippedStageReasons());
|
||||
value.put("snapshotEvidence", snapshotEvidence(definition));
|
||||
value.put("phaseEvidence", phaseEvidence(task.getPhaseEvidence()));
|
||||
value.put("waitingMillis", task.getWaitingMillis());
|
||||
value.put("simulationDurationMillis", simulatedDuration);
|
||||
value.put("historicalDurationMillis", historicalDuration);
|
||||
@@ -259,6 +282,42 @@ public final class SimulationVisualizationMapper {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 输出父任务关联的原始空车、载货和重复扫描记录,供页面追溯 GUID,而不参与重新规划。 */
|
||||
private List<Map<String, Object>> snapshotEvidence(HistoricalSimulationTask definition) {
|
||||
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
|
||||
if (definition == null) return values;
|
||||
for (HistoricalTaskSnapshotEvidence evidence : definition.getSnapshotEvidence()) {
|
||||
Map<String, Object> value = new LinkedHashMap<String, Object>();
|
||||
value.put("snapshotRow", evidence.getSnapshotRow());
|
||||
value.put("guid", evidence.getGuid());
|
||||
value.put("queueTaskType", evidence.getQueueTaskType());
|
||||
value.put("default02", evidence.getDefault02());
|
||||
value.put("historicalVehicleId", evidence.getHistoricalVehicleId());
|
||||
value.put("historicalPath", historicalPath(evidence.getHistoricalPath()));
|
||||
values.add(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/** 输出仿真器冻结的逐阶段真实路径;前端只选择和绘制,不重新推导业务阶段。 */
|
||||
private List<Map<String, Object>> phaseEvidence(List<TaskPhaseEvidence> evidence) {
|
||||
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
|
||||
for (TaskPhaseEvidence phase : evidence) {
|
||||
Map<String, Object> value = new LinkedHashMap<String, Object>();
|
||||
value.put("phase", phase.getPhase().name());
|
||||
value.put("fromSite", phase.getFromSite());
|
||||
value.put("toSite", phase.getToSite());
|
||||
value.put("plannedPath", phase.getPlannedPath());
|
||||
value.put("startedMillis", phase.getStartedMillis());
|
||||
value.put("completedMillis", phase.getCompletedMillis());
|
||||
value.put("holdCount", phase.getHoldCount());
|
||||
value.put("retryCount", phase.getRetryCount());
|
||||
value.put("completionReason", phase.getCompletionReason());
|
||||
values.add(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总父任务换层作业经过的楼层,供页面楼层选择器标记整条业务链涉及的地图。
|
||||
*/
|
||||
|
||||
@@ -189,6 +189,60 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.history-actions { justify-content: space-between; gap: 16px; }
|
||||
.history-actions > div:first-child { display: grid; gap: 3px; }
|
||||
.history-actions span { color: var(--color-muted); font-size: 12px; }
|
||||
.replay-mode-control {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-width: 220px;
|
||||
padding: 3px;
|
||||
background: #e3e9e7;
|
||||
border: 1px solid var(--color-line-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.replay-mode-control input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.replay-mode-control label {
|
||||
min-height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 10px;
|
||||
color: var(--color-muted);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.replay-mode-control input:checked + label {
|
||||
color: var(--color-ink);
|
||||
background: var(--color-surface);
|
||||
border-color: var(--color-line);
|
||||
box-shadow: 0 1px 3px rgba(22, 33, 31, .1);
|
||||
}
|
||||
.replay-mode-control input:focus-visible + label {
|
||||
outline: 3px solid rgba(20, 125, 157, .22);
|
||||
}
|
||||
.audit-count-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
||||
border: 1px solid var(--color-line);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.audit-count-strip > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 7px 10px;
|
||||
border-right: 1px solid var(--color-line);
|
||||
}
|
||||
.audit-count-strip > div:last-child { border-right: 0; }
|
||||
.audit-count-strip span { color: var(--color-muted); font-size: 10px; }
|
||||
.audit-count-strip strong {
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button-row { gap: 8px; }
|
||||
.kpi-strip { display: grid; grid-template-columns: repeat(6, minmax(110px, 1fr)); gap: 1px; background: var(--color-line); border: 1px solid var(--color-line); }
|
||||
.kpi-item { min-height: 62px; padding: 9px 12px; background: var(--color-surface); display: grid; gap: 2px; }
|
||||
@@ -277,6 +331,7 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.legend-line.traversed { border-color: var(--color-traversed-path); border-top-width: 5px; }
|
||||
.legend-line.reroute-old { border-color: var(--color-red); border-top-style: dashed; }
|
||||
.legend-line.reroute-new { border-color: var(--color-blue); border-top-width: 5px; }
|
||||
.legend-line.business-phase { border-color: #6a4c93; border-top-width: 6px; }
|
||||
.legend-dot { width: 10px; height: 10px; border: 2px solid; border-radius: 50%; }
|
||||
.legend-dot.hold { border-color: var(--color-amber); }
|
||||
.legend-transfer { width: 9px; height: 9px; border: 2px solid var(--color-blue); background: var(--color-blue-soft); transform: rotate(45deg); }
|
||||
@@ -299,6 +354,10 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.route-segment.traversed { stroke: var(--color-traversed-path); stroke-width: 8; }
|
||||
.route-segment.reroute-old { stroke: var(--color-red); stroke-width: 5; stroke-dasharray: 12 8; opacity: .82; }
|
||||
.route-segment.reroute-new { stroke: var(--color-blue); stroke-width: 5; }
|
||||
.route-segment.business-phase { stroke: #6a4c93; stroke-width: 7; }
|
||||
.route-segment.phase-loaded-return-to-source-front { stroke: var(--color-amber); }
|
||||
.route-segment.phase-loaded-to-place { stroke: var(--color-service-path); }
|
||||
.route-segment.phase-empty-return-to-target-front { stroke: var(--color-blue); }
|
||||
.traffic-event-marker { fill: #fff; stroke-width: 4; vector-effect: non-scaling-stroke; }
|
||||
.traffic-event-marker.hold-point { stroke: var(--color-amber); }
|
||||
.traffic-event-marker.reroute-point { stroke: var(--color-red); }
|
||||
@@ -342,7 +401,9 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.evidence-list > div { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #e4eae8; }
|
||||
.evidence-list dt { color: var(--color-muted); font-size: 11px; }
|
||||
.evidence-list dd { margin: 0; text-align: right; font-family: var(--font-mono); font-size: 12px; overflow-wrap: anywhere; }
|
||||
.decision-audit, .reroute-audit, .floor-transfer-panel { border-bottom: 1px solid var(--color-line); }
|
||||
.decision-audit, .reroute-audit, .floor-transfer-panel, .business-evidence-panel {
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
}
|
||||
.floor-transfer-list { display: grid; }
|
||||
.floor-transfer-job { display: grid; gap: 9px; padding: 11px 14px; border-top: 1px solid #e4eae8; }
|
||||
.floor-transfer-job-route { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; }
|
||||
@@ -356,6 +417,46 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.floor-transfer-job-key { display: grid; grid-template-columns: 48px minmax(0, 1fr); gap: 8px; padding-top: 7px; border-top: 1px dashed var(--color-line); }
|
||||
.floor-transfer-job-key span { color: var(--color-muted); font-size: 9px; }
|
||||
.floor-transfer-job-key code { font-family: var(--font-mono); font-size: 9px; line-height: 1.45; overflow-wrap: anywhere; }
|
||||
.business-evidence-grid {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.business-evidence-grid > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e4eae8;
|
||||
}
|
||||
.business-evidence-grid > div:nth-child(odd) { border-right: 1px solid #e4eae8; }
|
||||
.business-evidence-grid > div.span-all { grid-column: 1 / -1; border-right: 0; }
|
||||
.business-evidence-grid dt { color: var(--color-muted); font-size: 10px; }
|
||||
.business-evidence-grid dd {
|
||||
margin: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.phase-list { display: grid; gap: 1px; background: var(--color-line); }
|
||||
.phase-item {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(112px, .75fr) minmax(0, 1.25fr);
|
||||
gap: 3px 10px;
|
||||
padding: 9px 12px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-surface);
|
||||
border: 0;
|
||||
border-left: 4px solid transparent;
|
||||
text-align: left;
|
||||
}
|
||||
.phase-item:hover { background: var(--color-surface-muted); }
|
||||
.phase-item.is-active { background: #f0ebf6; border-left-color: #6a4c93; }
|
||||
.phase-item-name { font-size: 11px; font-weight: 750; }
|
||||
.phase-item-route { min-width: 0; font-family: var(--font-mono); font-size: 10px; overflow-wrap: anywhere; }
|
||||
.phase-item-meta { grid-column: 1 / -1; color: var(--color-muted); font-size: 9px; }
|
||||
.inspector-section-bar { min-height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 14px; background: var(--color-surface-muted); }
|
||||
.comparison-status { max-width: 240px; gap: 5px; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.evidence-badge { min-height: 23px; display: inline-flex; align-items: center; padding: 2px 7px; border: 1px solid var(--color-line); border-radius: 999px; font-size: 10px; font-weight: 700; white-space: nowrap; }
|
||||
@@ -437,6 +538,9 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.field-fault { grid-column: span 2; }
|
||||
.point-form .primary-button { grid-column: span 1; }
|
||||
.kpi-strip { grid-template-columns: repeat(3, 1fr); }
|
||||
.audit-count-strip { grid-template-columns: repeat(3, 1fr); }
|
||||
.audit-count-strip > div:nth-child(3) { border-right: 0; }
|
||||
.audit-count-strip > div:nth-child(-n+3) { border-bottom: 1px solid var(--color-line); }
|
||||
.audit-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@@ -469,9 +573,15 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.field-site { grid-column: 1; }
|
||||
#swap-sites { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
|
||||
.field:not(.field-site), .field-fault, .point-form .primary-button { grid-column: 1 / -1; }
|
||||
.history-actions { align-items: flex-start; }
|
||||
.history-actions { align-items: flex-start; flex-wrap: wrap; }
|
||||
.history-actions > div:first-child { flex: 1 1 120px; }
|
||||
.replay-mode-control { order: 3; width: 100%; min-width: 0; }
|
||||
.button-row { flex-wrap: wrap; justify-content: flex-end; }
|
||||
.kpi-strip { grid-template-columns: repeat(2, 1fr); }
|
||||
.audit-count-strip { grid-template-columns: repeat(2, 1fr); }
|
||||
.audit-count-strip > div { border-right: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); }
|
||||
.audit-count-strip > div:nth-child(even) { border-right: 0; }
|
||||
.audit-count-strip > div:nth-last-child(-n+2) { border-bottom: 0; }
|
||||
.section-bar { align-items: flex-start; padding: 10px 12px; }
|
||||
.route-tools { display: grid; grid-template-columns: auto auto; gap: 5px 7px; justify-items: end; }
|
||||
.view-mode { grid-column: 1; }
|
||||
@@ -484,6 +594,8 @@ h3 { font-size: 14px; line-height: 1.3; }
|
||||
.floor-transfer-job-route { grid-template-columns: minmax(0, 1fr) 20px minmax(0, 1fr); gap: 5px; }
|
||||
.floor-transfer-port { grid-template-columns: 1fr; }
|
||||
.floor-transfer-port small { grid-column: 1; }
|
||||
.phase-item { grid-template-columns: 1fr; }
|
||||
.phase-item-meta { grid-column: 1; }
|
||||
.comparison-status { max-width: 190px; }
|
||||
.decision-summary { grid-template-columns: 1fr; }
|
||||
.decision-summary > div, .decision-summary > div:nth-child(odd) { border-right: 0; }
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
pointResult: null,
|
||||
historyResult: null,
|
||||
mode: "point",
|
||||
historicalMode: "SOURCE_ALIGNED_BUSINESS",
|
||||
selectedTaskId: null,
|
||||
selectedPhaseIndex: 0,
|
||||
selectedPlanningStage: "SERVICE",
|
||||
selectedPathVersion: null,
|
||||
selectedRerouteIndex: null,
|
||||
@@ -37,8 +39,11 @@
|
||||
"point-controls", "history-controls", "point-form", "source-site", "target-site",
|
||||
"site-options", "task-type", "priority", "acceleration", "fault-preset",
|
||||
"swap-sites", "run-point", "run-history", "history-run-state", "history-kpis",
|
||||
"simulation-mode-business", "simulation-mode-raw", "raw-snapshot-count",
|
||||
"unique-queue-count", "duplicate-snapshot-count", "business-task-count",
|
||||
"operational-queue-count", "replay-mode-readout",
|
||||
"task-search", "task-status-filter", "task-filter-count", "history-task-body",
|
||||
"task-workspace", "route-map", "route-empty", "loading-state", "loading-label",
|
||||
"task-workspace", "route-title", "route-map", "route-empty", "loading-state", "loading-label",
|
||||
"floor-select", "show-historical", "map-fit", "path-fit", "zoom-in", "zoom-out",
|
||||
"reset-view", "show-initial-route", "show-current-route", "show-move-trace",
|
||||
"show-hold-points", "inspector-task-id", "task-status",
|
||||
@@ -48,6 +53,10 @@
|
||||
"evidence-hold-retry", "evidence-plc", "route-difference",
|
||||
"invalid-history-segments", "planned-path-output", "reason-output",
|
||||
"floor-transfer-panel", "floor-transfer-count", "floor-transfer-list",
|
||||
"business-evidence-panel", "cargo-state", "source-front-site",
|
||||
"target-front-site", "business-completed-at", "vehicle-released-at",
|
||||
"snapshot-guid-list", "phase-list", "current-business-phase",
|
||||
"vehicle-release-status",
|
||||
"route-comparability", "route-quality-comparison", "route-stage-select",
|
||||
"route-version-select", "decision-shortest-distance",
|
||||
"decision-selected-distance", "decision-detour-ratio", "decision-candidate-count",
|
||||
@@ -68,6 +77,17 @@
|
||||
elements["point-form"].addEventListener("submit", runPointSimulation);
|
||||
elements["swap-sites"].addEventListener("click", swapSites);
|
||||
elements["run-history"].addEventListener("click", runHistoricalSimulation);
|
||||
document.querySelectorAll("input[name='simulation-mode']").forEach(function (input) {
|
||||
input.addEventListener("change", function () {
|
||||
if (!input.checked) return;
|
||||
state.historicalMode = input.value;
|
||||
state.selectedPhaseIndex = 0;
|
||||
updateReplayModePresentation();
|
||||
if (state.mode === "history" && state.historyResult && !state.busy) {
|
||||
runHistoricalSimulation();
|
||||
}
|
||||
});
|
||||
});
|
||||
elements["task-search"].addEventListener("input", renderHistoryTable);
|
||||
elements["task-status-filter"].addEventListener("change", renderHistoryTable);
|
||||
elements["floor-select"].addEventListener("change", function () {
|
||||
@@ -198,12 +218,13 @@
|
||||
}
|
||||
|
||||
async function runHistoricalSimulation() {
|
||||
setBusy(true, "正在重新运行 240 条历史任务");
|
||||
var businessMode = state.historicalMode === "SOURCE_ALIGNED_BUSINESS";
|
||||
setBusy(true, businessMode ? "正在运行 76 个业务父任务" : "正在运行 240 条 RAW 快照");
|
||||
elements["history-run-state"].textContent = "运行中";
|
||||
try {
|
||||
state.historyResult = await api("/api/simulations/historical", {
|
||||
method: "POST",
|
||||
body: "{}"
|
||||
body: JSON.stringify({ mode: state.historicalMode })
|
||||
});
|
||||
elements["history-run-state"].textContent = "最近运行:" + new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
renderHistorySummary();
|
||||
@@ -211,7 +232,8 @@
|
||||
selectResult(state.historyResult, state.historyResult.tasks[0].simulationTaskId);
|
||||
elements["export-result"].disabled = false;
|
||||
elements["export-safety"].disabled = false;
|
||||
showToast("240 条历史任务仿真完成");
|
||||
updateReplayModePresentation();
|
||||
showToast(state.historyResult.summary.completedCount + " 条任务回放完成");
|
||||
} catch (error) {
|
||||
elements["history-run-state"].textContent = "运行失败";
|
||||
handleRunError(error);
|
||||
@@ -243,11 +265,28 @@
|
||||
} else if (mode === "safety") {
|
||||
renderSafety();
|
||||
}
|
||||
updateReplayModePresentation();
|
||||
}
|
||||
|
||||
/** 同步回放口径、按钮状态和地图图层含义,避免业务阶段与 RAW 路径差异混为一谈。 */
|
||||
function updateReplayModePresentation() {
|
||||
var business = state.historicalMode === "SOURCE_ALIGNED_BUSINESS";
|
||||
elements["simulation-mode-business"].checked = business;
|
||||
elements["simulation-mode-raw"].checked = !business;
|
||||
if (!state.historyResult) {
|
||||
setText("replay-mode-readout", business ? "业务任务" : "RAW 快照");
|
||||
}
|
||||
var businessHistory = state.mode === "history" && business;
|
||||
elements["show-historical"].disabled = businessHistory;
|
||||
elements["show-initial-route"].disabled = businessHistory;
|
||||
elements["route-title"].textContent = businessHistory
|
||||
? "业务阶段规划路径" : "算法规划路径";
|
||||
}
|
||||
|
||||
function selectResult(result, taskId) {
|
||||
state.selectedTaskId = taskId;
|
||||
state.tickIndex = 0;
|
||||
state.selectedPhaseIndex = 0;
|
||||
state.selectedPlanningStage = "SERVICE";
|
||||
state.selectedPathVersion = null;
|
||||
state.selectedRerouteIndex = null;
|
||||
@@ -322,11 +361,14 @@
|
||||
setEvidenceBadge("route-quality-comparison", task.routeQualityComparison,
|
||||
qualityCategoryLabel(task.routeQualityComparison));
|
||||
renderFloorTransferJobs(task);
|
||||
renderBusinessEvidence(task);
|
||||
elements["route-stage-select"].value = state.selectedPlanningStage;
|
||||
renderRouteDecision(task);
|
||||
renderRerouteComparison(task);
|
||||
elements["planned-path-output"].textContent = task.plannedPath.length
|
||||
? task.plannedPath.join(" → ") : "无规划路径";
|
||||
var displayedPath = isBusinessHistoryMode()
|
||||
? selectedPhasePath(task) : (task.plannedPath || []);
|
||||
elements["planned-path-output"].textContent = displayedPath.length
|
||||
? displayedPath.join(" → ") : "无规划路径";
|
||||
elements["reason-output"].textContent = task.reason || task.status;
|
||||
}
|
||||
|
||||
@@ -413,6 +455,96 @@
|
||||
return "已提交";
|
||||
}
|
||||
|
||||
/** 展示后端冻结的业务状态和阶段路线;页面不根据站点命名自行推断通道阶段。 */
|
||||
function renderBusinessEvidence(task) {
|
||||
var phases = (task && task.phaseEvidence) || [];
|
||||
var panel = elements["business-evidence-panel"];
|
||||
panel.hidden = phases.length === 0;
|
||||
if (!phases.length) {
|
||||
elements["phase-list"].replaceChildren();
|
||||
return;
|
||||
}
|
||||
if (state.selectedPhaseIndex >= phases.length) state.selectedPhaseIndex = 0;
|
||||
setEvidenceBadge("cargo-state", task.cargoState,
|
||||
cargoStateLabel(task.cargoState));
|
||||
setText("current-business-phase", phaseLabel(phases[state.selectedPhaseIndex].phase));
|
||||
setText("vehicle-release-status", vehicleReleaseLabel(task.vehicleReleaseStatus));
|
||||
setText("source-front-site", task.sourceFrontSite || "无通道阶段");
|
||||
setText("target-front-site", task.targetFrontSite || "无通道阶段");
|
||||
setText("business-completed-at", formatMillis(task.businessCompletedMillis));
|
||||
setText("vehicle-released-at", formatMillis(task.vehicleReleasedMillis)
|
||||
+ " · " + vehicleReleaseLabel(task.vehicleReleaseStatus));
|
||||
var guids = unique((task.snapshotEvidence || []).map(function (item) {
|
||||
return item.guid;
|
||||
}));
|
||||
setText("snapshot-guid-list", guids.length ? guids.join(" / ") : "无历史快照");
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
phases.forEach(function (phase, index) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "phase-item";
|
||||
button.dataset.phase = phase.phase;
|
||||
button.dataset.phaseIndex = String(index);
|
||||
button.classList.toggle("is-active", index === state.selectedPhaseIndex);
|
||||
var heading = textNode("span", phaseLabel(phase.phase), "phase-item-name");
|
||||
var endpoints = textNode("code", phase.fromSite + " → " + phase.toSite,
|
||||
"phase-item-route");
|
||||
var metrics = textNode("small", phase.plannedPath.length + " 点 · HOLD "
|
||||
+ phase.holdCount + " · 重试 " + phase.retryCount, "phase-item-meta");
|
||||
button.append(heading, endpoints, metrics);
|
||||
button.addEventListener("click", function () {
|
||||
state.selectedPhaseIndex = index;
|
||||
var start = siteById(phase.fromSite);
|
||||
if (start) state.selectedFloor = start.floor;
|
||||
renderBusinessEvidence(task);
|
||||
elements["planned-path-output"].textContent = phase.plannedPath.join(" → ");
|
||||
populateFloors();
|
||||
renderRoute();
|
||||
fitCurrentViewMode();
|
||||
});
|
||||
fragment.appendChild(button);
|
||||
});
|
||||
elements["phase-list"].replaceChildren(fragment);
|
||||
}
|
||||
|
||||
function selectedPhase(task) {
|
||||
var phases = (task && task.phaseEvidence) || [];
|
||||
return phases[state.selectedPhaseIndex] || null;
|
||||
}
|
||||
|
||||
function selectedPhasePath(task) {
|
||||
var phase = selectedPhase(task);
|
||||
return phase ? (phase.plannedPath || []) : [];
|
||||
}
|
||||
|
||||
function isBusinessHistoryMode() {
|
||||
var result = currentResult();
|
||||
return state.mode === "history" && result
|
||||
&& result.executionMode === "SOURCE_ALIGNED_BUSINESS";
|
||||
}
|
||||
|
||||
function cargoStateLabel(value) {
|
||||
return value === "DEPOSITED" ? "货物已入位"
|
||||
: value === "LOADED" ? "货物在车" : "车辆空载";
|
||||
}
|
||||
|
||||
function vehicleReleaseLabel(value) {
|
||||
return value === "COMPLETED" ? "车辆已释放"
|
||||
: value === "PENDING" ? "车辆待释放"
|
||||
: value === "FAILED" ? "车辆释放失败" : "无需释放";
|
||||
}
|
||||
|
||||
function phaseLabel(value) {
|
||||
var labels = {
|
||||
EMPTY_TO_PICKUP: "空车前往取货",
|
||||
LOADED_RETURN_TO_SOURCE_FRONT: "载货返回源通道前端",
|
||||
LOADED_TO_PLACE: "载货前往放货",
|
||||
EMPTY_RETURN_TO_TARGET_FRONT: "空车返回目标通道前端"
|
||||
};
|
||||
return labels[value] || value;
|
||||
}
|
||||
|
||||
function textNode(tag, value, className) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
@@ -572,6 +704,9 @@
|
||||
var vehicleFloor = null;
|
||||
if (task) {
|
||||
var ids = [].concat(task.plannedPath || [], task.historicalPath || []);
|
||||
(task.phaseEvidence || []).forEach(function (phase) {
|
||||
ids = ids.concat(phase.plannedPath || []);
|
||||
});
|
||||
var events = eventsForTask(currentResult(), task.simulationTaskId);
|
||||
events.forEach(function (event) {
|
||||
if (event.fromSite) ids.push(event.fromSite);
|
||||
@@ -633,9 +768,12 @@
|
||||
var approach = approachPath(task, moves);
|
||||
var historical = elements["show-historical"].checked ? task.historicalPath : [];
|
||||
var initialService = initialServicePath(task);
|
||||
var businessPhase = isBusinessHistoryMode() ? selectedPhase(task) : null;
|
||||
var businessPhasePath = businessPhase ? businessPhase.plannedPath : [];
|
||||
var rerouteDecision = selectedRerouteDecision(task);
|
||||
var currentSite = currentVehicleSite(task, traversedMoves);
|
||||
var allIds = unique([].concat(task.plannedPath, historical, approach, initialService,
|
||||
businessPhasePath,
|
||||
rerouteDecision ? rerouteDecision.oldRemainingPath : [],
|
||||
rerouteDecision ? rerouteDecision.newRemainingPath : [],
|
||||
moves.reduce(function (values, event) {
|
||||
@@ -653,16 +791,23 @@
|
||||
}
|
||||
hideMapHint();
|
||||
|
||||
if (elements["show-current-route"].checked) {
|
||||
drawPath(layer, approach, "approach", project);
|
||||
}
|
||||
if (elements["show-initial-route"].checked) {
|
||||
drawPath(layer, initialService, "initial-service", project);
|
||||
}
|
||||
drawRouteComparison(layer, task, project);
|
||||
if (rerouteDecision) {
|
||||
drawPath(layer, rerouteDecision.oldRemainingPath, "reroute-old", project);
|
||||
drawPath(layer, rerouteDecision.newRemainingPath, "reroute-new", project);
|
||||
if (businessPhase) {
|
||||
if (elements["show-current-route"].checked) {
|
||||
drawPath(layer, businessPhasePath,
|
||||
"business-phase phase-" + businessPhase.phase.toLowerCase(), project);
|
||||
}
|
||||
} else {
|
||||
if (elements["show-current-route"].checked) {
|
||||
drawPath(layer, approach, "approach", project);
|
||||
}
|
||||
if (elements["show-initial-route"].checked) {
|
||||
drawPath(layer, initialService, "initial-service", project);
|
||||
}
|
||||
drawRouteComparison(layer, task, project);
|
||||
if (rerouteDecision) {
|
||||
drawPath(layer, rerouteDecision.oldRemainingPath, "reroute-old", project);
|
||||
drawPath(layer, rerouteDecision.newRemainingPath, "reroute-new", project);
|
||||
}
|
||||
}
|
||||
if (elements["show-move-trace"].checked) {
|
||||
drawMoveSegments(layer, traversedMoves, "traversed", project);
|
||||
@@ -681,16 +826,18 @@
|
||||
var holdSite = elements["show-hold-points"].checked
|
||||
&& lastEvent && isHold(lastEvent.eventType)
|
||||
? (lastEvent.toSite || lastEvent.fromSite) : null;
|
||||
var comparisonNodes = comparisonNodeIds(task,
|
||||
elements["show-historical"].checked);
|
||||
var comparisonNodes = businessPhase ? new Set(businessPhasePath)
|
||||
: comparisonNodeIds(task, elements["show-historical"].checked);
|
||||
var displayStart = businessPhase ? businessPhase.fromSite : task.physicalStartSite;
|
||||
var displayTarget = businessPhase ? businessPhase.toSite : task.physicalEndSite;
|
||||
sites.forEach(function (site) {
|
||||
drawNode(layer, site, project, {
|
||||
passed: passedIds.has(site.siteId),
|
||||
start: site.siteId === task.physicalStartSite,
|
||||
target: site.siteId === task.physicalEndSite,
|
||||
start: site.siteId === displayStart,
|
||||
target: site.siteId === displayTarget,
|
||||
hold: site.siteId === holdSite,
|
||||
label: comparisonNodes.has(site.siteId) || site.siteId === currentSite
|
||||
|| site.siteId === task.physicalStartSite || site.siteId === task.physicalEndSite
|
||||
|| site.siteId === displayStart || site.siteId === displayTarget
|
||||
});
|
||||
});
|
||||
var vehicleSite = siteById(currentSite);
|
||||
@@ -992,6 +1139,7 @@
|
||||
if (!task) return [];
|
||||
var reroute = selectedRerouteDecision(task);
|
||||
var ids = [].concat(task.plannedPath || [], task.historicalPath || [],
|
||||
isBusinessHistoryMode() ? selectedPhasePath(task) : [],
|
||||
initialServicePath(task), reroute ? reroute.oldRemainingPath : [],
|
||||
reroute ? reroute.newRemainingPath : [],
|
||||
[task.physicalStartSite, task.physicalEndSite]);
|
||||
@@ -1255,13 +1403,21 @@
|
||||
function renderHistorySummary() {
|
||||
if (!state.historyResult) return;
|
||||
var summary = state.historyResult.summary;
|
||||
setText("raw-snapshot-count", summary.rawSnapshotCount);
|
||||
setText("unique-queue-count", summary.uniqueQueueCount);
|
||||
setText("duplicate-snapshot-count", summary.duplicateSnapshotCount);
|
||||
setText("business-task-count", summary.businessTaskCount);
|
||||
setText("operational-queue-count", summary.operationalQueueCount);
|
||||
setText("replay-mode-readout", state.historyResult.executionMode === "RAW_SNAPSHOT"
|
||||
? "RAW 快照" : "业务任务");
|
||||
var values = [
|
||||
["完成", summary.completedCount, summary.completedCount === summary.inputCount],
|
||||
["等待", summary.waitingCount, summary.waitingCount === 0],
|
||||
["失败", summary.failedCount, summary.failedCount === 0],
|
||||
["重新选车", summary.reassignmentCount, true],
|
||||
["路径变化", summary.replannedPathDifferenceCount, true],
|
||||
["总 tick", summary.tickCount, true]
|
||||
["车辆未释放", summary.incompleteVehicleReleaseCount,
|
||||
summary.incompleteVehicleReleaseCount === 0]
|
||||
];
|
||||
var fragment = document.createDocumentFragment();
|
||||
values.forEach(function (entry) {
|
||||
@@ -1310,7 +1466,9 @@
|
||||
appendCell(row, task.historicalVehicleId || "--", true);
|
||||
appendCell(row, task.simulatedVehicleId || "--", true);
|
||||
appendCell(row, historyAssignmentMode(task));
|
||||
appendCell(row, task.pathChanged ? "已重规划" : "一致");
|
||||
appendCell(row, state.historyResult.executionMode === "SOURCE_ALIGNED_BUSINESS"
|
||||
? (task.phaseEvidence || []).length + " 个阶段"
|
||||
: (task.pathChanged ? "已重规划" : "一致"));
|
||||
appendCell(row, task.status);
|
||||
appendCell(row, formatDuration(task.waitingMillis), true);
|
||||
appendCell(row, formatDuration(task.simulationDurationMillis), true);
|
||||
@@ -1504,6 +1662,13 @@
|
||||
elements["floor-transfer-panel"].hidden = true;
|
||||
elements["floor-transfer-list"].replaceChildren();
|
||||
setText("floor-transfer-count", "0 个");
|
||||
elements["business-evidence-panel"].hidden = true;
|
||||
elements["phase-list"].replaceChildren();
|
||||
["cargo-state", "source-front-site", "target-front-site",
|
||||
"business-completed-at", "vehicle-released-at", "snapshot-guid-list"]
|
||||
.forEach(function (id) { setText(id, "--"); });
|
||||
setText("current-business-phase", "--");
|
||||
setText("vehicle-release-status", "--");
|
||||
setEvidenceBadge("route-comparability", null, "--");
|
||||
setEvidenceBadge("route-quality-comparison", null, "--");
|
||||
elements["route-version-select"].replaceChildren();
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<nav id="mode-tabs" class="mode-tabs" aria-label="审计模式">
|
||||
<button type="button" class="mode-tab is-active" data-mode="point" aria-selected="true">A → B 单任务</button>
|
||||
<button type="button" class="mode-tab" data-mode="history" aria-selected="false">240 条历史任务</button>
|
||||
<button type="button" class="mode-tab" data-mode="history" aria-selected="false">历史任务回放</button>
|
||||
<button type="button" class="mode-tab" data-mode="safety" aria-selected="false">安全审计</button>
|
||||
</nav>
|
||||
|
||||
@@ -82,11 +82,25 @@
|
||||
<strong>真实历史负载</strong>
|
||||
<span id="history-run-state">尚未运行</span>
|
||||
</div>
|
||||
<div class="replay-mode-control" role="radiogroup" aria-label="历史回放口径">
|
||||
<input id="simulation-mode-business" name="simulation-mode" type="radio" value="SOURCE_ALIGNED_BUSINESS" checked>
|
||||
<label for="simulation-mode-business">业务任务</label>
|
||||
<input id="simulation-mode-raw" name="simulation-mode" type="radio" value="RAW_SNAPSHOT">
|
||||
<label for="simulation-mode-raw">RAW 快照</label>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="export-result" type="button" class="secondary-button" disabled>导出证据</button>
|
||||
<button id="run-history" type="button" class="primary-button"><span aria-hidden="true">▶</span>运行 240 条</button>
|
||||
<button id="run-history" type="button" class="primary-button"><span aria-hidden="true">▶</span>运行回放</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audit-count-strip" aria-label="历史输入审计口径">
|
||||
<div><span>原始快照</span><strong id="raw-snapshot-count">--</strong></div>
|
||||
<div><span>唯一队列</span><strong id="unique-queue-count">--</strong></div>
|
||||
<div><span>重复快照</span><strong id="duplicate-snapshot-count">--</strong></div>
|
||||
<div><span>业务父任务</span><strong id="business-task-count">--</strong></div>
|
||||
<div><span>运行作业</span><strong id="operational-queue-count">--</strong></div>
|
||||
<div><span>当前口径</span><strong id="replay-mode-readout">业务任务</strong></div>
|
||||
</div>
|
||||
<div id="history-kpis" class="kpi-strip"></div>
|
||||
<div class="table-toolbar">
|
||||
<label class="search-field"><span class="sr-only">搜索任务</span><input id="task-search" type="search" placeholder="任务号 / 站点 / 车辆"></label>
|
||||
@@ -143,6 +157,7 @@
|
||||
<span><i class="legend-line traversed"></i>已通过</span>
|
||||
<span><i class="legend-line reroute-old"></i>换路前</span>
|
||||
<span><i class="legend-line reroute-new"></i>换路后</span>
|
||||
<span><i class="legend-line business-phase"></i>业务阶段</span>
|
||||
<span><i class="legend-dot hold"></i>HOLD</span>
|
||||
<span><i class="legend-transfer"></i>换层端口</span>
|
||||
</div>
|
||||
@@ -194,6 +209,22 @@
|
||||
</div>
|
||||
<div id="floor-transfer-list" class="floor-transfer-list"></div>
|
||||
</section>
|
||||
<section id="business-evidence-panel" class="business-evidence-panel" aria-labelledby="business-evidence-title" hidden>
|
||||
<div class="inspector-section-bar">
|
||||
<div><span class="section-kicker">父任务执行链</span><h3 id="business-evidence-title">业务阶段路径</h3></div>
|
||||
<span id="cargo-state" class="evidence-badge is-neutral">--</span>
|
||||
</div>
|
||||
<dl class="business-evidence-grid">
|
||||
<div><dt>当前查看阶段</dt><dd id="current-business-phase">--</dd></div>
|
||||
<div><dt>车辆释放状态</dt><dd id="vehicle-release-status">--</dd></div>
|
||||
<div><dt>源通道前端</dt><dd id="source-front-site">--</dd></div>
|
||||
<div><dt>目标通道前端</dt><dd id="target-front-site">--</dd></div>
|
||||
<div><dt>业务完成</dt><dd id="business-completed-at">--</dd></div>
|
||||
<div><dt>车辆释放</dt><dd id="vehicle-released-at">--</dd></div>
|
||||
<div class="span-all"><dt>原始 GUID</dt><dd id="snapshot-guid-list">--</dd></div>
|
||||
</dl>
|
||||
<div id="phase-list" class="phase-list" aria-label="选择业务阶段路径"></div>
|
||||
</section>
|
||||
<section class="decision-audit" aria-labelledby="decision-audit-title">
|
||||
<div class="inspector-section-bar">
|
||||
<div>
|
||||
|
||||
@@ -30,6 +30,7 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static HistoricalQueueDataset dataset;
|
||||
private static HistoricalSimulationInput input;
|
||||
private static HistoricalSimulationInput rawInput;
|
||||
private static HistoricalSimulationTask sampleTask;
|
||||
|
||||
private HistoricalDispatchVisualizerServer server;
|
||||
@@ -39,6 +40,7 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
dataset = new HistoricalQueueLoader().load(
|
||||
HistoricalDispatchVisualizerServerTest.class.getClassLoader(), "shadow/history");
|
||||
input = new HistoricalSimulationInputFactory().create(dataset);
|
||||
rawInput = new HistoricalSimulationInputFactory().createRawSnapshots(dataset);
|
||||
sampleTask = input.getTasks().stream()
|
||||
.filter(task -> !task.getPhysicalSource().equals(task.getPhysicalTarget()))
|
||||
.findFirst()
|
||||
@@ -47,7 +49,7 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
|
||||
@BeforeEach
|
||||
void startServer() throws IOException {
|
||||
server = new HistoricalDispatchVisualizerServer(0, dataset, input);
|
||||
server = new HistoricalDispatchVisualizerServer(0, dataset, input, rawInput);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@@ -83,20 +85,52 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void historicalEndpointRunsAllCurrentTasks() throws Exception {
|
||||
HttpResult response = request("POST", "/api/simulations/historical", "{}");
|
||||
void historicalBusinessEndpointReturnsDualCountsAndPhaseEvidence() throws Exception {
|
||||
HttpResult response = request("POST", "/api/simulations/historical",
|
||||
"{\"mode\":\"SOURCE_ALIGNED_BUSINESS\"}");
|
||||
Map<String, Object> summary = map(map(response.body).get("summary"));
|
||||
Map<String, Object> task = map(list(map(response.body).get("tasks")).get(0));
|
||||
|
||||
assertEquals(200, response.status);
|
||||
// 历史接口默认运行去重组装后的业务父任务;240 条原始快照由输入审计单独展示。
|
||||
assertEquals(input.getTasks().size(), summary.get("inputCount"));
|
||||
assertEquals(input.getTasks().size(), summary.get("completedCount"));
|
||||
assertEquals("SOURCE_ALIGNED_BUSINESS", map(response.body).get("executionMode"));
|
||||
assertEquals(240, summary.get("rawSnapshotCount"));
|
||||
assertEquals(98, summary.get("uniqueQueueCount"));
|
||||
assertEquals(142, summary.get("duplicateSnapshotCount"));
|
||||
assertEquals(76, summary.get("businessTaskCount"));
|
||||
assertEquals(17, summary.get("operationalQueueCount"));
|
||||
assertEquals(76, summary.get("inputCount"));
|
||||
assertEquals(76, summary.get("completedCount"));
|
||||
assertEquals(0, summary.get("waitingCount"));
|
||||
assertEquals(0, summary.get("failedCount"));
|
||||
assertEquals(0, summary.get("incompleteVehicleReleaseCount"));
|
||||
assertFalse(list(task.get("phaseEvidence")).isEmpty());
|
||||
assertFalse(list(task.get("snapshotEvidence")).isEmpty());
|
||||
assertTrue(task.containsKey("businessType"));
|
||||
assertTrue(task.containsKey("cargoState"));
|
||||
assertTrue(task.containsKey("sourceFrontSite"));
|
||||
assertTrue(task.containsKey("targetFrontSite"));
|
||||
assertTrue(task.containsKey("businessCompletedMillis"));
|
||||
assertTrue(task.containsKey("vehicleReleasedMillis"));
|
||||
assertEquals("COMPLETED", task.get("vehicleReleaseStatus"));
|
||||
assertEquals(Boolean.FALSE, map(response.body).get("pathInfoUsedForPlanning"));
|
||||
assertEquals(0, map(map(response.body).get("invariants")).get("totalSafetyViolations"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rawModeStillReturns240CompatibilityResultsAndEmptyBodyDefaultsToBusiness()
|
||||
throws Exception {
|
||||
HttpResult raw = request("POST", "/api/simulations/historical",
|
||||
"{\"mode\":\"RAW_SNAPSHOT\"}");
|
||||
HttpResult defaultMode = request("POST", "/api/simulations/historical", "{}");
|
||||
|
||||
assertEquals(200, raw.status);
|
||||
assertEquals("RAW_SNAPSHOT", map(raw.body).get("executionMode"));
|
||||
assertEquals(240, map(map(raw.body).get("summary")).get("completedCount"));
|
||||
assertEquals(200, defaultMode.status);
|
||||
assertEquals("SOURCE_ALIGNED_BUSINESS", map(defaultMode.body).get("executionMode"));
|
||||
assertEquals(76, map(map(defaultMode.body).get("summary")).get("completedCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void servesTheVisualizerShellAndAssets() throws Exception {
|
||||
HttpResult page = request("GET", "/", null);
|
||||
@@ -129,6 +163,12 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
assertTrue(page.body.contains("id=\"route-candidate-body\""));
|
||||
assertTrue(page.body.contains("id=\"reroute-select\""));
|
||||
assertTrue(page.body.contains("id=\"reroute-comparison\""));
|
||||
assertTrue(page.body.contains("id=\"simulation-mode-business\""));
|
||||
assertTrue(page.body.contains("id=\"simulation-mode-raw\""));
|
||||
assertTrue(page.body.contains("id=\"raw-snapshot-count\""));
|
||||
assertTrue(page.body.contains("id=\"business-task-count\""));
|
||||
assertTrue(page.body.contains("id=\"phase-list\""));
|
||||
assertTrue(page.body.contains("id=\"business-completed-at\""));
|
||||
assertEquals(200, stylesheet.status);
|
||||
assertEquals("text/css; charset=utf-8", stylesheet.contentType);
|
||||
assertTrue(stylesheet.body.contains("--color-service-path"));
|
||||
@@ -145,6 +185,9 @@ class HistoricalDispatchVisualizerServerTest {
|
||||
assertTrue(script.body.contains("approachPlanningEvidence"));
|
||||
assertTrue(script.body.contains("servicePlanningEvidence"));
|
||||
assertTrue(script.body.contains("rerouteEvents"));
|
||||
assertTrue(script.body.contains("SOURCE_ALIGNED_BUSINESS"));
|
||||
assertTrue(script.body.contains("RAW_SNAPSHOT"));
|
||||
assertTrue(script.body.contains("phaseEvidence"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -87,6 +87,42 @@ class PointToPointSimulationServiceTest {
|
||||
.anyMatch(event -> "PLC_RETRY".equals(event.getType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundChannelTaskExposesPhaseEvidenceAndCompletedVehicleRelease() {
|
||||
HistoricalSimulationTask channelTask = baseInput.getTasks().stream()
|
||||
.filter(task -> task.getTargetFrontSite() != null)
|
||||
.filter(task -> !task.getPhysicalSource().equals(task.getPhysicalTarget()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("an inbound channel task is required"));
|
||||
PointToPointSimulationRequest request = PointToPointSimulationRequest.valid(
|
||||
channelTask.getPhysicalSource(), channelTask.getPhysicalTarget(),
|
||||
"INBOUND", 1, 100, null, Collections.emptyList(), "NONE");
|
||||
|
||||
SimulationTaskResult task = service().run(request).getTaskResults().get(0);
|
||||
|
||||
assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus());
|
||||
assertFalse(task.getPhaseEvidence().isEmpty());
|
||||
assertTrue(task.getPhaseEvidence().stream().anyMatch(phase ->
|
||||
"EMPTY_RETURN_TO_TARGET_FRONT".equals(phase.getPhase().name())));
|
||||
assertEquals("COMPLETED", task.getVehicleReleaseStatus().name());
|
||||
assertTrue(task.getVehicleReleasedMillis() >= task.getBusinessCompletedMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinarySitesWithoutCargoGroupingRemainNormalPickupDelivery() {
|
||||
String[] endpoints = ordinaryAdjacentEndpoints();
|
||||
PointToPointSimulationRequest request = PointToPointSimulationRequest.valid(
|
||||
endpoints[0], endpoints[1], "INBOUND", 1, 100,
|
||||
null, Collections.emptyList(), "NONE");
|
||||
|
||||
SimulationTaskResult task = service().run(request).getTaskResults().get(0);
|
||||
|
||||
assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus());
|
||||
assertEquals(Collections.singletonList("NO_CHANNEL_OPERATION"),
|
||||
task.getSkippedStageReasons());
|
||||
assertEquals(2, task.getPhaseEvidence().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesPriorityAccelerationTaskTypeAndFaultPreset() {
|
||||
assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid(
|
||||
@@ -106,4 +142,15 @@ class PointToPointSimulationServiceTest {
|
||||
private PointToPointSimulationService service() {
|
||||
return new PointToPointSimulationService(baseInput);
|
||||
}
|
||||
|
||||
private String[] ordinaryAdjacentEndpoints() {
|
||||
return baseInput.getGridMap().getSites().keySet().stream()
|
||||
.filter(site -> !baseInput.getChannelFrontTopology().frontFor(site).isPresent())
|
||||
.flatMap(site -> baseInput.getGridMap().getNeighbors(site).stream()
|
||||
.filter(neighbor -> !baseInput.getChannelFrontTopology()
|
||||
.frontFor(neighbor).isPresent())
|
||||
.map(neighbor -> new String[]{site, neighbor}))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("ordinary adjacent sites are required"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,24 @@ class SimulationVisualizationMapperTest {
|
||||
// A/B 审计也走二段式业务链,每个重新规划的业务阶段都必须作为独立路径版本保留下来。
|
||||
assertEquals(Arrays.asList(1, 2, 3), tasks.get(0).get("pathVersions"));
|
||||
assertTrue(tasks.get(0).containsKey("rerouteEvents"));
|
||||
assertEquals("SOURCE_ALIGNED_BUSINESS", payload.get("executionMode"));
|
||||
assertEquals(sourceTask.getCargoState().name(), tasks.get(0).get("cargoState"));
|
||||
assertEquals(sourceTask.getBusinessCompletedMillis(),
|
||||
tasks.get(0).get("businessCompletedMillis"));
|
||||
assertEquals(sourceTask.getVehicleReleasedMillis(),
|
||||
tasks.get(0).get("vehicleReleasedMillis"));
|
||||
assertEquals(sourceTask.getVehicleReleaseStatus().name(),
|
||||
tasks.get(0).get("vehicleReleaseStatus"));
|
||||
List<Map<String, Object>> phaseEvidence =
|
||||
listOfMaps(tasks.get(0).get("phaseEvidence"));
|
||||
assertFalse(phaseEvidence.isEmpty());
|
||||
assertEquals(sourceTask.getPhaseEvidence().get(0).getPhase().name(),
|
||||
phaseEvidence.get(0).get("phase"));
|
||||
assertEquals(sourceTask.getPhaseEvidence().get(0).getPlannedPath(),
|
||||
phaseEvidence.get(0).get("plannedPath"));
|
||||
assertTrue(phaseEvidence.get(0).containsKey("fromSite"));
|
||||
assertTrue(phaseEvidence.get(0).containsKey("toSite"));
|
||||
assertTrue(phaseEvidence.get(0).containsKey("completionReason"));
|
||||
Map<String, Object> comparison = map(tasks.get(0).get("pathComparison"));
|
||||
assertEquals(0, comparison.get("sharedEdgeCount"));
|
||||
assertEquals(sourceTask.getPlannedPath().size() - 1,
|
||||
@@ -122,6 +140,12 @@ class SimulationVisualizationMapperTest {
|
||||
assertEquals(1, summary.get("completedCount"));
|
||||
assertEquals(0, summary.get("waitingCount"));
|
||||
assertEquals(0, summary.get("failedCount"));
|
||||
assertEquals(1, summary.get("rawSnapshotCount"));
|
||||
assertEquals(1, summary.get("uniqueQueueCount"));
|
||||
assertEquals(0, summary.get("duplicateSnapshotCount"));
|
||||
assertEquals(1, summary.get("businessTaskCount"));
|
||||
assertEquals(0, summary.get("operationalQueueCount"));
|
||||
assertEquals(0, summary.get("incompleteVehicleReleaseCount"));
|
||||
assertEquals(1, map(summary.get("routeComparabilityCounts"))
|
||||
.get("INVALID_HISTORY_PATH"));
|
||||
assertEquals(0, invariants.get("totalSafetyViolations"));
|
||||
|
||||
Reference in New Issue
Block a user