feat(03-01): add agent registration controller, config, lifecycle monitor
- AgentRegistryConfig: heartbeat, stale, dead, ping, command expiry settings
- AgentRegistryBeanConfig: wires AgentRegistryService as Spring bean
- AgentLifecycleMonitor: @Scheduled lifecycle check + command expiry sweep
- AgentRegistrationController: POST /register, POST /{id}/heartbeat, GET /agents
- Updated Cameleer3ServerApplication with AgentRegistryConfig
- Updated application.yml with agent-registry section and async timeout
- 7 integration tests: register, re-register, heartbeat, list, filter, invalid status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.AbstractClickHouseIT;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AgentRegistrationControllerIT extends AbstractClickHouseIT {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private HttpHeaders protocolHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
return headers;
|
||||
}
|
||||
|
||||
private HttpHeaders protocolHeadersNoBody() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-Cameleer-Protocol-Version", "1");
|
||||
return headers;
|
||||
}
|
||||
|
||||
private ResponseEntity<String> registerAgent(String agentId, String name) {
|
||||
String json = """
|
||||
{
|
||||
"agentId": "%s",
|
||||
"name": "%s",
|
||||
"group": "test-group",
|
||||
"version": "1.0.0",
|
||||
"routeIds": ["route-1", "route-2"],
|
||||
"capabilities": {"tracing": true}
|
||||
}
|
||||
""".formatted(agentId, name);
|
||||
|
||||
return restTemplate.postForEntity(
|
||||
"/api/v1/agents/register",
|
||||
new HttpEntity<>(json, protocolHeaders()),
|
||||
String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerNewAgent_returns200WithAgentIdAndSseEndpoint() throws Exception {
|
||||
ResponseEntity<String> response = registerAgent("agent-it-1", "IT Agent 1");
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.get("agentId").asText()).isEqualTo("agent-it-1");
|
||||
assertThat(body.get("sseEndpoint").asText()).isEqualTo("/api/v1/agents/agent-it-1/events");
|
||||
assertThat(body.get("heartbeatIntervalMs").asLong()).isGreaterThan(0);
|
||||
assertThat(body.has("serverPublicKey")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reRegisterSameAgent_returns200WithLiveState() throws Exception {
|
||||
registerAgent("agent-it-reregister", "First Registration");
|
||||
|
||||
ResponseEntity<String> response = registerAgent("agent-it-reregister", "Second Registration");
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.get("agentId").asText()).isEqualTo("agent-it-reregister");
|
||||
}
|
||||
|
||||
@Test
|
||||
void heartbeatKnownAgent_returns200() {
|
||||
registerAgent("agent-it-hb", "HB Agent");
|
||||
|
||||
ResponseEntity<Void> response = restTemplate.exchange(
|
||||
"/api/v1/agents/agent-it-hb/heartbeat",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(protocolHeadersNoBody()),
|
||||
Void.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void heartbeatUnknownAgent_returns404() {
|
||||
ResponseEntity<Void> response = restTemplate.exchange(
|
||||
"/api/v1/agents/unknown-agent-xyz/heartbeat",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(protocolHeadersNoBody()),
|
||||
Void.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllAgents_returnsBothAgents() throws Exception {
|
||||
registerAgent("agent-it-list-1", "List Agent 1");
|
||||
registerAgent("agent-it-list-2", "List Agent 2");
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/agents",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(protocolHeadersNoBody()),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.isArray()).isTrue();
|
||||
// At minimum, our two agents should be present (may have more from other tests)
|
||||
assertThat(body.size()).isGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAgentsByStatus_filtersCorrectly() throws Exception {
|
||||
registerAgent("agent-it-filter", "Filter Agent");
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/agents?status=LIVE",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(protocolHeadersNoBody()),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.isArray()).isTrue();
|
||||
// All returned agents should be LIVE
|
||||
for (JsonNode agent : body) {
|
||||
assertThat(agent.get("state").asText()).isEqualTo("LIVE");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAgentsWithInvalidStatus_returns400() {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/agents?status=INVALID",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(protocolHeadersNoBody()),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user