Skip to content

Commit 0d55ed4

Browse files
authored
Merge pull request #19 from Deep-CodeAI/test/coverage-improvements
Test/coverage improvements
2 parents 84d2641 + 12a81f1 commit 0d55ed4

4 files changed

Lines changed: 585 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package agents_engine.composition.forum
2+
3+
import agents_engine.core.agent
4+
import agents_engine.generation.Generable
5+
import agents_engine.generation.Guide
6+
import agents_engine.model.LlmResponse
7+
import agents_engine.model.ModelClient
8+
import agents_engine.model.ToolCall
9+
import org.junit.jupiter.api.assertThrows
10+
import kotlin.test.Test
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertTrue
13+
14+
// Tests for #888 — direct coverage of Forum.castForumReturn. The captain
15+
// emits `forum_return(value=...)`, the framework throws ForumReturnException
16+
// internally, and Forum.invokeSuspend's catch block routes the value through
17+
// castForumReturn(). Different `value` types exercise each branch.
18+
19+
@Generable("forum verdict shape")
20+
data class ForumVerdict(@Guide("the answer") val answer: String)
21+
22+
class ForumCastReturnTest {
23+
24+
private fun participant() = agent<String, String>("p") {
25+
skills { skill<String, String>("p") { implementedBy { "participant-said-$it" } } }
26+
}
27+
28+
private fun captainEmitting(toolArgs: Map<String, Any?>) = agent<String, String>("c") {
29+
model {
30+
ollama("test")
31+
client = ModelClient { _ ->
32+
LlmResponse.ToolCalls(listOf(ToolCall("forum_return", toolArgs)))
33+
}
34+
}
35+
skills { skill<String, String>("c") { tools() } }
36+
}
37+
38+
private inline fun <reified T : Any> typedCaptainEmitting(toolArgs: Map<String, Any?>) =
39+
agent<String, T>("c") {
40+
model {
41+
ollama("test")
42+
client = ModelClient { _ ->
43+
LlmResponse.ToolCalls(listOf(ToolCall("forum_return", toolArgs)))
44+
}
45+
}
46+
skills { skill<String, T>("c") { tools() } }
47+
}
48+
49+
// L84 — outType == String, value is non-String (toString cast)
50+
51+
@Test
52+
fun `castForumReturn outType String coerces non-String via toString`() {
53+
val result = forum<String, String> {
54+
participant(participant())
55+
captain(captainEmitting(mapOf("value" to 42)))
56+
}("topic")
57+
assertEquals("42", result)
58+
}
59+
60+
// L85 — outType.java.isInstance(raw) — direct pass-through
61+
62+
@Test
63+
fun `castForumReturn passes through when raw is already an instance of OUT`() {
64+
// OUT=Int, value=42 (Int). outType==String fails, then
65+
// Int::class.java.isInstance(42) succeeds → cast through.
66+
val result = forum<String, Int> {
67+
participant(participant())
68+
captain(typedCaptainEmitting<Int>(mapOf("value" to 42)))
69+
}("topic")
70+
assertEquals(42, result)
71+
}
72+
73+
// L87-89 — raw is Map → constructFromMap → success
74+
75+
@Test
76+
fun `castForumReturn constructs Generable from Map raw value`() {
77+
val result = forum<String, ForumVerdict> {
78+
participant(participant())
79+
captain(typedCaptainEmitting<ForumVerdict>(mapOf("value" to mapOf("answer" to "hello"))))
80+
}("topic")
81+
assertEquals("hello", result.answer)
82+
}
83+
84+
// L89 ?: error — raw is Map but constructFromMap fails
85+
86+
@Test
87+
fun `castForumReturn errors when Map cannot be constructed into Generable`() {
88+
val ex = assertThrows<IllegalStateException> {
89+
forum<String, ForumVerdict> {
90+
participant(participant())
91+
captain(
92+
typedCaptainEmitting<ForumVerdict>(
93+
mapOf("value" to mapOf("wrongField" to "boom")),
94+
),
95+
)
96+
}("topic")
97+
}
98+
assertTrue(
99+
ex.message!!.contains("ForumVerdict") && ex.message!!.contains("could not be parsed"),
100+
"expected error to name the type: ${ex.message}",
101+
)
102+
}
103+
104+
// L91-93 — raw is String → fromLlmOutput → success
105+
106+
@Test
107+
fun `castForumReturn parses Generable from JSON String raw value`() {
108+
val result = forum<String, ForumVerdict> {
109+
participant(participant())
110+
captain(
111+
typedCaptainEmitting<ForumVerdict>(
112+
mapOf("value" to """{"answer":"world"}"""),
113+
),
114+
)
115+
}("topic")
116+
assertEquals("world", result.answer)
117+
}
118+
119+
// L93 ?: error — raw is String but fromLlmOutput fails
120+
121+
@Test
122+
fun `castForumReturn errors when String cannot be parsed as Generable`() {
123+
val ex = assertThrows<IllegalStateException> {
124+
forum<String, ForumVerdict> {
125+
participant(participant())
126+
captain(
127+
typedCaptainEmitting<ForumVerdict>(
128+
mapOf("value" to "not even close to JSON"),
129+
),
130+
)
131+
}("topic")
132+
}
133+
assertTrue(
134+
ex.message!!.contains("ForumVerdict"),
135+
"expected error to name the type: ${ex.message}",
136+
)
137+
}
138+
139+
// L95 — catch-all: raw is none of String/Map/instance-of-OUT
140+
141+
@Test
142+
fun `castForumReturn errors when raw is incompatible with OUT (catch-all)`() {
143+
val ex = assertThrows<IllegalStateException> {
144+
forum<String, ForumVerdict> {
145+
participant(participant())
146+
captain(
147+
// raw = Int 42, OUT = ForumVerdict
148+
// outType==String? no
149+
// ForumVerdict.java.isInstance(42)? no
150+
// raw is Map? no
151+
// raw is String? no
152+
// → catch-all error fires
153+
typedCaptainEmitting<ForumVerdict>(mapOf("value" to 42)),
154+
)
155+
}("topic")
156+
}
157+
assertTrue(
158+
ex.message!!.contains("incompatible"),
159+
"expected catch-all error wording: ${ex.message}",
160+
)
161+
assertTrue(
162+
ex.message!!.contains("ForumVerdict"),
163+
"error must name OUT type: ${ex.message}",
164+
)
165+
}
166+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package agents_engine.mcp
2+
3+
import agents_engine.core.agent
4+
import java.net.URI
5+
import java.net.http.HttpClient
6+
import java.net.http.HttpRequest
7+
import java.net.http.HttpResponse
8+
import kotlin.test.AfterTest
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertTrue
12+
13+
// Tests for #889 (catch-all) — McpServer error-path coverage.
14+
//
15+
// PIT NO_COVERAGE clusters in McpServer.handle / handleToolCall:
16+
// - L86: malformed JSON → 400
17+
// - L87: missing "method" → 400
18+
// - L107: internal exception → 500 (in the outer catch)
19+
// - L133: missing tool name in tools/call → -32602
20+
// - L135: unknown tool name → -32601
21+
// - L148: skill execution throws → isError:true response
22+
class McpServerErrorPathsTest {
23+
24+
private val toStop = mutableListOf<() -> Unit>()
25+
26+
@AfterTest fun cleanup() { toStop.forEach { runCatching { it() } } }
27+
28+
private fun trivialAgent() = agent<String, String>("greeter") {
29+
skills { skill<String, String>("greet", "Greets") { implementedBy { "hi $it" } } }
30+
}
31+
32+
private fun explodingAgent() = agent<String, String>("boomer") {
33+
skills {
34+
skill<String, String>("boom", "Always throws") {
35+
implementedBy { _ -> throw RuntimeException("kaboom") }
36+
}
37+
}
38+
}
39+
40+
private fun startServer(agent: agents_engine.core.Agent<*, *>, exposed: List<String>): McpServer {
41+
val server = McpServer.from(agent) { exposed.forEach { expose(it) }; port = 0 }.start()
42+
toStop.add { server.stop() }
43+
return server
44+
}
45+
46+
private fun postRaw(url: String, body: String): HttpResponse<String> {
47+
val req = HttpRequest.newBuilder()
48+
.uri(URI.create(url))
49+
.header("Content-Type", "application/json")
50+
.POST(HttpRequest.BodyPublishers.ofString(body))
51+
.build()
52+
return HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString())
53+
}
54+
55+
// L86 — malformed JSON body
56+
57+
@Test
58+
fun `malformed JSON body returns 400`() {
59+
val server = startServer(trivialAgent(), listOf("greet"))
60+
val r = postRaw(server.url, "not json at all")
61+
assertEquals(400, r.statusCode())
62+
}
63+
64+
// L87 — JSON without "method" field
65+
66+
@Test
67+
fun `JSON without method field returns 400`() {
68+
val server = startServer(trivialAgent(), listOf("greet"))
69+
val r = postRaw(server.url, """{"jsonrpc":"2.0","id":1}""")
70+
assertEquals(400, r.statusCode())
71+
}
72+
73+
// L107 — internal exception path. Hard to trigger directly (the outer
74+
// catch wraps anything that escapes the dispatcher). Sending a method
75+
// that the dispatcher can handle but with malformed `params` won't
76+
// exercise it because handlers tolerate empty params. Using a
77+
// `notifications/`-prefixed method exits early. The cleanest reachable
78+
// case: a method that LooksLikeJsonButIsn't valid for the parser
79+
// mid-deserialization. Skipping this branch — it's defensively wrapping
80+
// the entire dispatcher and only fires on truly unexpected runtime
81+
// errors. Documented but not exercised.
82+
83+
// L133 — tools/call without "name" parameter
84+
85+
@Test
86+
fun `tools-call without name returns JSON-RPC error code -32602`() {
87+
val server = startServer(trivialAgent(), listOf("greet"))
88+
val r = postRaw(
89+
server.url,
90+
"""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{}}""",
91+
)
92+
assertEquals(200, r.statusCode())
93+
assertTrue(r.body().contains("\"code\":-32602"), "body: ${r.body()}")
94+
assertTrue(
95+
r.body().contains("Missing tool name", ignoreCase = true),
96+
"body: ${r.body()}",
97+
)
98+
}
99+
100+
// L135 — tools/call with unknown tool name
101+
102+
@Test
103+
fun `tools-call with unknown tool name returns JSON-RPC error code -32601`() {
104+
val server = startServer(trivialAgent(), listOf("greet"))
105+
val r = postRaw(
106+
server.url,
107+
"""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"nonexistent"}}""",
108+
)
109+
assertEquals(200, r.statusCode())
110+
assertTrue(r.body().contains("\"code\":-32601"), "body: ${r.body()}")
111+
assertTrue(r.body().contains("nonexistent"), "body: ${r.body()}")
112+
}
113+
114+
// L148 — skill execution throws → isError:true response
115+
116+
@Test
117+
fun `tools-call where skill throws returns isError true with the exception message`() {
118+
val server = startServer(explodingAgent(), listOf("boom"))
119+
val r = postRaw(
120+
server.url,
121+
"""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"boom","arguments":{"input":"hi"}}}""",
122+
)
123+
assertEquals(200, r.statusCode(), "body: ${r.body()}")
124+
assertTrue(r.body().contains("\"isError\":true"), "must mark isError true; body: ${r.body()}")
125+
assertTrue(r.body().contains("kaboom"), "must include exception message; body: ${r.body()}")
126+
}
127+
128+
// Bonus — sanity that ordinary methods still work alongside these error tests.
129+
130+
@Test
131+
fun `unknown top-level method returns -32601 method not found`() {
132+
val server = startServer(trivialAgent(), listOf("greet"))
133+
val r = postRaw(
134+
server.url,
135+
"""{"jsonrpc":"2.0","id":1,"method":"completely/unknown"}""",
136+
)
137+
assertEquals(200, r.statusCode())
138+
assertTrue(r.body().contains("\"code\":-32601"), "body: ${r.body()}")
139+
assertTrue(r.body().contains("Method not found"), "body: ${r.body()}")
140+
}
141+
}

0 commit comments

Comments
 (0)