|
| 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