Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import io.github.jpicklyk.mcptask.current.domain.model.Role
import io.github.jpicklyk.mcptask.current.domain.repository.Result
import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.*

/**
Expand Down Expand Up @@ -193,22 +195,21 @@ Parameters:
): JsonElement {
val workItemRepo = context.workItemRepository()

// Fetch work and review items (two calls, merge results)
val workItems = when (val r = workItemRepo.findByRole(Role.WORK, limit = 200)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
}
val reviewItems = when (val r = workItemRepo.findByRole(Role.REVIEW, limit = 200)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
// Fetch work and review items in parallel, merge results
val (workItems, reviewItems) = coroutineScope {
val workDeferred = async { workItemRepo.findByRole(Role.WORK, limit = 200) }
val reviewDeferred = async { workItemRepo.findByRole(Role.REVIEW, limit = 200) }

Pair(
workDeferred.await().getOrElse(emptyList()),
reviewDeferred.await().getOrElse(emptyList())
)
}
val activeItems = (workItems + reviewItems)
val activeItems = workItems + reviewItems

// Recent transitions since the given timestamp
val recentTransitions = when (val r = context.roleTransitionRepository().findSince(since, limit = transitionLimit)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
}
val recentTransitions = context.roleTransitionRepository().findSince(since, limit = transitionLimit)
.getOrElse(emptyList())

// Stalled items: active items with missing required notes
val stalledItems = findStalledItems(activeItems, context)
Expand Down Expand Up @@ -266,20 +267,20 @@ Parameters:
private suspend fun executeHealthCheckMode(context: ToolExecutionContext, includeAncestors: Boolean): JsonElement {
val workItemRepo = context.workItemRepository()

val workItems = when (val r = workItemRepo.findByRole(Role.WORK, limit = 200)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
}
val reviewItems = when (val r = workItemRepo.findByRole(Role.REVIEW, limit = 200)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
}
val blockedItems = when (val r = workItemRepo.findByRole(Role.BLOCKED, limit = 200)) {
is Result.Success -> r.data
is Result.Error -> emptyList()
// Fetch work, review, and blocked items in parallel
val (workItems, reviewItems, blockedItems) = coroutineScope {
val workDeferred = async { workItemRepo.findByRole(Role.WORK, limit = 200) }
val reviewDeferred = async { workItemRepo.findByRole(Role.REVIEW, limit = 200) }
val blockedDeferred = async { workItemRepo.findByRole(Role.BLOCKED, limit = 200) }

Triple(
workDeferred.await().getOrElse(emptyList()),
reviewDeferred.await().getOrElse(emptyList()),
blockedDeferred.await().getOrElse(emptyList())
)
}

val activeItems = (workItems + reviewItems)
val activeItems = workItems + reviewItems
val stalledItems = findStalledItems(activeItems, context)

// Resolve ancestor chains once for all items if requested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ sealed class Result<out T> {
}
return this
}

/**
* Returns the data if this is a success result, otherwise returns the provided default value.
*/
fun getOrElse(default: @UnsafeVariance T): T = when (this) {
is Success -> data
is Error -> default
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,43 @@ class GetContextToolTest {
assertEquals("Root", childAncestors[0].jsonObject["title"]!!.jsonPrimitive.content)
}

// ──────────────────────────────────────────────
// Parallel query tests
// ──────────────────────────────────────────────

@Test
fun `health check parallel queries return correct combined results`(): Unit = runBlocking {
val workItem = makeItem(title = "Work Task", role = Role.WORK)
val reviewItem = makeItem(title = "Review Task", role = Role.REVIEW)
val blockedItem = makeItem(title = "Blocked Task", role = Role.BLOCKED)

coEvery { workItemRepo.findByRole(Role.WORK, any()) } returns Result.Success(listOf(workItem))
coEvery { workItemRepo.findByRole(Role.REVIEW, any()) } returns Result.Success(listOf(reviewItem))
coEvery { workItemRepo.findByRole(Role.BLOCKED, any()) } returns Result.Success(listOf(blockedItem))

val result = tool.execute(params(), context)

val data = extractData(result)
assertEquals("health-check", data["mode"]!!.jsonPrimitive.content)

// Verify active items contain both work and review items
val activeItems = data["activeItems"]!!.jsonArray
assertEquals(2, activeItems.size)
val activeTitles = activeItems.map { it.jsonObject["title"]!!.jsonPrimitive.content }.toSet()
assertTrue("Work Task" in activeTitles, "Expected 'Work Task' in active items")
assertTrue("Review Task" in activeTitles, "Expected 'Review Task' in active items")

// Verify blocked items section
val blockedItems = data["blockedItems"]!!.jsonArray
assertEquals(1, blockedItems.size)
assertEquals("Blocked Task", blockedItems[0].jsonObject["title"]!!.jsonPrimitive.content)

// Verify all three findByRole calls were made
coVerify(exactly = 1) { workItemRepo.findByRole(Role.WORK, any()) }
coVerify(exactly = 1) { workItemRepo.findByRole(Role.REVIEW, any()) }
coVerify(exactly = 1) { workItemRepo.findByRole(Role.BLOCKED, any()) }
}

// ──────────────────────────────────────────────
// guidancePointer tests
// ──────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.github.jpicklyk.mcptask.current.domain.repository

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.util.UUID

class ResultTest {

private val testError = RepositoryError.NotFound(UUID.randomUUID(), "Not found")

// ──────────────────────────────────────────────
// getOrElse
// ──────────────────────────────────────────────

@Test
fun `getOrElse returns data on success`() {
val result: Result<String> = Result.Success("hello")
assertEquals("hello", result.getOrElse("default"))
}

@Test
fun `getOrElse returns default on error`() {
val result: Result<String> = Result.Error(testError)
assertEquals("default", result.getOrElse("default"))
}

@Test
fun `getOrElse returns empty list on error for list types`() {
val result: Result<List<Int>> = Result.Error(testError)
assertEquals(emptyList<Int>(), result.getOrElse(emptyList()))
}

@Test
fun `getOrElse returns populated list on success`() {
val result: Result<List<Int>> = Result.Success(listOf(1, 2, 3))
assertEquals(listOf(1, 2, 3), result.getOrElse(emptyList()))
}

// ──────────────────────────────────────────────
// getOrNull
// ──────────────────────────────────────────────

@Test
fun `getOrNull returns data on success`() {
val result: Result<String> = Result.Success("hello")
assertEquals("hello", result.getOrNull())
}

@Test
fun `getOrNull returns null on error`() {
val result: Result<String> = Result.Error(testError)
assertNull(result.getOrNull())
}

// ──────────────────────────────────────────────
// map
// ──────────────────────────────────────────────

@Test
fun `map transforms success data`() {
val result: Result<Int> = Result.Success(5)
val mapped = result.map { it * 2 }
assertEquals(10, (mapped as Result.Success).data)
}

@Test
fun `map passes error through unchanged`() {
val result: Result<Int> = Result.Error(testError)
val mapped = result.map { it * 2 }
assertTrue(mapped.isError())
assertEquals(testError, (mapped as Result.Error).error)
}

// ──────────────────────────────────────────────
// isSuccess / isError
// ──────────────────────────────────────────────

@Test
fun `isSuccess returns true for success`() {
assertTrue(Result.Success("data").isSuccess())
}

@Test
fun `isSuccess returns false for error`() {
assertFalse(Result.Error(testError).isSuccess())
}

@Test
fun `isError returns true for error`() {
assertTrue(Result.Error(testError).isError())
}

@Test
fun `isError returns false for success`() {
assertFalse(Result.Success("data").isError())
}

// ──────────────────────────────────────────────
// onSuccess / onError callbacks
// ──────────────────────────────────────────────

@Test
fun `onSuccess executes block for success`() {
var captured: String? = null
Result.Success("hello").onSuccess { captured = it }
assertEquals("hello", captured)
}

@Test
fun `onSuccess does not execute block for error`() {
var executed = false
Result.Error(testError).onSuccess { executed = true }
assertFalse(executed)
}

@Test
fun `onError executes block for error`() {
var captured: RepositoryError? = null
Result.Error(testError).onError { captured = it }
assertEquals(testError, captured)
}

@Test
fun `onError does not execute block for success`() {
var executed = false
Result.Success("hello").onError { executed = true }
assertFalse(executed)
}

@Test
fun `onSuccess returns same result for chaining`() {
val result = Result.Success("hello")
assertSame(result, result.onSuccess { })
}

@Test
fun `onError returns same result for chaining`() {
val result: Result<String> = Result.Error(testError)
assertSame(result, result.onError { })
}
}
Loading