env(admin): per-environment color field + V2 migration
- V2__add_environment_color.sql adds a CHECK-constrained VARCHAR color column (default 'slate'); existing rows backfill to slate. - Environment record + EnvironmentColor constants (8 preset values) flow through repository, service, and admin API. - UpdateEnvironmentRequest.color nullable: null preserves existing; unknown values → 400. - ITs cover valid / invalid / null-preserves behaviour; existing Environment constructor call-sites updated with the new color arg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ class AgentLifecycleEvaluatorTest {
|
||||
events = mock(AgentEventRepository.class);
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(
|
||||
new Environment(ENV_ID, ENV_SLUG, "Prod", true, true, Map.of(), 5, Instant.EPOCH)));
|
||||
new Environment(ENV_ID, ENV_SLUG, "Prod", true, true, Map.of(), 5, "slate", Instant.EPOCH)));
|
||||
eval = new AgentLifecycleEvaluator(events, envRepo);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class ExchangeMatchEvaluatorTest {
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
eval = new ExchangeMatchEvaluator(searchIndex, envRepo, props);
|
||||
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, null);
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class LogPatternEvaluatorTest {
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
eval = new LogPatternEvaluator(logStore, envRepo);
|
||||
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, null);
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class RouteMetricEvaluatorTest {
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
eval = new RouteMetricEvaluator(statsStore, envRepo);
|
||||
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, null);
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
|
||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class NotificationContextBuilderTest {
|
||||
// ---- helpers ----
|
||||
|
||||
private Environment env() {
|
||||
return new Environment(ENV_ID, "prod", "Production", true, true, Map.of(), 5, Instant.EPOCH);
|
||||
return new Environment(ENV_ID, "prod", "Production", true, true, Map.of(), 5, "slate", Instant.EPOCH);
|
||||
}
|
||||
|
||||
private AlertRule rule(ConditionKind kind) {
|
||||
|
||||
@@ -88,9 +88,80 @@ class EnvironmentAdminControllerIT extends AbstractPostgresIT {
|
||||
assertThat(body.path("displayName").asText()).isEqualTo("Staging");
|
||||
assertThat(body.path("production").asBoolean()).isFalse();
|
||||
assertThat(body.path("enabled").asBoolean()).isTrue();
|
||||
assertThat(body.path("color").asText()).isEqualTo("slate");
|
||||
assertThat(body.has("id")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEnvironment_withValidColor_persists() throws Exception {
|
||||
restTemplate.exchange(
|
||||
"/api/v1/admin/environments", HttpMethod.POST,
|
||||
new HttpEntity<>("""
|
||||
{"slug": "color-ok", "displayName": "Color OK", "production": false}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/admin/environments/color-ok", HttpMethod.PUT,
|
||||
new HttpEntity<>("""
|
||||
{"displayName": "Color OK", "production": false, "enabled": true, "color": "amber"}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.path("color").asText()).isEqualTo("amber");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEnvironment_withNullColor_preservesExisting() throws Exception {
|
||||
restTemplate.exchange(
|
||||
"/api/v1/admin/environments", HttpMethod.POST,
|
||||
new HttpEntity<>("""
|
||||
{"slug": "color-preserve", "displayName": "Keep", "production": false}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
// Set color to teal
|
||||
restTemplate.exchange(
|
||||
"/api/v1/admin/environments/color-preserve", HttpMethod.PUT,
|
||||
new HttpEntity<>("""
|
||||
{"displayName": "Keep", "production": false, "enabled": true, "color": "teal"}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
// Update without color field → teal preserved
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/admin/environments/color-preserve", HttpMethod.PUT,
|
||||
new HttpEntity<>("""
|
||||
{"displayName": "Still Keep", "production": false, "enabled": true}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.path("displayName").asText()).isEqualTo("Still Keep");
|
||||
assertThat(body.path("color").asText()).isEqualTo("teal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEnvironment_withUnknownColor_returns400() throws Exception {
|
||||
restTemplate.exchange(
|
||||
"/api/v1/admin/environments", HttpMethod.POST,
|
||||
new HttpEntity<>("""
|
||||
{"slug": "color-bad", "displayName": "Bad", "production": false}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"/api/v1/admin/environments/color-bad", HttpMethod.PUT,
|
||||
new HttpEntity<>("""
|
||||
{"displayName": "Bad", "production": false, "enabled": true, "color": "neon"}
|
||||
""", securityHelper.authHeaders(adminJwt)),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEnvironment_asAdmin_returns200() throws Exception {
|
||||
// Create an environment first
|
||||
|
||||
Reference in New Issue
Block a user