feat(02-03): detail controller, tree reconstruction, processor snapshot endpoint

- Implement findRawById and findProcessorSnapshot in ClickHouseExecutionRepository
- DetailController with GET /executions/{id} returning nested processor tree
- GET /executions/{id}/processors/{index}/snapshot for per-processor exchange data
- 5 unit tests for tree reconstruction (linear, branching, multiple roots, empty)
- 6 integration tests for detail endpoint, snapshot, and 404 handling
- Added assertj and mockito test dependencies to core module

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-11 16:29:53 +01:00
parent 82a190c8e2
commit 0615a9851d
5 changed files with 626 additions and 1 deletions

View File

@@ -0,0 +1,140 @@
package com.cameleer3.server.core.detail;
import com.cameleer3.server.core.storage.ExecutionRepository;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link DetailService#reconstructTree} logic.
* <p>
* Verifies correct parent-child wiring from flat parallel arrays.
*/
class TreeReconstructionTest {
private final DetailService detailService = new DetailService(mock(ExecutionRepository.class));
private static final Instant NOW = Instant.parse("2026-03-10T10:00:00Z");
@Test
void linearChain_rootChildGrandchild() {
// [root, child, grandchild], depths=[0,1,2], parents=[-1,0,1]
List<ProcessorNode> roots = detailService.reconstructTree(
new String[]{"root", "child", "grandchild"},
new String[]{"log", "bean", "to"},
new String[]{"COMPLETED", "COMPLETED", "COMPLETED"},
new Instant[]{NOW, NOW, NOW},
new Instant[]{NOW, NOW, NOW},
new long[]{10, 20, 30},
new String[]{"n1", "n2", "n3"},
new String[]{"", "", ""},
new String[]{"", "", ""},
new int[]{0, 1, 2},
new int[]{-1, 0, 1}
);
assertThat(roots).hasSize(1);
ProcessorNode root = roots.get(0);
assertThat(root.getProcessorId()).isEqualTo("root");
assertThat(root.getChildren()).hasSize(1);
ProcessorNode child = root.getChildren().get(0);
assertThat(child.getProcessorId()).isEqualTo("child");
assertThat(child.getChildren()).hasSize(1);
ProcessorNode grandchild = child.getChildren().get(0);
assertThat(grandchild.getProcessorId()).isEqualTo("grandchild");
assertThat(grandchild.getChildren()).isEmpty();
}
@Test
void multipleRoots_noNesting() {
// [A, B, C], depths=[0,0,0], parents=[-1,-1,-1]
List<ProcessorNode> roots = detailService.reconstructTree(
new String[]{"A", "B", "C"},
new String[]{"log", "log", "log"},
new String[]{"COMPLETED", "COMPLETED", "COMPLETED"},
new Instant[]{NOW, NOW, NOW},
new Instant[]{NOW, NOW, NOW},
new long[]{10, 20, 30},
new String[]{"n1", "n2", "n3"},
new String[]{"", "", ""},
new String[]{"", "", ""},
new int[]{0, 0, 0},
new int[]{-1, -1, -1}
);
assertThat(roots).hasSize(3);
assertThat(roots.get(0).getProcessorId()).isEqualTo("A");
assertThat(roots.get(1).getProcessorId()).isEqualTo("B");
assertThat(roots.get(2).getProcessorId()).isEqualTo("C");
roots.forEach(r -> assertThat(r.getChildren()).isEmpty());
}
@Test
void branchingTree_parentWithTwoChildren_secondChildHasGrandchild() {
// [parent, child1, child2, grandchild], depths=[0,1,1,2], parents=[-1,0,0,2]
List<ProcessorNode> roots = detailService.reconstructTree(
new String[]{"parent", "child1", "child2", "grandchild"},
new String[]{"split", "log", "bean", "to"},
new String[]{"COMPLETED", "COMPLETED", "COMPLETED", "COMPLETED"},
new Instant[]{NOW, NOW, NOW, NOW},
new Instant[]{NOW, NOW, NOW, NOW},
new long[]{100, 20, 30, 5},
new String[]{"n1", "n2", "n3", "n4"},
new String[]{"", "", "", ""},
new String[]{"", "", "", ""},
new int[]{0, 1, 1, 2},
new int[]{-1, 0, 0, 2}
);
assertThat(roots).hasSize(1);
ProcessorNode parent = roots.get(0);
assertThat(parent.getProcessorId()).isEqualTo("parent");
assertThat(parent.getChildren()).hasSize(2);
ProcessorNode child1 = parent.getChildren().get(0);
assertThat(child1.getProcessorId()).isEqualTo("child1");
assertThat(child1.getChildren()).isEmpty();
ProcessorNode child2 = parent.getChildren().get(1);
assertThat(child2.getProcessorId()).isEqualTo("child2");
assertThat(child2.getChildren()).hasSize(1);
ProcessorNode grandchild = child2.getChildren().get(0);
assertThat(grandchild.getProcessorId()).isEqualTo("grandchild");
assertThat(grandchild.getChildren()).isEmpty();
}
@Test
void emptyArrays_producesEmptyList() {
List<ProcessorNode> roots = detailService.reconstructTree(
new String[]{},
new String[]{},
new String[]{},
new Instant[]{},
new Instant[]{},
new long[]{},
new String[]{},
new String[]{},
new String[]{},
new int[]{},
new int[]{}
);
assertThat(roots).isEmpty();
}
@Test
void nullArrays_producesEmptyList() {
List<ProcessorNode> roots = detailService.reconstructTree(
null, null, null, null, null, null, null, null, null, null, null
);
assertThat(roots).isEmpty();
}
}