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");
+ }
+}