99
1010import jdk .incubator .java .util .json .JsonBoolean ;
1111import jdk .incubator .java .util .json .JsonArray ;
12+ import jdk .incubator .java .util .json .JsonNumber ;
1213import jdk .incubator .java .util .json .JsonObject ;
1314import jdk .incubator .java .util .json .JsonString ;
15+ import jdk .incubator .java .util .json .Json ;
1416
1517import java .util .Set ;
1618import java .util .Map ;
@@ -234,4 +236,108 @@ void testNormalizeTypeName() {
234236 .isEqualTo ("String" );
235237 }
236238 }
239+
240+ @ Nested
241+ @ DisplayName ("Drift Detection Gates" )
242+ class DriftDetectionGateTests {
243+
244+ private static JsonObject upstreamErrorDiff (String className , String error ) {
245+ return JsonObject .of (Map .of (
246+ "className" , JsonString .of (className ),
247+ "status" , JsonString .of ("UPSTREAM_ERROR" ),
248+ "error" , JsonString .of (error )
249+ ));
250+ }
251+
252+ private static JsonObject report (long differentApi , long missingUpstream , JsonArray differences ) {
253+ return JsonObject .of (Map .of (
254+ "timestamp" , JsonString .of ("2026-08-30T00:00:00Z" ),
255+ "summary" , JsonObject .of (Map .of (
256+ "totalClasses" , JsonNumber .of (differentApi + missingUpstream ),
257+ "matchingClasses" , JsonNumber .of (0 ),
258+ "differentApi" , JsonNumber .of (differentApi ),
259+ "missingUpstream" , JsonNumber .of (missingUpstream )
260+ )),
261+ "differences" , differences
262+ ));
263+ }
264+
265+ @ Test
266+ @ DisplayName ("Upstream fetch failures must count as drift, not all-clear" )
267+ void testHasDifferencesTreatsUpstreamErrorAsDrift () {
268+ LoggingControl .setupCleanLogging ();
269+ java .util .logging .Logger .getLogger (getClass ().getName ())
270+ .info (() -> "TEST: testHasDifferencesTreatsUpstreamErrorAsDrift" );
271+ final var allErrors = report (0 , 2 , JsonArray .of (java .util .List .of (
272+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found" ),
273+ upstreamErrorDiff ("JsonObject" , "HTTP_ERROR: Status 500" )
274+ )));
275+
276+ assertThat (ApiTracker .hasDifferences (allErrors ))
277+ .as ("a fully-blind detector run (every class UPSTREAM_ERROR) must report drift" )
278+ .isTrue ();
279+
280+ final var mixed = report (1 , 1 , JsonArray .of (java .util .List .of (
281+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found" ),
282+ JsonObject .of (Map .of (
283+ "className" , JsonString .of ("JsonValue" ),
284+ "status" , JsonString .of ("DIFFERENT" ),
285+ "differences" , JsonArray .of (java .util .List .of ())
286+ ))
287+ )));
288+ assertThat (ApiTracker .hasDifferences (mixed )).isTrue ();
289+ }
290+
291+ @ Test
292+ @ DisplayName ("Fingerprint covers UPSTREAM_ERROR classes and is stable and distinct" )
293+ void testFingerprintCoversUpstreamErrors () {
294+ LoggingControl .setupCleanLogging ();
295+ java .util .logging .Logger .getLogger (getClass ().getName ())
296+ .info (() -> "TEST: testFingerprintCoversUpstreamErrors" );
297+ final var none = report (0 , 0 , JsonArray .of (java .util .List .of ()));
298+ assertThat (ApiTracker .generateFingerprint (none )).isEqualTo ("0000000" );
299+
300+ final var errorsA = report (0 , 2 , JsonArray .of (java .util .List .of (
301+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found" ),
302+ upstreamErrorDiff ("JsonObject" , "NOT_FOUND: Upstream file not found" )
303+ )));
304+ final var errorsAgain = report (0 , 2 , JsonArray .of (java .util .List .of (
305+ upstreamErrorDiff ("JsonObject" , "NOT_FOUND: Upstream file not found" ),
306+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found" )
307+ )));
308+ final var errorsB = report (0 , 1 , JsonArray .of (java .util .List .of (
309+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found" )
310+ )));
311+
312+ final var fpA = ApiTracker .generateFingerprint (errorsA );
313+ assertThat (fpA )
314+ .as ("fetch-failure drift must not hash to the no-differences sentinel" )
315+ .isNotEqualTo ("0000000" );
316+ assertThat (ApiTracker .generateFingerprint (errorsAgain ))
317+ .as ("same error set in different order must fingerprint identically" )
318+ .isEqualTo (fpA );
319+ assertThat (ApiTracker .generateFingerprint (errorsB ))
320+ .as ("different error sets must fingerprint differently" )
321+ .isNotEqualTo (fpA );
322+ }
323+
324+ @ Test
325+ @ DisplayName ("Summary renders a Missing Upstream section for fetch failures" )
326+ void testSummaryRendersMissingUpstreamSection () {
327+ LoggingControl .setupCleanLogging ();
328+ java .util .logging .Logger .getLogger (getClass ().getName ())
329+ .info (() -> "TEST: testSummaryRendersMissingUpstreamSection" );
330+ final var allErrors = report (0 , 2 , JsonArray .of (java .util .List .of (
331+ upstreamErrorDiff ("JsonNumber" , "NOT_FOUND: Upstream file not found (possibly deleted or renamed)" ),
332+ upstreamErrorDiff ("JsonObject" , "HTTP_ERROR: Status 500" )
333+ )));
334+
335+ final var summary = ApiTracker .generateSummary (allErrors );
336+
337+ assertThat (summary ).contains ("Missing Upstream" );
338+ assertThat (summary ).contains ("JsonNumber" );
339+ assertThat (summary ).contains ("JsonObject" );
340+ assertThat (summary ).contains ("NOT_FOUND: Upstream file not found (possibly deleted or renamed)" );
341+ }
342+ }
237343}
0 commit comments