diff --git a/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py b/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py
index 9a674cf..6baa173 100644
--- a/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py
+++ b/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py
@@ -203,6 +203,8 @@ class TestCloudWatchAlarms:
alarm = next(a for a in alarms if a["AlarmName"] == alarm_name)
assert alarm["StateValue"] == "INSUFFICIENT_DATA"
+ assert response["CompositeAlarms"] == []
+ assert response["LogAlarms"] == []
finally:
cloudwatch_client.delete_alarms(AlarmNames=[alarm_name])
diff --git a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java
index 2aef32e..35c0307 100644
--- a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java
+++ b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java
@@ -263,6 +263,8 @@ public class CloudWatchMetricsJsonHandler {
if (a.getStateReasonData() != null) node.put("StateReasonData", a.getStateReasonData());
node.put("StateUpdatedTimestamp", a.getStateUpdatedTimestamp());
}
+ response.putArray("CompositeAlarms");
+ response.putArray("LogAlarms");
return Response.ok(response).build();
}
diff --git a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java
index d3a0542..7b55d0e 100644
--- a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java
+++ b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java
@@ -259,6 +259,8 @@ public class CloudWatchMetricsQueryHandler {
toAlarmXml(xml, a);
}
xml.end("MetricAlarms");
+ xml.start("CompositeAlarms").end("CompositeAlarms");
+ xml.start("LogAlarms").end("LogAlarms");
return Response.ok(AwsQueryResponse.envelope("DescribeAlarms", null, xml.build())).build();
}
diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java
index 2d3a2a2..cc81e98 100644
--- a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java
+++ b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java
@@ -350,6 +350,22 @@ class CloudWatchMetricsJsonHandlerTest {
assertEquals("Count", alarm.get("Unit").asText());
}
+ // AWS sends every alarm collection even when it is empty. The CLI's text output writes a
+ // None line for each paginator result key missing from the page (floci-io/floci#6362).
+ @Test
+ void describeAlarms_returnsEmptyCompositeAndLogAlarmCollections() {
+ handler.handle("PutMetricAlarm", alarmRequest("MetricOnly"), REGION);
+
+ ObjectNode response = (ObjectNode) handler.handle("DescribeAlarms", MAPPER.createObjectNode(), REGION)
+ .getEntity();
+
+ assertEquals(1, response.get("MetricAlarms").size());
+ assertTrue(response.get("CompositeAlarms").isArray());
+ assertTrue(response.get("CompositeAlarms").isEmpty());
+ assertTrue(response.get("LogAlarms").isArray());
+ assertTrue(response.get("LogAlarms").isEmpty());
+ }
+
// An alarm the caller never gave an M for reports no DatapointsToAlarm at all. AWS omits the
// member in that case, and echoing EvaluationPeriods instead reads as a change to any client
// that re-plans against its own configuration, which is what floci-io/floci#3660 hit.
diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java
index 881de60..27869c6 100644
--- a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java
+++ b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java
@@ -242,4 +242,15 @@ class CloudWatchMetricsQueryHandlerTest {
verify(metricsService).putMetricData(eq("NS"), captor.capture(), eq(REGION));
assertEquals(List.of(" "), captor.getValue().stream().map(MetricDatum::getMetricName).toList());
}
+
+ @Test
+ void describeAlarmsReturnsEmptyCompositeAndLogAlarmCollections() {
+ when(metricsService.describeAlarms(any(), any(), eq(REGION))).thenReturn(List.of());
+
+ String xml = (String) handler.handle("DescribeAlarms", new MultivaluedHashMap<>(), REGION).getEntity();
+
+ assertTrue(xml.contains(""), xml);
+ assertTrue(xml.contains(""), xml);
+ assertTrue(xml.contains(""), xml);
+ }
}