diff --git a/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md b/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md index 3ad8c0d..71e6b5c 100644 --- a/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md +++ b/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md @@ -10,7 +10,7 @@ **Status (2026-07-31):** Completed and independently reviewed. Final evidence run: `outputs/wcs-shadow-history-replay-20260731-0945-af0b52d`; Maven verification: -354 tests, 0 failures, 0 errors, 0 skipped. +355 tests, 0 failures, 0 errors, 0 skipped. ## Global Constraints diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java index b01ce41..69c9d86 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java @@ -8,6 +8,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; import java.nio.file.Path; import java.security.MessageDigest; import java.net.URLClassLoader; @@ -82,7 +86,16 @@ public final class HistoricalQueueLoader { } public static void verifySha256(String expected, byte[] bytes) { - byte[] canonicalBytes = new String(bytes, StandardCharsets.UTF_8) + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + String text; + try { + text = decoder.decode(ByteBuffer.wrap(bytes)).toString(); + } catch (CharacterCodingException ex) { + throw new IllegalArgumentException("history snapshot must be valid UTF-8", ex); + } + byte[] canonicalBytes = text .replace("\r\n", "\n") .replace('\r', '\n') .getBytes(StandardCharsets.UTF_8); diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java index 2a9677e..d24388f 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java @@ -10,7 +10,6 @@ import java.nio.file.Path; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; -import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -50,6 +49,7 @@ public final class HistoricalReplayReportWriter { } summary.put("vehicles", vehicles); summary.put("taskTypes", taskTypes); + summary.put("dimensionScope", "REPRESENTATIVE_SAMPLE"); summary.put("replayableRows", paths.stream().filter(HistoricalPath::isReplayable).count()); summary.put("qualityIssueRows", paths.stream().filter(p -> !p.getIssues().isEmpty()).count()); Map issueCounts = new LinkedHashMap(); diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java index a94dada..1690442 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java @@ -45,4 +45,13 @@ class HistoricalQueueLoaderTest { HistoricalQueueLoader.verifySha256( canonicalSha256, "a\r\nb\r\n".getBytes(StandardCharsets.UTF_8)); } + + @Test + void rejectsMalformedUtf8BeforeCanonicalHashing() { + String replacementCharacterSha256 = + "83d544ccc223c057d2bf80d3f2a32982c32c3c0db8e2674820da5064783fb097"; + assertThrows(IllegalArgumentException.class, () -> + HistoricalQueueLoader.verifySha256( + replacementCharacterSha256, new byte[]{(byte) 0xc3})); + } } diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java index 8072569..6deab6e 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java @@ -92,6 +92,7 @@ class HistoricalReplayAnalyzerTest { assertEquals("2026-03-01 10:18:24", summaryJson.path("minCreateDate").asText()); assertEquals(3, summaryJson.path("vehicles").size()); assertEquals(3, summaryJson.path("taskTypes").size()); + assertEquals("REPRESENTATIVE_SAMPLE", summaryJson.path("dimensionScope").asText()); assertEquals(2, summaryJson.path("qualityIssueCounts").path("UNKNOWN_MAP_SITE").asInt()); assertEquals(dataset.getRecords().size(), summaryJson.path("replayGrant").asInt()