feat(kanban): add bulk operations, board management, and file attachments

This commit is contained in:
M57 committed 2026-09-15 00:01:33 +03:00
1 parent 6e94fa0be2
commit 05e78ba135
7 files changed
+870 -46

No files matched your search

@@ -1,12 +1,18 @@
package com.m57.hermescontrol.ui.kanban
import com.m57.hermescontrol.data.local.InMemoryKanbanPreferencesStore
import com.m57.hermescontrol.data.model.BulkTaskResult
import com.m57.hermescontrol.data.model.BulkTasksResponse
import com.m57.hermescontrol.data.model.CreateBoardResponse
import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateTaskResponse
import com.m57.hermescontrol.data.model.DeleteBoardResponse
import com.m57.hermescontrol.data.model.KanbanBoard
import com.m57.hermescontrol.data.model.KanbanBoardResponse
import com.m57.hermescontrol.data.model.KanbanColumn
import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.data.model.RenameBoardResponse
import com.m57.hermescontrol.data.model.TaskEstimate
import com.m57.hermescontrol.data.model.UpdateTaskBody
import com.m57.hermescontrol.data.model.UpdateTaskResponse
import com.m57.hermescontrol.data.remote.NetworkError
@@ -341,4 +347,147 @@ class KanbanMutationTest {
.name,
)
}
@Test
fun testCreateBoardSuccess() =
runTest(testDispatcher) {
val board = KanbanBoard(id = "sprint-2", name = "Sprint 2")
coEvery { mockRepository.createBoard(any()) } returns
NetworkResult.Success(CreateBoardResponse(board = board))
coEvery { mockRepository.getBoards() } returns
NetworkResult.Success(
com.m57.hermescontrol.data.model
.KanbanBoardsResponse(listOf(board), "sprint-2"),
)
val vm = createViewModel()
vm.createBoard(slug = "sprint-2", name = "Sprint 2", description = "Next cycle")
testDispatcher.scheduler.advanceUntilIdle()
coVerify {
mockRepository.createBoard(
match {
it.slug == "sprint-2" && it.name == "Sprint 2" && it.description == "Next cycle"
},
)
}
assertEquals("Board created: Sprint 2", vm.uiState.value.toastMessage)
}
@Test
fun testRenameBoardSuccess() =
runTest(testDispatcher) {
val board = KanbanBoard(id = "dev", name = "Dev Renamed")
coEvery { mockRepository.updateBoard("dev", any()) } returns
NetworkResult.Success(RenameBoardResponse(board = board))
coEvery { mockRepository.getBoards() } returns
NetworkResult.Success(
com.m57.hermescontrol.data.model
.KanbanBoardsResponse(listOf(board), "dev"),
)
val vm = createViewModel()
vm.renameBoard("dev", "Dev Renamed")
testDispatcher.scheduler.advanceUntilIdle()
coVerify {
mockRepository.updateBoard("dev", match { it.name == "Dev Renamed" })
}
assertEquals("Board renamed to Dev Renamed", vm.uiState.value.toastMessage)
}
@Test
fun testDeleteBoardSuccess() =
runTest(testDispatcher) {
coEvery { mockRepository.deleteBoard("old-board", delete = true) } returns
NetworkResult.Success(DeleteBoardResponse(current = null))
coEvery { mockRepository.getBoards() } returns
NetworkResult.Success(
com.m57.hermescontrol.data.model
.KanbanBoardsResponse(emptyList(), null),
)
val vm = createViewModel()
vm.deleteBoard("old-board")
testDispatcher.scheduler.advanceUntilIdle()
coVerify { mockRepository.deleteBoard("old-board", delete = true) }
assertEquals("Board deleted", vm.uiState.value.toastMessage)
}
@Test
fun testBulkMoveTasksSuccess() =
runTest(testDispatcher) {
val board = KanbanBoard(id = "dev", name = "Dev")
coEvery { mockRepository.getBoard("dev") } returns
NetworkResult.Success(KanbanBoardResponse(columns = listOf(KanbanColumn("done", emptyList()))))
coEvery { mockRepository.bulkTasks("dev", any()) } returns
NetworkResult.Success(
BulkTasksResponse(
listOf(
BulkTaskResult("t_1", true),
BulkTaskResult("t_2", true),
),
),
)
val vm = createViewModel()
vm.selectBoard(board)
testDispatcher.scheduler.advanceUntilIdle()
vm.bulkMove(listOf("t_1", "t_2"), "done")
testDispatcher.scheduler.advanceUntilIdle()
coVerify {
mockRepository.bulkTasks(
"dev",
match { it.ids == listOf("t_1", "t_2") && it.status == "done" },
)
}
assertEquals("Moved 2 tasks to done", vm.uiState.value.toastMessage)
}
@Test
fun testBulkArchiveTasksSuccess() =
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(
listOf(
BulkTaskResult("t_1", true),
),
),
)
val vm = createViewModel()
vm.selectBoard(board)
testDispatcher.scheduler.advanceUntilIdle()
vm.bulkArchive(listOf("t_1"))
testDispatcher.scheduler.advanceUntilIdle()
coVerify {
mockRepository.bulkTasks(
"dev",
match { it.ids == listOf("t_1") && it.archive },
)
}
assertEquals("Archived 1 tasks", vm.uiState.value.toastMessage)
}
@Test
fun testEstimateNewTaskSuccess() =
runTest(testDispatcher) {
coEvery { mockRepository.estimateNew("Title", "Body") } returns
NetworkResult.Success(TaskEstimate(ok = true, estTokens = 1200, complexity = "low"))
val vm = createViewModel()
val estimate = vm.estimateNewTask("Title", "Body")
assertEquals(1200, estimate?.estTokens)
assertEquals("low", estimate?.complexity)
}
}
@@ -172,4 +172,29 @@ class KanbanTaskViewModelTest {
?.contains("coder") == true,
)
}
@Test
fun testUploadAttachmentSuccess() =
runTest(testDispatcher) {
val task = KanbanTaskFull(id = "t_1", title = "Task with file", status = "todo")
coEvery { mockRepository.getTask("t_1", "dev") } returns
NetworkResult.Success(KanbanTaskDetailResponse(task = task))
coEvery { mockRepository.uploadAttachment("t_1", "dev", any()) } returns
NetworkResult.Success(
com.m57.hermescontrol.data.model.AttachmentUploadResponse(
attachment =
com.m57.hermescontrol.data.model.KanbanAttachment(
id = 101L,
filename = "test.png",
),
),
)
val vm = createViewModel()
vm.uploadAttachment("dev", "t_1", "test.png", "image/png", byteArrayOf(1, 2, 3))
testDispatcher.scheduler.advanceUntilIdle()
coVerify { mockRepository.uploadAttachment("t_1", "dev", any()) }
assertEquals("Attachment uploaded", vm.uiState.value.toastMessage)
}
}