diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ExecutionControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ExecutionControllerIT.java index 7f7fb26c..7c818119 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ExecutionControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ExecutionControllerIT.java @@ -2,12 +2,15 @@ package com.cameleer.server.app.controller; import com.cameleer.server.app.AbstractPostgresIT; import com.cameleer.server.app.TestSecurityHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,6 +18,11 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +/** + * POST /api/v1/data/executions is owned by ChunkIngestionController (the + * legacy ExecutionController is @ConditionalOnMissingBean(ChunkAccumulator) + * and never binds). All payloads here are ExecutionChunk envelopes. + */ class ExecutionControllerIT extends AbstractPostgresIT { @Autowired @@ -23,27 +31,33 @@ class ExecutionControllerIT extends AbstractPostgresIT { @Autowired private TestSecurityHelper securityHelper; + private final ObjectMapper objectMapper = new ObjectMapper(); + private HttpHeaders authHeaders; + private HttpHeaders viewerHeaders; @BeforeEach void setUp() { String jwt = securityHelper.registerTestAgent("test-agent-execution-it"); authHeaders = securityHelper.authHeaders(jwt); + viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken()); } @Test void postSingleExecution_returns202() { String json = """ { - "routeId": "route-1", "exchangeId": "exchange-1", + "applicationId": "test-group", + "instanceId": "test-agent-execution-it", + "routeId": "route-1", "correlationId": "corr-1", "status": "COMPLETED", "startTime": "2026-03-11T10:00:00Z", "endTime": "2026-03-11T10:00:01Z", "durationMs": 1000, - "errorMessage": "", - "errorStackTrace": "", + "chunkSeq": 0, + "final": true, "processors": [] } """; @@ -60,22 +74,30 @@ class ExecutionControllerIT extends AbstractPostgresIT { void postArrayOfExecutions_returns202() { String json = """ [{ - "routeId": "route-2", "exchangeId": "exchange-2", + "applicationId": "test-group", + "instanceId": "test-agent-execution-it", + "routeId": "route-2", "status": "COMPLETED", "startTime": "2026-03-11T10:00:00Z", "endTime": "2026-03-11T10:00:01Z", "durationMs": 1000, + "chunkSeq": 0, + "final": true, "processors": [] }, { - "routeId": "route-3", "exchangeId": "exchange-3", + "applicationId": "test-group", + "instanceId": "test-agent-execution-it", + "routeId": "route-3", "status": "FAILED", "startTime": "2026-03-11T10:00:00Z", "endTime": "2026-03-11T10:00:02Z", "durationMs": 2000, "errorMessage": "Something went wrong", + "chunkSeq": 0, + "final": true, "processors": [] }] """; @@ -92,13 +114,17 @@ class ExecutionControllerIT extends AbstractPostgresIT { void postExecution_dataAppearsAfterFlush() { String json = """ { - "routeId": "flush-test-route", "exchangeId": "flush-exchange-1", + "applicationId": "test-group", + "instanceId": "test-agent-execution-it", + "routeId": "flush-test-route", "correlationId": "flush-corr-1", "status": "COMPLETED", "startTime": "2026-03-11T10:00:00Z", "endTime": "2026-03-11T10:00:01Z", "durationMs": 1000, + "chunkSeq": 0, + "final": true, "processors": [] } """; @@ -108,11 +134,17 @@ class ExecutionControllerIT extends AbstractPostgresIT { new HttpEntity<>(json, authHeaders), String.class); - await().atMost(10, SECONDS).untilAsserted(() -> { - Integer count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM executions WHERE route_id = 'flush-test-route'", - Integer.class); - assertThat(count).isGreaterThanOrEqualTo(1); + // Executions live in ClickHouse; drive the visibility check through + // the REST search API (env-scoped), never through raw SQL. + await().atMost(15, SECONDS).untilAsserted(() -> { + ResponseEntity r = restTemplate.exchange( + "/api/v1/environments/default/executions?correlationId=flush-corr-1", + HttpMethod.GET, + new HttpEntity<>(viewerHeaders), + String.class); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode body = objectMapper.readTree(r.getBody()); + assertThat(body.get("total").asLong()).isGreaterThanOrEqualTo(1); }); } @@ -120,11 +152,15 @@ class ExecutionControllerIT extends AbstractPostgresIT { void postExecution_unknownFieldsAccepted() { String json = """ { - "routeId": "route-unk", "exchangeId": "exchange-unk", + "applicationId": "test-group", + "instanceId": "test-agent-execution-it", + "routeId": "route-unk", "status": "COMPLETED", "startTime": "2026-03-11T10:00:00Z", "durationMs": 500, + "chunkSeq": 0, + "final": true, "unknownField": "should-be-ignored", "anotherUnknown": 42, "processors": [] diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/MetricsControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/MetricsControllerIT.java index c93196d4..977a2a5a 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/MetricsControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/controller/MetricsControllerIT.java @@ -2,12 +2,15 @@ package com.cameleer.server.app.controller; import com.cameleer.server.app.AbstractPostgresIT; import com.cameleer.server.app.TestSecurityHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -23,12 +26,18 @@ class MetricsControllerIT extends AbstractPostgresIT { @Autowired private TestSecurityHelper securityHelper; + private final ObjectMapper objectMapper = new ObjectMapper(); + private HttpHeaders authHeaders; + private HttpHeaders viewerHeaders; + private String agentId; @BeforeEach void setUp() { - String jwt = securityHelper.registerTestAgent("test-agent-metrics-it"); + agentId = "test-agent-metrics-it"; + String jwt = securityHelper.registerTestAgent(agentId); authHeaders = securityHelper.authHeaders(jwt); + viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken()); } @Test @@ -53,26 +62,43 @@ class MetricsControllerIT extends AbstractPostgresIT { @Test void postMetrics_dataAppearsAfterFlush() { + // Post fresh now-stamped metrics so the default 1h lookback window of + // GET /agents/{id}/metrics sees them deterministically. + java.time.Instant now = java.time.Instant.now(); String json = """ [{ - "instanceId": "agent-flush-test", - "collectedAt": "2026-03-11T10:00:00Z", + "instanceId": "%s", + "collectedAt": "%s", "metricName": "memory.used", "metricValue": 1024.0, "tags": {} }] - """; + """.formatted(agentId, now.toString()); - restTemplate.postForEntity( + ResponseEntity ingestResponse = restTemplate.postForEntity( "/api/v1/data/metrics", new HttpEntity<>(json, authHeaders), String.class); + assertThat(ingestResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); - await().atMost(10, SECONDS).untilAsserted(() -> { - Integer count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM agent_metrics WHERE instance_id = 'agent-flush-test'", - Integer.class); - assertThat(count).isGreaterThanOrEqualTo(1); + // agent_metrics lives in ClickHouse; drive the visibility check through + // the env-scoped REST metrics endpoint, never through raw SQL. + await().atMost(15, SECONDS).untilAsserted(() -> { + ResponseEntity r = restTemplate.exchange( + "/api/v1/environments/default/agents/" + agentId + + "/metrics?names=memory.used", + HttpMethod.GET, + new HttpEntity<>(viewerHeaders), + String.class); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode body = objectMapper.readTree(r.getBody()); + JsonNode series = body.path("metrics").path("memory.used"); + assertThat(series.isArray()).isTrue(); + long nonZero = 0; + for (JsonNode bucket : series) { + if (bucket.get("value").asDouble() > 0) nonZero++; + } + assertThat(nonZero).isGreaterThanOrEqualTo(1); }); } }