fix: continue card-detail fallbacks on 2xx incompatibility
This commit is contained in:
@@ -757,7 +757,8 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
when (evaluateCommentResponse(response.body)) {
|
||||
CommentResponseClassification.Success -> return@withContext BoardsApiResult.Success(Unit)
|
||||
CommentResponseClassification.LogicalFailure -> {
|
||||
return@withContext BoardsApiResult.Failure(extractLogicalFailureMessage(response.body))
|
||||
lastFailure = extractLogicalFailureMessage(response.body)
|
||||
continue
|
||||
}
|
||||
CommentResponseClassification.ParseIncompatible -> continue
|
||||
}
|
||||
@@ -1175,31 +1176,31 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
}
|
||||
|
||||
val parsed = parseJsonValue(body) ?: return null
|
||||
val items = when (parsed) {
|
||||
is List<*> -> parsed.mapNotNull { it as? Map<*, *> }
|
||||
is Map<*, *> -> extractActivityArray(parsed)
|
||||
else -> return null
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
val trimmed = body.trim()
|
||||
if (trimmed == "[]") {
|
||||
val items: List<Map<*, *>> = when (parsed) {
|
||||
is List<*> -> {
|
||||
if (parsed.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val asMap = parsed as? Map<*, *> ?: return emptyList()
|
||||
val hadContainer = sequenceOf(
|
||||
"activities",
|
||||
"actions",
|
||||
"cardActivities",
|
||||
"card_activities",
|
||||
"items",
|
||||
"data",
|
||||
).any { key -> asMap.containsKey(key) || ((asMap["data"] as? Map<*, *>)?.containsKey(key) == true) }
|
||||
if (hadContainer) {
|
||||
if (parsed.any { it !is Map<*, *> }) {
|
||||
return null
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
parsed as List<Map<*, *>>
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
val extracted = extractActivityContainer(parsed) ?: return null
|
||||
if (extracted.any { it !is Map<*, *> }) {
|
||||
return null
|
||||
}
|
||||
if (extracted.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
extracted.mapNotNull { it as? Map<*, *> }
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return items.mapNotNull { raw ->
|
||||
val id = extractString(raw, "id", "publicId", "public_id")
|
||||
@@ -1218,13 +1219,24 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractActivityArray(source: Map<*, *>): List<Map<*, *>> {
|
||||
val topLevel = extractObjectArray(source, "activities", "actions", "cardActivities", "card_activities", "items", "data")
|
||||
if (topLevel.isNotEmpty()) {
|
||||
return topLevel
|
||||
private fun extractActivityContainer(source: Map<*, *>): List<*>? {
|
||||
val keys = listOf("activities", "actions", "cardActivities", "card_activities", "items", "data")
|
||||
keys.forEach { key ->
|
||||
if (source.containsKey(key)) {
|
||||
val value = source[key]
|
||||
return value as? List<*>
|
||||
}
|
||||
val data = source["data"] as? Map<*, *> ?: return emptyList()
|
||||
return extractObjectArray(data, "activities", "actions", "cardActivities", "card_activities", "items", "data")
|
||||
}
|
||||
|
||||
val data = source["data"] as? Map<*, *> ?: return null
|
||||
keys.forEach { key ->
|
||||
if (data.containsKey(key)) {
|
||||
val value = data[key]
|
||||
return value as? List<*>
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseEpochMillis(raw: String): Long? {
|
||||
|
||||
@@ -166,7 +166,7 @@ class CardDetailApiCompatibilityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listCardActivities_triesEndpointVariants_thenReturnsNewestFirstTopTen() = runTest {
|
||||
fun listCardActivities_fallsBackToThirdEndpoint_whenFirstTwo2xxPayloadsAreUnparseable() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.register(
|
||||
path = "/api/v1/cards/card-3/activities?limit=50",
|
||||
@@ -174,12 +174,18 @@ class CardDetailApiCompatibilityTest {
|
||||
status = 200,
|
||||
responseBody = "{\"data\":\"not-an-array\"}",
|
||||
)
|
||||
server.register(
|
||||
path = "/api/v1/cards/card-3/actions?limit=50",
|
||||
method = "GET",
|
||||
status = 200,
|
||||
responseBody = "{\"actions\":{\"unexpected\":true}}",
|
||||
)
|
||||
val secondPayloadItems = (1..12).joinToString(",") { index ->
|
||||
val day = index.toString().padStart(2, '0')
|
||||
"""{"id":"a-$index","type":"comment","text":"Item $index","createdAt":"2026-01-${day}T00:00:00Z"}"""
|
||||
}
|
||||
server.register(
|
||||
path = "/api/v1/cards/card-3/actions?limit=50",
|
||||
path = "/api/v1/cards/card-3/card-activities?limit=50",
|
||||
method = "GET",
|
||||
status = 200,
|
||||
responseBody = "{\"data\":[${secondPayloadItems}]}",
|
||||
@@ -194,7 +200,7 @@ class CardDetailApiCompatibilityTest {
|
||||
val requests = server.findRequests("/api/v1/cards/card-3/activities?limit=50") +
|
||||
server.findRequests("/api/v1/cards/card-3/actions?limit=50") +
|
||||
server.findRequests("/api/v1/cards/card-3/card-activities?limit=50")
|
||||
assertEquals(2, requests.size)
|
||||
assertEquals(3, requests.size)
|
||||
assertEquals(
|
||||
LocalDate.of(2026, 1, 12).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(),
|
||||
activities.first().createdAtEpochMillis,
|
||||
@@ -203,25 +209,33 @@ class CardDetailApiCompatibilityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addCardComment_fallbacksOnAmbiguousSuccess_andStopsOnLogicalFailure() = runTest {
|
||||
fun addCardComment_continuesFallbackAfter2xxLogicalFailure_untilLaterVariantSucceeds() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.registerSequence(
|
||||
path = "/api/v1/cards/card-4/comment-actions",
|
||||
method = "POST",
|
||||
responses = listOf(
|
||||
200 to "{\"maybe\":\"unknown\"}",
|
||||
200 to "{\"success\":false,\"message\":\"blocked\"}",
|
||||
500 to "{}",
|
||||
),
|
||||
)
|
||||
server.register(
|
||||
path = "/api/v1/cards/card-4/actions/comments",
|
||||
method = "POST",
|
||||
status = 200,
|
||||
responseBody = "{\"action\":{\"id\":\"act-1\"}}",
|
||||
)
|
||||
|
||||
val result = HttpKanbnApiClient().addCardComment(server.baseUrl, "api", "card-4", "A comment")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Failure)
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val requests = server.findRequests("/api/v1/cards/card-4/comment-actions")
|
||||
assertEquals(2, requests.size)
|
||||
assertEquals("{\"text\":\"A comment\"}", requests[0].body)
|
||||
assertEquals("{\"comment\":\"A comment\"}", requests[1].body)
|
||||
assertTrue(server.findRequests("/api/v1/cards/card-4/actions/comments").isEmpty())
|
||||
val thirdRequests = server.findRequests("/api/v1/cards/card-4/actions/comments")
|
||||
assertEquals(1, thirdRequests.size)
|
||||
assertEquals("{\"text\":\"A comment\"}", thirdRequests[0].body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user