rfellows commented on code in PR #11263:
URL: https://github.com/apache/nifi/pull/11263#discussion_r3284016409
##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -3950,6 +3950,26 @@ public Set<ControllerServiceEntity>
getConnectorControllerServices(final String
.collect(Collectors.toSet());
}
+ @Override
+ public ParameterContextEntity getConnectorParameterContext(final String
connectorId, final String processGroupId) {
+ final ConnectorNode connectorNode =
connectorDAO.getConnector(connectorId, ConnectorSyncMode.LOCAL_ONLY);
+ final ProcessGroup managedProcessGroup =
connectorNode.getActiveFlowContext().getManagedProcessGroup();
+ final ProcessGroup targetProcessGroup =
managedProcessGroup.findProcessGroup(processGroupId);
+ if (targetProcessGroup == null) {
+ throw new ResourceNotFoundException("Process Group with ID " +
processGroupId + " was not found within Connector " + connectorId);
+ }
+
+ final ParameterContext parameterContext =
targetProcessGroup.getParameterContext();
+ if (parameterContext == null) {
+ return null;
+ }
+
+ // Connector-managed parameter contexts (and any contexts they inherit
from) are not registered with the
+ // global flow's ParameterContextManager. The DTO factory now resolves
a parameter's source context by
+ // walking the in-memory inheritance graph on the supplied context, so
an empty lookup is sufficient here.
Review Comment:
Nit: "now resolves" is patch-relative — it describes the code relative to
what it used to do rather than its permanent contract. Comments should explain
the invariant, not the change (those belong in the commit message).
Suggested rewrite:
```java
// Connector-managed parameter contexts are not registered with the global
ParameterContextManager,
// so the DAO-backed lookup would fail for any inherited parameter. The
in-memory inheritance graph
// reachable from the supplied context is sufficient to resolve parameter
source contexts for
// connector-managed flows, making an empty lookup safe here.
```
##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java:
##########
@@ -1631,6 +1630,51 @@ public ParameterDTO createParameterDto(final
ParameterContext parameterContext,
return dto;
}
+ /**
+ * Resolves the {@link ParameterContext} where the given parameter was
originally defined.
+ *
+ * <p>For parameters declared directly on the current context (or whose
source id matches the current
+ * context's identifier), the current context is returned without
consulting any external lookup. For
+ * inherited parameters, the source context is found by walking the
in-memory inheritance graph reachable
+ * from the current context via {@link
ParameterContext#getInheritedParameterContexts()}. Only if the
+ * source context is not reachable on that graph (which is expected only
for legacy callers that pass a
+ * registry-backed lookup) does the provided {@link
ParameterContextLookup} get consulted as a fallback.</p>
+ */
+ private ParameterContext resolveContainingParameterContext(final
ParameterContext parameterContext, final Parameter parameter,
+ final
ParameterContextLookup parameterContextLookup) {
+ final String sourceId = parameter.getParameterContextId();
+ if (sourceId == null ||
sourceId.equals(parameterContext.getIdentifier())) {
+ return parameterContext;
+ }
+
+ final ParameterContext fromGraph =
findInheritedParameterContext(parameterContext, sourceId, new HashSet<>());
+ if (fromGraph != null) {
+ return fromGraph;
+ }
+
+ return parameterContextLookup.getParameterContext(sourceId);
Review Comment:
`resolveContainingParameterContext` can return `null` here when both the
in-memory graph walk and the lookup come up empty. The connector path in
`StandardNiFiServiceFacade` passes `ParameterContextLookup.EMPTY`, whose
`getParameterContext` unconditionally returns `null`. If a parameter carries a
`parameterContextId` that is not reachable in the in-memory graph (e.g. data
inconsistency after a reconciliation), this method returns `null`, and the
caller on line 1625 immediately dereferences it with `.getIdentifier()`,
producing an NPE.
Suggested fix — guard the return value:
```java
final ParameterContext result =
parameterContextLookup.getParameterContext(sourceId);
return result != null ? result : parameterContext;
```
Falling back to `parameterContext` (treating the parameter as locally
defined) is conservative and safe. Alternatively, throw an
`IllegalStateException` with a descriptive message so the inconsistency
surfaces clearly rather than crashing silently.
##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java:
##########
@@ -752,4 +761,199 @@ void testCreateAssetReferenceDtoWhenContentFileMissing() {
assertNotNull(dto.getMissingContent());
assertTrue(dto.getMissingContent());
}
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextWhenParameterContextIdIsNull() {
+ final String contextId = "context-1";
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertEquals("param-name", dto.getName());
+ assertEquals("param-value", dto.getValue());
+ assertFalse(dto.getInherited());
+
+ final ParameterContextReferenceEntity reference =
dto.getParameterContext();
+ assertNotNull(reference);
+ assertEquals(contextId, reference.getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextWhenParameterContextIdMatchesCurrent()
{
+ final String contextId = "context-1";
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(contextId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertFalse(dto.getInherited());
+ assertEquals(contextId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void testCreateParameterDtoResolvesSourceContextFromInheritedGraph() {
+ final String childId = "context-child";
+ final String parentId = "context-parent";
+
+ final ParameterContext parentContext =
createMockParameterContext(parentId, "parent", Collections.emptyList());
+ final ParameterContext childContext =
createMockParameterContext(childId, "child", List.of(parentContext));
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(parentId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto = dtoFactory.createParameterDto(childContext,
parameter, mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(parentId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextFromTransitiveInheritedGraph() {
+ final String childId = "context-child";
+ final String parentId = "context-parent";
+ final String grandparentId = "context-grandparent";
+
+ final ParameterContext grandparentContext =
createMockParameterContext(grandparentId, "grandparent",
Collections.emptyList());
+ final ParameterContext parentContext =
createMockParameterContext(parentId, "parent", List.of(grandparentContext));
+ final ParameterContext childContext =
createMockParameterContext(childId, "child", List.of(parentContext));
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(grandparentId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto = dtoFactory.createParameterDto(childContext,
parameter, mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(grandparentId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoFallsBackToLookupWhenSourceNotReachableInGraph() {
+ final String contextId = "context-1";
+ final String externalId = "context-external";
+
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+ final ParameterContext externalContext =
createMockParameterContext(externalId, "context-external-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(externalId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
when(lookup.getParameterContext(externalId)).thenReturn(externalContext);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(externalId, dto.getParameterContext().getId());
+
+ verify(lookup).getParameterContext(externalId);
+ }
Review Comment:
This test covers the fallback path when the lookup returns a valid context,
but there is no companion test for the case where both the graph walk **and**
the lookup return `null` — i.e. `ParameterContextLookup.EMPTY`, which is
exactly what the connector path passes. Without it, the behavior of
`resolveContainingParameterContext` when both paths come up empty is
unspecified by the test suite, and the NPE at the call site (line 1625) would
go undetected.
Suggested addition alongside the null-guard fix:
```java
@Test
void
testCreateParameterDtoFallsBackToCurrentContextWhenSourceNotReachableInGraphAndLookupIsEmpty()
{
final String contextId = "context-1";
final String externalId = "context-external";
final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
final Parameter parameter = new Parameter.Builder()
.name("param-name")
.value("param-value")
.parameterContextId(externalId)
.build();
final DtoFactory dtoFactory = newDtoFactoryForParameters();
final ParameterDTO dto = dtoFactory.createParameterDto(
parameterContext, parameter, mock(RevisionManager.class),
ParameterContextLookup.EMPTY);
// Source context not found in graph or lookup; falls back to treating
as local
assertFalse(dto.getInherited());
assertEquals(contextId, dto.getParameterContext().getId());
}
```
(Exact assertion depends on the chosen fallback behavior, but the test is
needed either way to pin the contract.)
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connector-canvas/connector-canvas.effects.spec.ts:
##########
@@ -1080,4 +1159,244 @@ describe('ConnectorCanvasEffects', () => {
expect(emitted).toEqual([reloadConnectorFlow()]);
});
});
+
+ describe('loadConnectorParameterContextOnLoadSuccess$', () => {
+ function flowSuccessAction(connectorId: string, processGroupId: string
| null) {
+ return loadConnectorFlowSuccess({
+ connectorId,
+ processGroupId,
+ parentProcessGroupId: null,
+ breadcrumb: null,
+ labels: [],
+ funnels: [],
+ inputPorts: [],
+ outputPorts: [],
+ remoteProcessGroups: [],
+ processGroups: [],
+ processors: [],
+ connections: []
+ });
+ }
+
+ function controllerServicesSuccessAction(connectorId: string,
processGroupId: string) {
+ return loadConnectorControllerServicesSuccess({
+ response: {
+ connectorId,
+ processGroupId,
+ controllerServices: [],
+ breadcrumb: null,
+ loadedTimestamp: '2024-01-01T00:00:00.000Z'
+ }
+ });
+ }
+
+ it('should dispatch loadConnectorParameterContext scoped to
CONNECTOR_CANVAS after flow load success', async () => {
+ const { effects, actions$ } = await setup();
+ actions$(of(flowSuccessAction('connector-123', 'pg-abc')));
+
+ const result = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$);
+ expect(result).toEqual(
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-abc',
+ errorContext: ErrorContextKey.CONNECTOR_CANVAS
+ })
+ );
+ });
+
+ it('should dispatch loadConnectorParameterContext scoped to
CONTROLLER_SERVICES after controller services load success (deep link)', async
() => {
+ const { effects, actions$ } = await setup();
+ actions$(of(controllerServicesSuccessAction('connector-123',
'pg-abc')));
+
+ const result = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$);
+ expect(result).toEqual(
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-abc',
+ errorContext: ErrorContextKey.CONTROLLER_SERVICES
+ })
+ );
+ });
+
+ it('should not dispatch when processGroupId is null', async () => {
+ const { effects, actions$ } = await setup();
+ actions$(of(flowSuccessAction('connector-123', null)));
+
+ const emitted = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$.pipe(toArray()));
+
+ expect(emitted).toEqual([]);
+ });
+
+ it('should dispatch only once when polling refires
loadConnectorFlowSuccess for the same process group', async () => {
+ const { effects, actions$ } = await setup();
+ actions$(
+ of(
+ flowSuccessAction('connector-123', 'pg-abc'),
+ flowSuccessAction('connector-123', 'pg-abc'),
+ flowSuccessAction('connector-123', 'pg-abc')
+ )
+ );
+
+ const emitted = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$.pipe(toArray()));
+
+ expect(emitted).toEqual([
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-abc',
+ errorContext: ErrorContextKey.CONNECTOR_CANVAS
+ })
+ ]);
+ });
+
+ it('should dedupe canvas → controller-services transition within the
same process group', async () => {
+ const { effects, actions$ } = await setup();
+ actions$(
+ of(
+ flowSuccessAction('connector-123', 'pg-abc'),
+ controllerServicesSuccessAction('connector-123', 'pg-abc')
+ )
+ );
+
+ const emitted = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$.pipe(toArray()));
+
+ expect(emitted).toEqual([
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-abc',
+ errorContext: ErrorContextKey.CONNECTOR_CANVAS
+ })
+ ]);
+ });
+
+ it('should dispatch again when the process group changes', async () =>
{
+ const { effects, actions$ } = await setup();
+ actions$(
+ of(
+ flowSuccessAction('connector-123', 'pg-abc'),
+ flowSuccessAction('connector-123', 'pg-abc'),
+ flowSuccessAction('connector-123', 'pg-xyz')
+ )
+ );
+
+ const emitted = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$.pipe(toArray()));
+
+ expect(emitted).toEqual([
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-abc',
+ errorContext: ErrorContextKey.CONNECTOR_CANVAS
+ }),
+ loadConnectorParameterContext({
+ connectorId: 'connector-123',
+ processGroupId: 'pg-xyz',
+ errorContext: ErrorContextKey.CONNECTOR_CANVAS
+ })
+ ]);
+ });
Review Comment:
The dedup key in `loadConnectorParameterContextOnLoadSuccess$` is
`(connectorId, processGroupId)` — both fields must change (or one must change)
to pass the `distinctUntilChanged` predicate. This test covers the PG-changes
case, but the symmetric case — same PG id, different connector — is missing.
Without it, someone could simplify the predicate to check only `processGroupId`
and no test would fail, even though navigating between two connectors with the
same root PG id would silently skip refetching the parameter context.
Suggested addition:
```typescript
it('should dispatch again when the connector changes even if the process
group id is the same', async () => {
const { effects, actions$ } = await setup();
actions$(
of(
flowSuccessAction('connector-123', 'pg-abc'),
flowSuccessAction('connector-456', 'pg-abc')
)
);
const emitted = await
firstValueFrom(effects.loadConnectorParameterContextOnLoadSuccess$.pipe(toArray()));
expect(emitted).toEqual([
loadConnectorParameterContext({
connectorId: 'connector-123',
processGroupId: 'pg-abc',
errorContext: ErrorContextKey.CONNECTOR_CANVAS
}),
loadConnectorParameterContext({
connectorId: 'connector-456',
processGroupId: 'pg-abc',
errorContext: ErrorContextKey.CONNECTOR_CANVAS
})
]);
});
```
##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java:
##########
@@ -752,4 +761,199 @@ void testCreateAssetReferenceDtoWhenContentFileMissing() {
assertNotNull(dto.getMissingContent());
assertTrue(dto.getMissingContent());
}
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextWhenParameterContextIdIsNull() {
+ final String contextId = "context-1";
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertEquals("param-name", dto.getName());
+ assertEquals("param-value", dto.getValue());
+ assertFalse(dto.getInherited());
+
+ final ParameterContextReferenceEntity reference =
dto.getParameterContext();
+ assertNotNull(reference);
+ assertEquals(contextId, reference.getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextWhenParameterContextIdMatchesCurrent()
{
+ final String contextId = "context-1";
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(contextId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertFalse(dto.getInherited());
+ assertEquals(contextId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void testCreateParameterDtoResolvesSourceContextFromInheritedGraph() {
+ final String childId = "context-child";
+ final String parentId = "context-parent";
+
+ final ParameterContext parentContext =
createMockParameterContext(parentId, "parent", Collections.emptyList());
+ final ParameterContext childContext =
createMockParameterContext(childId, "child", List.of(parentContext));
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(parentId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto = dtoFactory.createParameterDto(childContext,
parameter, mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(parentId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoResolvesSourceContextFromTransitiveInheritedGraph() {
+ final String childId = "context-child";
+ final String parentId = "context-parent";
+ final String grandparentId = "context-grandparent";
+
+ final ParameterContext grandparentContext =
createMockParameterContext(grandparentId, "grandparent",
Collections.emptyList());
+ final ParameterContext parentContext =
createMockParameterContext(parentId, "parent", List.of(grandparentContext));
+ final ParameterContext childContext =
createMockParameterContext(childId, "child", List.of(parentContext));
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(grandparentId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto = dtoFactory.createParameterDto(childContext,
parameter, mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(grandparentId, dto.getParameterContext().getId());
+
+ verify(lookup, never()).getParameterContext(anyString());
+ }
+
+ @Test
+ void
testCreateParameterDtoFallsBackToLookupWhenSourceNotReachableInGraph() {
+ final String contextId = "context-1";
+ final String externalId = "context-external";
+
+ final ParameterContext parameterContext =
createMockParameterContext(contextId, "context-1-name",
Collections.emptyList());
+ final ParameterContext externalContext =
createMockParameterContext(externalId, "context-external-name",
Collections.emptyList());
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(externalId)
+ .build();
+
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
when(lookup.getParameterContext(externalId)).thenReturn(externalContext);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto =
dtoFactory.createParameterDto(parameterContext, parameter,
mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(externalId, dto.getParameterContext().getId());
+
+ verify(lookup).getParameterContext(externalId);
+ }
+
+ @Test
+ void testCreateParameterDtoInheritanceGraphHandlesCycles() {
+ final String childId = "context-child";
+ final String parentId = "context-parent";
+ final String missingId = "context-missing";
+
+ final ParameterContext parentContext = mock(ParameterContext.class);
+ final ParameterContext childContext = mock(ParameterContext.class);
+ configureBaseParameterContext(childContext, childId, "child");
+ configureBaseParameterContext(parentContext, parentId, "parent");
+
when(childContext.getInheritedParameterContexts()).thenReturn(List.of(parentContext));
+
when(parentContext.getInheritedParameterContexts()).thenReturn(List.of(childContext));
+
+ final Parameter parameter = new Parameter.Builder()
+ .name("param-name")
+ .value("param-value")
+ .parameterContextId(missingId)
+ .build();
+
+ final ParameterContext fallbackContext =
createMockParameterContext(missingId, "missing", Collections.emptyList());
+ final ParameterContextLookup lookup =
mock(ParameterContextLookup.class);
+
when(lookup.getParameterContext(missingId)).thenReturn(fallbackContext);
+
+ final DtoFactory dtoFactory = newDtoFactoryForParameters();
+ final ParameterDTO dto = dtoFactory.createParameterDto(childContext,
parameter, mock(RevisionManager.class), lookup);
+
+ assertTrue(dto.getInherited());
+ assertEquals(missingId, dto.getParameterContext().getId());
+
+ verify(lookup).getParameterContext(missingId);
+ }
Review Comment:
The cycle test covers mutual recursion (child → parent → child), but the
`visited` set in `findInheritedParameterContext` also guards against redundant
traversal in diamond inheritance (A inherits B and C, both inherit D). That
second behavior is untested. Without it, a future change that accidentally
removes or weakens the visited-set guard would not be caught.
Suggested addition:
```java
@Test
void
testCreateParameterDtoResolvesSourceContextFromDiamondInheritanceGraph() {
final String contextAId = "context-a";
final String contextBId = "context-b";
final String contextCId = "context-c";
final String contextDId = "context-d";
final ParameterContext contextD = createMockParameterContext(contextDId,
"context-d-name", Collections.emptyList());
final ParameterContext contextB = createMockParameterContext(contextBId,
"context-b-name", List.of(contextD));
final ParameterContext contextC = createMockParameterContext(contextCId,
"context-c-name", List.of(contextD));
final ParameterContext contextA = createMockParameterContext(contextAId,
"context-a-name", List.of(contextB, contextC));
final Parameter parameter = new Parameter.Builder()
.name("param-name")
.value("param-value")
.parameterContextId(contextDId)
.build();
final DtoFactory dtoFactory = newDtoFactoryForParameters();
final ParameterDTO dto = dtoFactory.createParameterDto(
contextA, parameter, mock(RevisionManager.class),
ParameterContextLookup.EMPTY);
assertTrue(dto.getInherited());
assertEquals(contextDId, dto.getParameterContext().getId());
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]