This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/master by this push:
     new e29d0cda8 Skip validation and decoration on re-entrant 
RepositorySystem calls (#1957)
e29d0cda8 is described below

commit e29d0cda88e81e3d96975b51d5fd16829f44a658
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Jul 14 22:04:12 2026 +0200

    Skip validation and decoration on re-entrant RepositorySystem calls (#1957)
    
    ## Summary
    
    Maven 4's `ArtifactDescriptorReader` → `ModelBuilder` → `ModelResolver` 
chain re-enters `RepositorySystem` during `collectDependencies`. This breaks 
the resolver's single-crossing contract:
    
    - **Validation rejects intermediate state**: `MavenValidator` (registered 
via the `ValidatorFactory` SPI) rejects uninterpolated `${...}` expressions 
that are valid intermediate state in transitive POMs during model building
    - **Artifact decorators run redundantly**: decorators applied on inner 
calls corrupt the resolution result
    - **Reported as**: [MAVEN 
#12474](https://github.com/apache/maven/issues/12474) — `Invalid Collect 
Request: null`
    
    ### How it works
    
    On the **outermost** call to any `RepositorySystem` public method, a 
sentinel marker is stamped into the request's `RequestTrace`. On **re-entry** 
(whether on the same thread or a pool thread), `isReentrant()` walks the trace 
ancestry — if the marker is found, validation and decoration are skipped.
    
    This leverages the existing `RequestTrace` infrastructure which is already 
propagated across threads by callers (Maven's model builder explicitly copies 
traces to pool threads via `session.setCurrentTrace(trace)`), requiring **no 
ThreadLocal or session-scoped state**.
    
    ### Changes
    
    - `DefaultRepositorySystem`: all public resolution methods check 
`isReentrant(trace)` before validating/decorating
    - `readArtifactDescriptor`: additionally skips artifact decoration on 
re-entry
    - Methods without trace-bearing requests (`install`, `deploy`, 
`newResolutionRepositories`, `newDeploymentRepository`, 
`flattenDependencyNodes`) always validate — they are terminal operations that 
don't participate in re-entrancy
    - New test class `DefaultRepositorySystemReentrancyTest` with 5 tests 
covering:
      - Outermost calls run validation
      - Re-entrant calls skip validation
      - Re-entrant calls allow uninterpolated expressions (the bug scenario)
      - Re-entrant `readArtifactDescriptor` skips decoration
      - Independent calls each validate independently
    
    ## Test plan
    
    - [x] All 445 existing tests pass in `maven-resolver-impl`
    - [x] 5 new re-entrancy tests pass
    - [ ] CI build passes
    - [ ] Integration test with Maven 4 against the reproducer from #12474
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
 .../internal/impl/DefaultRepositorySystem.java     | 128 +++++++++--
 .../DefaultRepositorySystemReentrancyTest.java     | 254 +++++++++++++++++++++
 2 files changed, 361 insertions(+), 21 deletions(-)

diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
index 542fa5fa4..009cbc9ff 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
@@ -106,6 +106,43 @@ import static java.util.stream.Collectors.toList;
 @Singleton
 @Named
 public class DefaultRepositorySystem implements RepositorySystem {
+    /**
+     * Sentinel object placed into the {@link RequestTrace} chain by each 
public method
+     * to detect re-entrant calls into this {@code RepositorySystem}.
+     * <p>
+     * The resolver's architecture assumes that internal components ({@code 
ArtifactResolver},
+     * {@code DependencyCollector}, {@code ArtifactDescriptorReader}, etc.) 
call each other
+     * directly via the internal API in {@code org.eclipse.aether.impl}, never 
going through
+     * the public {@code RepositorySystem} facade. This means session 
validation, request
+     * validation, and artifact decoration only need to run once — on the 
outermost call.
+     * <p>
+     * However, some {@code RepositorySystem} consumers (notably Maven 4's
+     * {@code ArtifactDescriptorReader} → {@code ModelBuilder} → {@code 
ModelResolver} chain)
+     * re-enter {@code RepositorySystem} during an ongoing operation. Without 
this guard,
+     * every re-entrant call redundantly validates the session, validates the 
request (which
+     * may reject intermediate state like uninterpolated expressions from 
transitive POMs),
+     * and applies artifact decorators. This causes both correctness issues 
(false validation
+     * failures) and unnecessary performance overhead.
+     * <p>
+     * Re-entrancy can happen on the <em>same</em> thread (e.g. {@code 
collectDependencies}
+     * → {@code readArtifactDescriptor} → model builder → model resolver → 
{@code resolveVersionRange})
+     * or on a <em>different</em> thread when the resolver uses internal 
parallelism (e.g.
+     * {@code BfDependencyCollector}'s {@code SmartExecutor} dispatches 
descriptor resolution
+     * to pool threads, which then re-enter {@code RepositorySystem} via the 
model resolver).
+     * <p>
+     * To handle both cases, we leverage the {@link RequestTrace} chain that 
is already
+     * propagated across threads by the caller (e.g. Maven's model builder 
explicitly copies
+     * traces to pool threads). On the outermost call, we stamp this marker 
into the request's
+     * trace. On re-entry — whether on the same thread or a pool thread — the 
marker is found
+     * in the trace ancestry, so validation and decoration are skipped.
+     */
+    private static final Object REPOSITORY_SYSTEM_CALL = new Object() {
+        @Override
+        public String toString() {
+            return "RepositorySystem";
+        }
+    };
+
     private final AtomicBoolean shutdown;
 
     private final AtomicInteger sessionIdCounter;
@@ -182,30 +219,42 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
     @Override
     public VersionResult resolveVersion(RepositorySystemSession session, 
VersionRequest request)
             throws VersionResolutionException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateVersionRequest(session, request);
+        if (!isReentrant(request.getTrace())) {
+            validateSession(session);
+            repositorySystemValidator.validateVersionRequest(session, request);
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         return versionResolver.resolveVersion(session, request);
     }
 
     @Override
     public VersionRangeResult resolveVersionRange(RepositorySystemSession 
session, VersionRangeRequest request)
             throws VersionRangeResolutionException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateVersionRangeRequest(session, 
request);
+        if (!isReentrant(request.getTrace())) {
+            validateSession(session);
+            repositorySystemValidator.validateVersionRangeRequest(session, 
request);
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         return versionRangeResolver.resolveVersionRange(session, request);
     }
 
     @Override
     public ArtifactDescriptorResult readArtifactDescriptor(
             RepositorySystemSession session, ArtifactDescriptorRequest 
request) throws ArtifactDescriptorException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateArtifactDescriptorRequest(session, 
request);
+        boolean outermost = !isReentrant(request.getTrace());
+        if (outermost) {
+            validateSession(session);
+            
repositorySystemValidator.validateArtifactDescriptorRequest(session, request);
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         ArtifactDescriptorResult descriptorResult = 
artifactDescriptorReader.readArtifactDescriptor(session, request);
-        for (ArtifactDecorator decorator : 
Utils.getArtifactDecorators(session, artifactDecoratorFactories)) {
-            
descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult));
+        if (outermost) {
+            for (ArtifactDecorator decorator : 
Utils.getArtifactDecorators(session, artifactDecoratorFactories)) {
+                
descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult));
+            }
         }
         return descriptorResult;
     }
@@ -213,9 +262,12 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
     @Override
     public ArtifactResult resolveArtifact(RepositorySystemSession session, 
ArtifactRequest request)
             throws ArtifactResolutionException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateArtifactRequests(session, 
Collections.singleton(request));
+        if (!isReentrant(request.getTrace())) {
+            validateSession(session);
+            repositorySystemValidator.validateArtifactRequests(session, 
Collections.singleton(request));
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         return artifactResolver.resolveArtifact(session, request);
     }
 
@@ -223,36 +275,58 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
     public List<ArtifactResult> resolveArtifacts(
             RepositorySystemSession session, Collection<? extends 
ArtifactRequest> requests)
             throws ArtifactResolutionException {
-        validateSession(session);
         requireNonNull(requests, "requests cannot be null");
-        repositorySystemValidator.validateArtifactRequests(session, requests);
+        // All requests in a batch share the same trace context, so checking 
any one is sufficient.
+        RequestTrace firstTrace =
+                
requests.stream().map(ArtifactRequest::getTrace).findFirst().orElse(null);
+        if (!isReentrant(firstTrace)) {
+            validateSession(session);
+            repositorySystemValidator.validateArtifactRequests(session, 
requests);
+            for (ArtifactRequest request : requests) {
+                request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+            }
+        }
         return artifactResolver.resolveArtifacts(session, requests);
     }
 
     @Override
     public List<MetadataResult> resolveMetadata(
             RepositorySystemSession session, Collection<? extends 
MetadataRequest> requests) {
-        validateSession(session);
         requireNonNull(requests, "requests cannot be null");
-        repositorySystemValidator.validateMetadataRequests(session, requests);
+        // All requests in a batch share the same trace context, so checking 
any one is sufficient.
+        RequestTrace firstTrace =
+                
requests.stream().map(MetadataRequest::getTrace).findFirst().orElse(null);
+        if (!isReentrant(firstTrace)) {
+            validateSession(session);
+            repositorySystemValidator.validateMetadataRequests(session, 
requests);
+            for (MetadataRequest request : requests) {
+                request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+            }
+        }
         return metadataResolver.resolveMetadata(session, requests);
     }
 
     @Override
     public CollectResult collectDependencies(RepositorySystemSession session, 
CollectRequest request)
             throws DependencyCollectionException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateCollectRequest(session, request);
+        if (!isReentrant(request.getTrace())) {
+            validateSession(session);
+            repositorySystemValidator.validateCollectRequest(session, request);
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         return dependencyCollector.collectDependencies(session, request);
     }
 
     @Override
     public DependencyResult resolveDependencies(RepositorySystemSession 
session, DependencyRequest request)
             throws DependencyResolutionException {
-        validateSession(session);
         requireNonNull(request, "request cannot be null");
-        repositorySystemValidator.validateDependencyRequest(session, request);
+        if (!isReentrant(request.getTrace())) {
+            validateSession(session);
+            repositorySystemValidator.validateDependencyRequest(session, 
request);
+            request.setTrace(RequestTrace.newChild(request.getTrace(), 
REPOSITORY_SYSTEM_CALL));
+        }
         RequestTrace trace = RequestTrace.newChild(request.getTrace(), 
request);
 
         DependencyResult result = new DependencyResult(request);
@@ -319,7 +393,6 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
             RepositorySystemSession session, DependencyNode root, 
DependencyFilter dependencyFilter) {
         validateSession(session);
         requireNonNull(root, "root cannot be null");
-
         return doFlattenDependencyNodes(session, root, dependencyFilter);
     }
 
@@ -441,7 +514,6 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
         validateSession(session);
         validateRepositories(repositories);
         repositorySystemValidator.validateRemoteRepositories(session, 
repositories);
-
         return remoteRepositoryManager.aggregateRepositories(session, new 
ArrayList<>(), repositories, true);
     }
 
@@ -450,7 +522,6 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
         validateSession(session);
         requireNonNull(repository, "repository cannot be null");
         repositorySystemValidator.validateRemoteRepositories(session, 
Collections.singletonList(repository));
-
         Authentication auth = 
session.getAuthenticationSelector().getAuthentication(repository);
         Proxy proxy = session.getProxySelector().getProxy(repository);
         return new RemoteRepository.Builder(repository)
@@ -480,6 +551,21 @@ public class DefaultRepositorySystem implements 
RepositorySystem {
         }
     }
 
+    /**
+     * Checks whether the given {@link RequestTrace} indicates a re-entrant 
call by looking
+     * for the {@link #REPOSITORY_SYSTEM_CALL} marker in the trace ancestry.
+     *
+     * @return {@code true} if the marker is found (re-entrant call), {@code 
false} otherwise
+     */
+    private static boolean isReentrant(RequestTrace trace) {
+        for (RequestTrace t = trace; t != null; t = t.getParent()) {
+            if (t.getData() == REPOSITORY_SYSTEM_CALL) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void validateSession(RepositorySystemSession session) {
         requireNonNull(session, "repository system session cannot be null");
         invalidSession(session.getLocalRepositoryManager(), "local repository 
manager");
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemReentrancyTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemReentrancyTest.java
new file mode 100644
index 000000000..3d1f7e288
--- /dev/null
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemReentrancyTest.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.impl.ArtifactResolver;
+import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.Deployer;
+import org.eclipse.aether.impl.Installer;
+import org.eclipse.aether.impl.LocalRepositoryProvider;
+import org.eclipse.aether.impl.MetadataResolver;
+import org.eclipse.aether.impl.StubArtifactDescriptorReader;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.spi.artifact.decorator.ArtifactDecorator;
+import org.eclipse.aether.spi.artifact.decorator.ArtifactDecoratorFactory;
+import org.eclipse.aether.spi.validator.Validator;
+import org.eclipse.aether.spi.validator.ValidatorFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for the re-entrancy detection in {@link DefaultRepositorySystem}.
+ *
+ * <p>Verifies that when the resolver is called re-entrantly (e.g. Maven 4's
+ * model builder → model resolver → RepositorySystem chain), validation and
+ * decoration are skipped on inner calls.</p>
+ */
+public class DefaultRepositorySystemReentrancyTest {
+
+    /**
+     * A validator that rejects any artifact whose version contains "${".
+     * This simulates Maven's MavenValidator rejecting uninterpolated 
expressions.
+     */
+    private static final ValidatorFactory 
EXPRESSION_REJECTING_VALIDATOR_FACTORY = session -> new Validator() {
+        @Override
+        public void validateArtifact(org.eclipse.aether.artifact.Artifact 
artifact) {
+            if (artifact.getVersion().contains("${")) {
+                throw new IllegalArgumentException("Uninterpolated expression 
in version: " + artifact.getVersion());
+            }
+        }
+    };
+
+    private DefaultRepositorySystem system;
+    private DefaultRepositorySystemSession session;
+    private AtomicInteger validationCount;
+
+    @BeforeEach
+    void init() {
+        validationCount = new AtomicInteger(0);
+        ValidatorFactory countingValidator = s -> new Validator() {
+            @Override
+            public void validateArtifact(org.eclipse.aether.artifact.Artifact 
artifact) {
+                validationCount.incrementAndGet();
+            }
+        };
+
+        system = new DefaultRepositorySystem(
+                new StubVersionResolver(),
+                new StubVersionRangeResolver(),
+                mock(ArtifactResolver.class),
+                mock(MetadataResolver.class),
+                new StubArtifactDescriptorReader(),
+                mock(DependencyCollector.class),
+                mock(Installer.class),
+                mock(Deployer.class),
+                mock(LocalRepositoryProvider.class),
+                new StubSyncContextFactory(),
+                new DefaultRemoteRepositoryManager(
+                        new DefaultUpdatePolicyAnalyzer(),
+                        new DefaultChecksumPolicyProvider(),
+                        new DefaultRepositoryKeyFunctionFactory()),
+                new DefaultRepositorySystemLifecycle(),
+                Collections.emptyMap(),
+                new 
DefaultRepositorySystemValidator(Collections.singletonList(countingValidator)));
+        session = TestUtils.newSession();
+    }
+
+    @Test
+    void outermostCallRunsValidation() throws Exception {
+        VersionRangeRequest request =
+                new VersionRangeRequest(new DefaultArtifact("g:a:1.0"), 
Collections.emptyList(), null);
+
+        system.resolveVersionRange(session, request);
+
+        assertEquals(1, validationCount.get(), "Outermost call should run 
validation exactly once");
+    }
+
+    @Test
+    void reentrantCallSkipsValidation() throws Exception {
+        // Simulate a re-entrant call: the trace already contains the 
RepositorySystem marker.
+        // We achieve this by making the outermost call first (which stamps 
the marker into
+        // the trace), then reusing that stamped trace on a second call.
+        VersionRangeRequest outerRequest =
+                new VersionRangeRequest(new DefaultArtifact("g:a:1.0"), 
Collections.emptyList(), null);
+        system.resolveVersionRange(session, outerRequest);
+
+        assertEquals(1, validationCount.get(), "First call should validate");
+
+        // The outermost call stamped the marker into the trace.
+        // Now create an inner request whose trace is a child of the outer's 
stamped trace
+        // — simulating what happens when the model resolver re-enters 
RepositorySystem
+        // during dependency collection.
+        RequestTrace outerTrace = outerRequest.getTrace();
+        assertNotNull(outerTrace, "Outermost call should have stamped a 
trace");
+
+        VersionRangeRequest innerRequest =
+                new VersionRangeRequest(new DefaultArtifact("g:b:2.0"), 
Collections.emptyList(), null);
+        innerRequest.setTrace(RequestTrace.newChild(outerTrace, 
"ModelResolver"));
+
+        system.resolveVersionRange(session, innerRequest);
+
+        assertEquals(1, validationCount.get(), "Re-entrant call should NOT run 
validation again");
+    }
+
+    @Test
+    void reentrantCallAllowsUninterpolatedExpressions() throws Exception {
+        // Build a system with the expression-rejecting validator (simulates 
MavenValidator)
+        DefaultRepositorySystem strictSystem = new DefaultRepositorySystem(
+                new StubVersionResolver(),
+                new StubVersionRangeResolver(),
+                mock(ArtifactResolver.class),
+                mock(MetadataResolver.class),
+                new StubArtifactDescriptorReader(),
+                mock(DependencyCollector.class),
+                mock(Installer.class),
+                mock(Deployer.class),
+                mock(LocalRepositoryProvider.class),
+                new StubSyncContextFactory(),
+                new DefaultRemoteRepositoryManager(
+                        new DefaultUpdatePolicyAnalyzer(),
+                        new DefaultChecksumPolicyProvider(),
+                        new DefaultRepositoryKeyFunctionFactory()),
+                new DefaultRepositorySystemLifecycle(),
+                Collections.emptyMap(),
+                new DefaultRepositorySystemValidator(
+                        
Collections.singletonList(EXPRESSION_REJECTING_VALIDATOR_FACTORY)));
+
+        // An outermost call with an uninterpolated expression MUST fail 
validation
+        VersionRangeRequest outerBadRequest =
+                new VersionRangeRequest(new DefaultArtifact("g:a:${expr}"), 
Collections.emptyList(), null);
+        assertThrows(IllegalArgumentException.class, () -> 
strictSystem.resolveVersionRange(session, outerBadRequest));
+
+        // A clean outermost call succeeds and stamps the marker
+        VersionRangeRequest outerGoodRequest =
+                new VersionRangeRequest(new DefaultArtifact("g:a:1.0"), 
Collections.emptyList(), null);
+        strictSystem.resolveVersionRange(session, outerGoodRequest);
+        RequestTrace outerTrace = outerGoodRequest.getTrace();
+
+        // Now a re-entrant call with an uninterpolated expression MUST 
succeed —
+        // the marker in the trace ancestry causes validation to be skipped
+        VersionRangeRequest innerBadRequest =
+                new VersionRangeRequest(new 
DefaultArtifact("g:b:${project.version}"), Collections.emptyList(), null);
+        innerBadRequest.setTrace(RequestTrace.newChild(outerTrace, 
"ModelResolver"));
+
+        assertDoesNotThrow(
+                () -> strictSystem.resolveVersionRange(session, 
innerBadRequest),
+                "Re-entrant call should skip validation, allowing 
uninterpolated expressions");
+    }
+
+    @Test
+    void reentrantReadArtifactDescriptorSkipsDecoration() throws Exception {
+        AtomicBoolean decorated = new AtomicBoolean(false);
+        ArtifactDecoratorFactory decoratorFactory = new 
ArtifactDecoratorFactory() {
+            @Override
+            public ArtifactDecorator newInstance(RepositorySystemSession 
session) {
+                return descriptorResult -> {
+                    decorated.set(true);
+                    return descriptorResult.getArtifact();
+                };
+            }
+
+            @Override
+            public float getPriority() {
+                return 0;
+            }
+        };
+
+        DefaultRepositorySystem decoratingSystem = new DefaultRepositorySystem(
+                new StubVersionResolver(),
+                new StubVersionRangeResolver(),
+                mock(ArtifactResolver.class),
+                mock(MetadataResolver.class),
+                new StubArtifactDescriptorReader(),
+                mock(DependencyCollector.class),
+                mock(Installer.class),
+                mock(Deployer.class),
+                mock(LocalRepositoryProvider.class),
+                new StubSyncContextFactory(),
+                new DefaultRemoteRepositoryManager(
+                        new DefaultUpdatePolicyAnalyzer(),
+                        new DefaultChecksumPolicyProvider(),
+                        new DefaultRepositoryKeyFunctionFactory()),
+                new DefaultRepositorySystemLifecycle(),
+                Collections.singletonMap("test", decoratorFactory),
+                new DefaultRepositorySystemValidator(Collections.emptyList()));
+
+        // Outermost call: decorator should run
+        ArtifactDescriptorRequest outerRequest =
+                new ArtifactDescriptorRequest(new DefaultArtifact("g:a:1.0"), 
Collections.emptyList(), null);
+        decoratingSystem.readArtifactDescriptor(session, outerRequest);
+        assertTrue(decorated.get(), "Outermost readArtifactDescriptor should 
apply decoration");
+
+        // Re-entrant call: decorator should NOT run
+        decorated.set(false);
+        RequestTrace outerTrace = outerRequest.getTrace();
+        ArtifactDescriptorRequest innerRequest =
+                new ArtifactDescriptorRequest(new DefaultArtifact("g:b:2.0"), 
Collections.emptyList(), null);
+        innerRequest.setTrace(RequestTrace.newChild(outerTrace, 
"ModelResolver"));
+        decoratingSystem.readArtifactDescriptor(session, innerRequest);
+        assertFalse(decorated.get(), "Re-entrant readArtifactDescriptor should 
skip decoration");
+    }
+
+    @Test
+    void independentCallsEachValidate() throws Exception {
+        // Two independent calls (no shared trace) should each validate
+        VersionRangeRequest request1 =
+                new VersionRangeRequest(new DefaultArtifact("g:a:1.0"), 
Collections.emptyList(), null);
+        VersionRangeRequest request2 =
+                new VersionRangeRequest(new DefaultArtifact("g:b:2.0"), 
Collections.emptyList(), null);
+
+        system.resolveVersionRange(session, request1);
+        system.resolveVersionRange(session, request2);
+
+        assertEquals(2, validationCount.get(), "Two independent calls should 
each validate");
+    }
+}

Reply via email to