@@ -517,11 +517,12 @@ private Optional<Server> startJetty(final List<Object> configuredObjects) throws
517517 /**
518518 * Block until deployed webapps reach the readiness level required for tests.
519519 * <p>
520- * Standalone mode deploys a single context at {@code /}; distribution mode deploys {@code /exist}
521- * plus an optional portal at {@code /}. Only {@code /exist} must be {@code isAvailable()} before
522- * rewrite-based HTTP tests run. Standalone integration tests (webdav, restxq, file) use direct
523- * servlet mappings and rely on {@link org.exist.TestUtils#awaitJettyWebAppReady} HTTP probes
524- * after {@code isStarted()}. The portal redirect webapp is non-gating (see Option C in test docs).
520+ * Every context except the distribution portal at {@code /} must be
521+ * {@link org.eclipse.jetty.server.handler.ContextHandler#isAvailable()} — Jetty returns
522+ * {@code 503} on all paths while unavailable. The portal coexists with {@code /exist} and is
523+ * non-gating. Integration test webapps defer servlet {@code load-on-startup} so the context
524+ * can become available before Milton/REST init; {@link org.exist.TestUtils} HTTP probes then
525+ * warm direct servlet mappings.
525526 */
526527 private void awaitWebAppContextsStarted (final List <Handler > handlers ) throws InterruptedException {
527528 final List <WebAppContext > webApps = new ArrayList <>();
@@ -534,6 +535,7 @@ private void awaitWebAppContextsStarted(final List<Handler> handlers) throws Int
534535 return ;
535536 }
536537
538+ final boolean distributionLayout = isDistributionLayout (webApps );
537539 final long timeoutMs = slowEnvironmentStartupDeadlineMs ();
538540 final long deadline = System .currentTimeMillis () + timeoutMs ;
539541 while (System .currentTimeMillis () < deadline ) {
@@ -543,7 +545,7 @@ private void awaitWebAppContextsStarted(final List<Handler> handlers) throws Int
543545 throw new IllegalStateException (
544546 "Web application failed to start: " + webApp .getContextPath ());
545547 }
546- if (!isWebAppContextReady (webApp )) {
548+ if (!isWebAppContextReady (webApp , distributionLayout )) {
547549 allReady = false ;
548550 break ;
549551 }
@@ -556,43 +558,53 @@ private void awaitWebAppContextsStarted(final List<Handler> handlers) throws Int
556558 }
557559 throw new IllegalStateException (
558560 "Web application context did not become ready within " + timeoutMs + "ms: "
559- + describePendingWebApps (webApps ));
561+ + describePendingWebApps (webApps , distributionLayout ));
562+ }
563+
564+ private static boolean isDistributionLayout (final List <WebAppContext > webApps ) {
565+ return webApps .stream ().anyMatch (webApp -> "/exist" .equals (webApp .getContextPath ()));
560566 }
561567
562568 /**
563- * {@code /exist} (distribution) must be {@code isAvailable()} for rewrite-based HTTP tests.
564- * Standalone {@code /} and the distribution portal only need {@code isStarted()}; module HTTP
565- * probes or the in-process broker cover the rest.
569+ * Distribution portal {@code /} only needs {@code isStarted()}. Standalone {@code /} and
570+ * {@code /exist} must be {@code isAvailable()} or HTTP clients see {@code 503}.
566571 */
567- private static boolean isWebAppContextReady (final WebAppContext webApp ) {
572+ private static boolean isWebAppContextReady (final WebAppContext webApp , final boolean distributionLayout ) {
568573 if (!webApp .isStarted ()) {
569574 return false ;
570575 }
571- return !"/exist" .equals (webApp .getContextPath ()) || webApp .isAvailable ();
576+ if (distributionLayout && "/" .equals (webApp .getContextPath ())) {
577+ return true ;
578+ }
579+ return webApp .isAvailable ();
572580 }
573581
574- private static String describePendingWebApps (final List <WebAppContext > webApps ) {
582+ private static String describePendingWebApps (final List <WebAppContext > webApps , final boolean distributionLayout ) {
575583 final StringBuilder details = new StringBuilder ();
576584 for (final WebAppContext webApp : webApps ) {
577585 if (webApp .isFailed ()) {
578586 continue ;
579587 }
580- if (!isWebAppContextReady (webApp )) {
588+ if (!isWebAppContextReady (webApp , distributionLayout )) {
581589 if (!details .isEmpty ()) {
582590 details .append ("; " );
583591 }
584592 details .append (webApp .getContextPath ())
585593 .append (" started=" ).append (webApp .isStarted ())
586594 .append (" available=" ).append (webApp .isAvailable ())
587- .append (" requireAvailable=" ).append ("/exist" . equals (webApp . getContextPath () ));
595+ .append (" requireAvailable=" ).append (requiresAvailability (webApp , distributionLayout ));
588596 }
589597 }
590598 return details .isEmpty () ? "unknown" : details .toString ();
591599 }
592600
601+ private static boolean requiresAvailability (final WebAppContext webApp , final boolean distributionLayout ) {
602+ return !(distributionLayout && "/" .equals (webApp .getContextPath ()));
603+ }
604+
593605 private static long slowEnvironmentStartupDeadlineMs () {
594606 if (isWindowsHost ()) {
595- return System .getenv ("CI" ) != null ? 180_000L : 120_000L ;
607+ return System .getenv ("CI" ) != null ? 300_000L : 120_000L ;
596608 }
597609 if (System .getenv ("CI" ) != null ) {
598610 return 180_000L ;
0 commit comments