Skip to content

Commit e37109d

Browse files
Skobeltsynclaude
andcommitted
test(#889): parseToolArguments — branch coverage across all 8 cases
OllamaClient.kt:26-69 `parseToolArguments` has 8 branches: null / Map / String-empty / String→Map / String→null / String→other / String→scalar / else. The existing OllamaClientIntegrationTest covered only the String→list case ("malformed list"). This file covers the other seven, killing PIT mutants on each branch. 10 tests, every branch of the conversion shim plus edge cases: - null input → empty map, no raw, no error - Map input → round-trips with String-coerced keys - Map with non-String keys → toString-coerced (kills the mutant that removes the .toString() call) - Empty + whitespace-only strings → empty map, raw preserved (kills the mutant that removes .trim()) - Valid JSON object string → canonical happy path - JSON-garbage string → empty map + parseError mentioning "JSON object" - Scalar JSON (e.g. "42") → distinct error path from garbage (catches mutant that conflates parsed-to-null with parsed-to-non-map) - Arbitrary non-String/Map/null value (Int, List) → `else` branch, rawArguments = .toString() of input Closes out the OllamaClient cluster (2 methods / 6 lines) from the #889 mutant list. Combined with previous #889 batches tonight: - McpServerLifecycleTest (60bcc53) — 8 tests, 6-8 mutants - McpRunnerMissingFlagValueTest (72d3cba) — 5 tests, ~3 mutants - LenientJsonParserUnterminatedTest (72d3cba) — 9 tests, ~5 mutants - ParseToolArgumentsBranchTest (this) — 10 tests, ~6 mutants That's ~20-22 of the ~30 mutants from #889's cluster list addressed. The remaining clusters per the issue body are equivalent-mutant noise on synthetic Kotlin getters (issue explicitly says not to chase those). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b634711 commit e37109d

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package agents_engine.model
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertNotNull
6+
import kotlin.test.assertNull
7+
8+
// Tests for #889 — `parseToolArguments` (OllamaClient.kt:26-69) has 8 branches:
9+
// null / Map / String-empty / String→Map / String→null / String→other (e.g. list) /
10+
// String→scalar / else. The existing OllamaClientIntegrationTest covers only the
11+
// String→list case ("malformed list"). This file covers the other seven, killing
12+
// PIT mutants on each branch.
13+
//
14+
// Internal API tested directly because the function is the conversion shim every
15+
// tool-call argument value goes through; misbehavior on any branch silently
16+
// corrupts tool dispatch.
17+
class ParseToolArgumentsBranchTest {
18+
19+
@Test
20+
fun `null raw args produces empty map with no rawArguments and no parseError`() {
21+
val parsed = parseToolArguments(null)
22+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
23+
assertNull(parsed.rawArguments, "null input → no raw to remember")
24+
assertNull(parsed.parseError, "null input is canonical (no args), not an error")
25+
}
26+
27+
@Test
28+
fun `map raw args round-trips with String-coerced keys`() {
29+
val raw = mapOf("name" to "Alice", "count" to 3)
30+
val parsed = parseToolArguments(raw)
31+
assertEquals("Alice", parsed.arguments["name"])
32+
assertEquals(3, parsed.arguments["count"])
33+
assertNull(parsed.rawArguments, "Map input → no raw text to remember")
34+
assertNull(parsed.parseError, "valid Map is canonical, not an error")
35+
}
36+
37+
@Test
38+
fun `map with non-String keys gets coerced to String via toString`() {
39+
// Kills the mutant that swaps `.toString()` for `.javaClass.simpleName` or similar.
40+
val raw = mapOf(42 to "answer", true to "yes")
41+
val parsed = parseToolArguments(raw)
42+
assertEquals("answer", parsed.arguments["42"], "non-String keys must be toString-coerced")
43+
assertEquals("yes", parsed.arguments["true"])
44+
}
45+
46+
@Test
47+
fun `empty string preserves the raw value and produces no error`() {
48+
// OllamaClient.kt:42-43 — `trimmed.isEmpty()` branch. Kills the mutant
49+
// that flips the condition to .isNotEmpty() (would route to parser
50+
// path which then errors on empty input).
51+
val parsed = parseToolArguments("")
52+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
53+
assertEquals("", parsed.rawArguments, "empty string raw value preserved")
54+
assertNull(parsed.parseError, "empty string is canonical empty-args, not an error")
55+
}
56+
57+
@Test
58+
fun `whitespace-only string is treated as empty`() {
59+
// .trim().isEmpty() branch — kills the mutant that removes the .trim() call.
60+
val parsed = parseToolArguments(" \n\t ")
61+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
62+
assertEquals(" \n\t ", parsed.rawArguments, "raw is the ORIGINAL untrimmed string")
63+
assertNull(parsed.parseError)
64+
}
65+
66+
@Test
67+
fun `valid JSON object string parses canonically`() {
68+
// The happy path for the LLM emitting `arguments: "{\"name\":\"Alice\"}"`.
69+
val parsed = parseToolArguments("""{"name": "Alice", "count": 3}""")
70+
assertEquals("Alice", parsed.arguments["name"])
71+
assertEquals(3, parsed.arguments["count"])
72+
assertEquals("""{"name": "Alice", "count": 3}""", parsed.rawArguments)
73+
assertNull(parsed.parseError)
74+
}
75+
76+
@Test
77+
fun `JSON string that parses to null surfaces a parse-error message`() {
78+
// Lenient parser returns null on garbage. Kills the mutant that
79+
// swaps the error message OR removes the parseError field.
80+
val parsed = parseToolArguments("not valid json at all")
81+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
82+
assertEquals("not valid json at all", parsed.rawArguments, "raw preserved for repair-loop replay")
83+
assertNotNull(parsed.parseError)
84+
assert(parsed.parseError!!.contains("JSON object", ignoreCase = true)) {
85+
"error message should explain the expected shape: '${parsed.parseError}'"
86+
}
87+
}
88+
89+
@Test
90+
fun `JSON string that parses to a non-object surfaces a parse-error message`() {
91+
// The case the existing integration test covers — but specifically the
92+
// SCALAR case (parser returns an Int / Double / Boolean / String), not
93+
// the list case. Catches the mutant that conflates "parsed-to-non-map"
94+
// with "parse-failed" (different code paths, different errors).
95+
val parsed = parseToolArguments("42")
96+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
97+
assertEquals("42", parsed.rawArguments, "raw preserved for repair-loop replay")
98+
assertNotNull(parsed.parseError)
99+
assert(parsed.parseError!!.contains("JSON object")) {
100+
"error should explain that scalars are rejected: '${parsed.parseError}'"
101+
}
102+
}
103+
104+
@Test
105+
fun `arbitrary non-String non-Map non-null value surfaces a generic parse error`() {
106+
// The `else` branch at line 64-68. Catches the mutant that removes the
107+
// `rawArguments = rawArgs.toString()` field assignment.
108+
val parsed = parseToolArguments(42)
109+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
110+
assertEquals("42", parsed.rawArguments, "raw is the .toString() of the input")
111+
assertNotNull(parsed.parseError)
112+
assert(parsed.parseError!!.contains("JSON object")) {
113+
"error should explain: '${parsed.parseError}'"
114+
}
115+
}
116+
117+
@Test
118+
fun `arbitrary non-String non-Map non-null value also handles List`() {
119+
// Lists arriving as the raw value (not as a JSON-string-containing-list)
120+
// hit the same `else` branch. Different from the String-with-list-content
121+
// case the existing integration test covers.
122+
val parsed = parseToolArguments(listOf("Alice", "Bob"))
123+
assertEquals(emptyMap<String, Any?>(), parsed.arguments)
124+
assertNotNull(parsed.rawArguments)
125+
assertNotNull(parsed.parseError)
126+
}
127+
}

0 commit comments

Comments
 (0)