provides: "Integration tests for all search filter combinations"
min_lines: 100
key_links:
- from: "SearchController"
to: "SearchService"
via: "constructor injection, delegates search()"
pattern: "searchService\\.search"
- from: "SearchService"
to: "ClickHouseSearchEngine"
via: "SearchEngine interface"
pattern: "engine\\.search"
- from: "ClickHouseSearchEngine"
to: "route_executions table"
via: "dynamic SQL with parameterized WHERE"
pattern: "SELECT.*FROM route_executions.*WHERE"
- from: "DetailController"
to: "DetailService"
via: "constructor injection"
pattern: "detailService\\.getDetail"
- from: "DetailService"
to: "ClickHouseExecutionRepository"
via: "findRawById for flat data, then reconstructTree"
pattern: "findRawById|reconstructTree"
---
<objective>
Implement the search endpoints (GET and POST), the ClickHouse search engine with dynamic SQL, the transaction detail endpoint with nested tree reconstruction, and comprehensive integration tests.
Purpose: This is the core query capability of Phase 2 — users need to find transactions by any combination of filters and drill into execution details. The search engine abstraction allows future swap to OpenSearch.
- Test searchByStatus: Insert 3 executions (COMPLETED, FAILED, RUNNING). GET /api/v1/search/executions?status=FAILED returns only the FAILED execution. Response has envelope: {"data":[...],"total":1,"offset":0,"limit":50}
- Test searchByTimeRange: Insert executions at different times. Filter by timeFrom/timeTo returns only those in range
- Test searchByDuration: Insert executions with different durations. Filter by durationMin=100&durationMax=500 returns only matching
- Test searchByCorrelationId: Insert executions with different correlationIds. Filter returns only matching
- Test fullTextSearchGlobal: Insert execution with error_message="NullPointerException in OrderService". Search text=NullPointerException returns it. Search text=nonexistent returns empty
- Test fullTextSearchInBody: Insert execution with exchange body containing "customer-123". textInBody=customer-123 returns it
- Test fullTextSearchInHeaders: Insert execution with exchange headers containing "Content-Type". textInHeaders=Content-Type returns it
- Test fullTextSearchInErrors: Insert execution with error_stacktrace containing "com.example.MyException". textInErrors=MyException returns it
- Test combinedFilters: status=FAILED + text=NullPointer returns only failed executions with that error
- Test postAdvancedSearch: POST /api/v1/search/executions with JSON body containing all filters returns correct results
- Test pagination: Insert 10 executions. Request with offset=2&limit=3 returns 3 items, total=10, offset=2
- Test emptyResults: Search with no matches returns {"data":[],"total":0,"offset":0,"limit":50}
- Implements SearchEngine interface from core module.
- Constructor takes JdbcTemplate.
-`search(SearchRequest)` method:
- Build dynamic WHERE clause from non-null SearchRequest fields using ArrayList<String> conditions and ArrayList<Object> params.
- status: `"status = ?"` with `req.status()`
- timeFrom: `"start_time >= ?"` with `Timestamp.from(req.timeFrom())`
- timeTo: `"start_time <= ?"` with `Timestamp.from(req.timeTo())`
- durationMin: `"duration_ms >= ?"` with `req.durationMin()`
- durationMax: `"duration_ms <= ?"` with `req.durationMax()`
- correlationId: `"correlation_id = ?"` with `req.correlationId()`
- text (global): `"(error_message LIKE ? OR error_stacktrace LIKE ? OR exchange_bodies LIKE ? OR exchange_headers LIKE ?)"` with `"%" + escapeLike(req.text()) + "%"` repeated 4 times
- textInBody: `"exchange_bodies LIKE ?"` with escaped pattern
- textInHeaders: `"exchange_headers LIKE ?"` with escaped pattern
- textInErrors: `"(error_message LIKE ? OR error_stacktrace LIKE ?)"` with escaped pattern repeated 2 times
- Combine conditions with AND. If empty, no WHERE clause.
- Count query: `SELECT count() FROM route_executions` + where
- Data query: `SELECT execution_id, route_id, agent_id, status, start_time, end_time, duration_ms, correlation_id, error_message, diagram_content_hash FROM route_executions` + where + ` ORDER BY start_time DESC LIMIT ? OFFSET ?`
- Map rows to ExecutionSummary records. Use `rs.getTimestamp("start_time").toInstant()` for Instant fields.
- Return SearchResult with data, total from count query, offset, limit.
-`escapeLike(String)` utility: escape `%`, `_`, `\` characters in user input to prevent LIKE injection. Replace `\` with `\\`, `%` with `\%`, `_` with `\_`.
-`count(SearchRequest)` method: same WHERE building, just count query.
- Use TestRestTemplate (auto-configured by @SpringBootTest with RANDOM_PORT).
- Seed test data: Insert multiple RouteExecution objects with varying statuses, times, durations, correlationIds, error messages, and exchange snapshot data. Use the POST /api/v1/data/executions endpoint to insert, then wait for flush (Awaitility).
- Write tests for each behavior listed above. Use GET for basic filter tests, POST for advanced/combined filter tests.
- All requests include X-Cameleer-Protocol-Version: 1 header per ProtocolVersionInterceptor requirement.
- Assert response structure matches the envelope format.
<done>All search filter types work independently and in combination, response envelope has correct format, pagination works correctly, full-text search finds matches in all text fields, LIKE patterns are properly escaped</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: DetailController, tree reconstruction, exchange snapshot endpoint, and integration tests</name>
- Unit test: reconstructTree with [root, child, grandchild], depths=[0,1,2], parents=[-1,0,1] produces single root with one child that has one grandchild
- Unit test: reconstructTree with [A, B, C], depths=[0,0,0], parents=[-1,-1,-1] produces 3 roots (no nesting)
- Unit test: reconstructTree with [parent, child1, child2, grandchild], depths=[0,1,1,2], parents=[-1,0,0,2] produces parent with 2 children, second child has one grandchild
- Unit test: reconstructTree with empty arrays produces empty list
- Integration test: GET /api/v1/executions/{id} returns ExecutionDetail with nested processors tree matching the ingested structure
- Integration test: detail response includes diagramContentHash field (can be empty string if not set)
- Integration test: GET /api/v1/executions/{nonexistent-id} returns 404
- Integration test: GET /api/v1/executions/{id}/processors/{index}/snapshot returns exchange snapshot data for that processor
</behavior>
<action>
1. Create `TreeReconstructionTest` in core module test directory:
- Pure unit test (no Spring context needed).
- Test DetailService.reconstructTree (make it a static method or package-accessible for testing).
- Cover cases: single root, linear chain, wide tree (multiple roots), branching tree, empty arrays.
- Verify correct parent-child wiring and that ProcessorNode.children() lists are correctly populated.
2. Extend `ClickHouseExecutionRepository` with query methods:
- Add `findRawById(String executionId)` method that queries all columns from route_executions WHERE execution_id = ?. Return Optional<RawExecutionRow> (use the record created in Plan 01 or create it here if needed). The RawExecutionRow should contain ALL columns including the parallel arrays for processors.
- Add `findProcessorSnapshot(String executionId, int processorIndex)` method: queries processor_input_bodies[index+1], processor_output_bodies[index+1], processor_input_headers[index+1], processor_output_headers[index+1] for the given execution. Returns a DTO with inputBody, outputBody, inputHeaders, outputHeaders. ClickHouse arrays are 1-indexed in SQL, so add 1 to the Java 0-based index.
-`GET /api/v1/executions/{executionId}`: call detailService.getDetail(executionId). If empty, return 404. Otherwise return 200 with ExecutionDetail JSON. The processors field is a nested tree of ProcessorNode objects.
-`GET /api/v1/executions/{executionId}/processors/{index}/snapshot`: call repository's findProcessorSnapshot. If execution not found or index out of bounds, return 404. Return JSON with inputBody, outputBody, inputHeaders, outputHeaders. Per user decision: exchange snapshot data fetched separately per processor, not inlined in detail response.
- Seed a RouteExecution with a 3-level processor tree (root with 2 children, one child has a grandchild). Give processors exchange snapshot data (bodies, headers).
- Also seed a RouteGraph diagram for the route to test diagram hash linking.
- POST to ingestion endpoints, wait for flush.
- Test GET /api/v1/executions/{id}: verify response has nested processors tree with correct depths. Root should have 2 children, one child should have 1 grandchild. Verify diagramContentHash is present.
- Test GET /api/v1/executions/{id}/processors/0/snapshot: returns snapshot data for root processor.
- Test GET /api/v1/executions/{nonexistent}/: returns 404.
- Test GET /api/v1/executions/{id}/processors/999/snapshot: returns 404 for out-of-bounds index.
<done>Tree reconstruction correctly rebuilds nested processor trees from flat arrays, detail endpoint returns nested tree with all fields, snapshot endpoint returns per-processor exchange data, diagram hash included in detail response, all tests pass</done>