fix(kanban): address PR correctness and CI regressions (#1137)

This commit is contained in:
M57 committed 2026-09-16 21:49:35 +03:00
1 parent 190a93a4cf
commit 1735de7d9d
25 files changed
+809 -159

No files matched your search

@@ -289,4 +289,29 @@ class KanbanSerializationTest {
assertEquals("M", estimate.complexity)
assertEquals("Multi-file changes needed", estimate.rationale)
}
@Test
fun testBulkAssignmentUsesExplicitEmptyStringAndReclaim() {
val assignJson =
json.encodeToString(
BulkTasksBody(
ids = listOf("t_1"),
assignee = "researcher",
reclaimFirst = true,
),
)
val unassignJson =
json.encodeToString(
BulkTasksBody(
ids = listOf("t_1"),
assignee = "",
reclaimFirst = true,
),
)
assertTrue(assignJson.contains("\"assignee\":\"researcher\""))
assertTrue(assignJson.contains("\"reclaim_first\":true"))
assertTrue(unassignJson.contains("\"assignee\":\"\""))
assertTrue(unassignJson.contains("\"reclaim_first\":true"))
}
}
@@ -63,4 +63,20 @@ class KanbanModelOverrideTest {
assertEquals("low", patch.reasoningEffort)
assertFalse(patch.clearReasoningEffort)
}
@Test
fun testFullReasoningScaleAndCapabilities() {
assertEquals(
listOf("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"),
supportedKanbanReasoningEfforts(supportsReasoning = true, canDisableReasoning = true),
)
assertEquals(
listOf("minimal", "low", "medium", "high", "xhigh", "max", "ultra"),
supportedKanbanReasoningEfforts(supportsReasoning = true, canDisableReasoning = false),
)
assertEquals(
listOf(""),
supportedKanbanReasoningEfforts(supportsReasoning = false, canDisableReasoning = false),
)
}
}
@@ -374,6 +374,40 @@ class KanbanMutationTest {
assertEquals("Board created: Sprint 2", vm.uiState.value.toastMessage)
}
@Test
fun testBulkAssignUsesReclaimAndRetainsOnlyFailures() =
runTest(testDispatcher) {
val board = KanbanBoard(id = "dev", name = "Dev")
coEvery { mockRepository.getBoard("dev") } returns
NetworkResult.Success(KanbanBoardResponse(columns = listOf(KanbanColumn("todo", emptyList()))))
coEvery { mockRepository.bulkTasks("dev", any()) } returns
NetworkResult.Success(
BulkTasksResponse(
results =
listOf(
BulkTaskResult("t_1", ok = true),
BulkTaskResult("t_2", ok = false, error = "locked"),
),
),
)
val vm = createViewModel()
vm.selectBoard(board)
testDispatcher.scheduler.advanceUntilIdle()
var failedIds = emptySet<String>()
vm.bulkAssign(listOf("t_1", "t_2"), assignee = null) { failedIds = it }
testDispatcher.scheduler.advanceUntilIdle()
coVerify {
mockRepository.bulkTasks(
"dev",
match { it.ids == listOf("t_1", "t_2") && it.assignee == "" && it.reclaimFirst },
)
}
assertEquals(setOf("t_2"), failedIds)
}
@Test
fun testRenameBoardSuccess() =
runTest(testDispatcher) {
@@ -408,11 +442,13 @@ class KanbanMutationTest {
)
val vm = createViewModel()
preferences.setSelectedBoard("http://127.0.0.1:9119", "old-board")
vm.deleteBoard("old-board")
testDispatcher.scheduler.advanceUntilIdle()
coVerify { mockRepository.deleteBoard("old-board", delete = false) }
assertEquals("Board archived", vm.uiState.value.toastMessage)
assertEquals(null, preferences.getSelectedBoard("http://127.0.0.1:9119"))
}
@Test
@@ -15,6 +15,7 @@ import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
@@ -31,10 +32,11 @@ class KanbanTaskViewModelTest {
private val mockRepository = mockk<KanbanRepository>(relaxed = true)
private val mockEventsClient = mockk<KanbanEventsClient>(relaxed = true)
private fun createViewModel(): KanbanTaskViewModel =
private fun createViewModel(enableFallbackPolling: Boolean = false): KanbanTaskViewModel =
KanbanTaskViewModel(
repository = mockRepository,
eventsClientProvider = { mockEventsClient },
enableFallbackPolling = enableFallbackPolling,
)
@Before
@@ -285,4 +287,38 @@ class KanbanTaskViewModelTest {
assertTrue(deleted)
assertEquals("Task deleted", vm.uiState.value.toastMessage)
}
@Test
fun testFallbackPollingReloadsTaskWithoutEvents() =
runTest(testDispatcher) {
val first = KanbanTaskFull(id = "t_1", title = "Before", status = "todo")
val second = KanbanTaskFull(id = "t_1", title = "After", status = "todo")
coEvery { mockRepository.getTask("t_1", "dev") } returnsMany
listOf(
NetworkResult.Success(KanbanTaskDetailResponse(task = first)),
NetworkResult.Success(KanbanTaskDetailResponse(task = second)),
)
val vm = createViewModel(enableFallbackPolling = true)
vm.loadTask("dev", "t_1")
testDispatcher.scheduler.runCurrent()
assertEquals(
"Before",
vm.uiState.value.detail
?.task
?.title,
)
testDispatcher.scheduler.advanceTimeBy(30_000L)
testDispatcher.scheduler.runCurrent()
assertEquals(
"After",
vm.uiState.value.detail
?.task
?.title,
)
coVerify(exactly = 2) { mockRepository.getTask("t_1", "dev") }
vm.onCleared()
}
}
@@ -0,0 +1,68 @@
package com.m57.hermescontrol.util
import android.content.ContentResolver
import android.net.Uri
import io.mockk.every
import io.mockk.mockk
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okio.Buffer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.IOException
class StreamingUriRequestBodyTest {
@Test
fun testStreamsDirectlyIntoSink() {
val testData = "test archive data streaming content".toByteArray(Charsets.UTF_8)
val mockResolver = mockk<ContentResolver>()
val mockUri = mockk<Uri>()
every { mockResolver.openInputStream(mockUri) } answers {
ByteArrayInputStream(testData)
}
val mediaType = "application/gzip".toMediaTypeOrNull()
val requestBody =
StreamingUriRequestBody(
contentResolver = mockResolver,
uri = mockUri,
contentType = mediaType,
contentLength = testData.size.toLong(),
)
assertEquals(mediaType, requestBody.contentType())
assertEquals(testData.size.toLong(), requestBody.contentLength())
val buffer = Buffer()
requestBody.writeTo(buffer)
assertEquals("test archive data streaming content", buffer.readUtf8())
}
@Test
fun testThrowsWhenInputStreamCannotBeOpened() {
val mockResolver = mockk<ContentResolver>()
val mockUri = mockk<Uri>()
every { mockResolver.openInputStream(mockUri) } returns null
val requestBody =
StreamingUriRequestBody(
contentResolver = mockResolver,
uri = mockUri,
contentType = null,
)
var thrown = false
try {
val buffer = Buffer()
requestBody.writeTo(buffer)
} catch (e: IOException) {
thrown = true
}
assertTrue(thrown)
}
}