feat(ft546): add WCS adapter configuration profiles

This commit is contained in:
zgw
2026-08-11 11:41:50 +08:00
parent f9ebbcf51c
commit 11a21e0688
9 changed files with 605 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
package com.ruoyi.project.ft546.config;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class Ft546ConfigurationValidatorTest {
@Test
void defaultsToLegacySoDeploymentDoesNotTakeSchedulingOwnership() {
Ft546WcsProperties properties = new Ft546WcsProperties();
assertEquals(Ft546WcsProperties.RuntimeMode.LEGACY,
properties.getRuntimeMode());
assertTrue(properties.getDispatchIntervalMs() > 0L);
assertTrue(properties.getCommandPollIntervalMs() > 0L);
}
@Test
void rejectsLegacyProfileThatClaimsPrepareCommit() {
Ft546DeviceProfile profile = legacySrgvProfile();
profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
assertThrows(IllegalArgumentException.class,
() -> new Ft546ConfigurationValidator().validate(profile));
}
@Test
void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
Ft546DeviceProfile profile = legacySrgvProfile();
profile.getRegisters().put("taskType", "4000");
assertThrows(IllegalArgumentException.class,
() -> new Ft546ConfigurationValidator().validate(profile));
}
@Test
void rejectsTwoStageCapabilityWithoutPassingEvidenceForExactProfile() {
Ft546DeviceProfile profile = legacySrgvProfile();
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
Ft546CapabilityGate gate = gate();
CapabilityGateResult result = gate.evaluate(profile, null);
assertFalse(result.isPublishable());
assertEquals("TWO_STAGE_EVIDENCE_REQUIRED", result.getReasonCode());
assertFalse(result.isEnabled("TWO_STAGE_ATOMIC_TASK"));
}
@Test
void enablesTwoStageOnlyForPassingUnexpiredEvidenceBoundToProfile() {
Ft546DeviceProfile profile = legacySrgvProfile();
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
Ft546CapabilityEvidence evidence = new Ft546CapabilityEvidence(
1L, "CORPUS-SHA256", "RUN-1", "COMMIT-1", true,
Instant.parse("2026-08-10T00:00:00Z"),
Instant.parse("2026-09-10T00:00:00Z"));
CapabilityGateResult result = gate().evaluate(profile, evidence);
assertTrue(result.isPublishable());
assertTrue(result.isEnabled("TWO_STAGE_ATOMIC_TASK"));
}
@Test
void legacyProfileKeepsUnsupportedCapabilitiesDisabled() {
CapabilityGateResult result = gate().evaluate(legacySrgvProfile(), null);
assertFalse(result.isEnabled("COMMAND_RESULT_QUERY"));
assertFalse(result.isEnabled("ORDERED_DEVICE_SNAPSHOT"));
assertFalse(result.isEnabled("CANDIDATE_BUFFER"));
assertFalse(result.isEnabled("PREPARE_COMMIT_SEGMENT"));
assertFalse(result.isEnabled("IDEMPOTENT_COMMAND_KEY"));
assertFalse(result.isEnabled("BOOT_SESSION_ID"));
assertTrue(result.isEnabled("POINT_TO_POINT_COMMAND"));
}
@Test
void checkedInProfileKeepsEndpointsOutAndLegacyAddressesExact()
throws Exception {
String yaml = readProjectFile("ruoyi-admin/src/main/resources/ft546/"
+ "wcs-device-profiles.yml");
assertFalse(yaml.contains("192.168."));
assertFalse(yaml.toLowerCase().contains("password"));
assertTrue(yaml.contains("taskNo: \"4000\""));
assertTrue(yaml.contains("taskType: \"4002\""));
assertTrue(yaml.contains("path: \"4004\""));
assertTrue(yaml.contains("trigger: \"4021\""));
}
@Test
void packagesAdminOwnedAdapterConfigurationMigration() throws Exception {
String sql = readProjectFile(
"sql/wcs/V004__ft546_project_adapter_config.sql");
assertTrue(sql.contains("project_device_endpoint"));
assertTrue(sql.contains("project_plc_address"));
assertTrue(sql.contains("project_protocol_profile"));
assertTrue(sql.contains("project_protocol_capability_evidence"));
assertTrue(sql.contains("project_wms_mapping"));
}
private static Ft546CapabilityGate gate() {
return new Ft546CapabilityGate(() ->
Instant.parse("2026-08-11T00:00:00Z"));
}
private static Ft546DeviceProfile legacySrgvProfile() {
Ft546DeviceProfile profile = new Ft546DeviceProfile();
profile.setDeviceId("1");
profile.setDeviceType("SHUTTLE");
profile.setAdapterType("FT546_LEGACY_SRGV");
profile.setProfileVersion(1L);
profile.setProtocolCorpusHash("CORPUS-SHA256");
profile.setCapabilities(new LinkedHashSet<String>(
Collections.singletonList("POINT_TO_POINT_COMMAND")));
LinkedHashMap<String, String> registers = new LinkedHashMap<String, String>();
registers.put("taskNo", "4000");
registers.put("taskType", "4002");
registers.put("path", "4004");
registers.put("trigger", "4021");
registers.put("status", "4068");
registers.put("execution", "4060");
profile.setRegisters(registers);
return profile;
}
private static String readProjectFile(String relative) throws Exception {
Path current = Paths.get(System.getProperty(
"maven.multiModuleProjectDirectory", "."))
.toAbsolutePath().normalize();
while (current != null) {
Path file = current.resolve(relative);
if (Files.isRegularFile(file)) {
return new String(Files.readAllBytes(file),
StandardCharsets.UTF_8);
}
current = current.getParent();
}
throw new java.nio.file.NoSuchFileException(relative);
}
}