|
| 1 | +package com.itsaky.androidide.agent.repository |
| 2 | + |
| 3 | +import org.junit.Assert.assertEquals |
| 4 | +import org.junit.Assert.assertNotNull |
| 5 | +import org.junit.Assert.assertNull |
| 6 | +import org.junit.Test |
| 7 | +import org.junit.runner.RunWith |
| 8 | +import org.junit.runners.JUnit4 |
| 9 | + |
| 10 | +@RunWith(JUnit4::class) |
| 11 | +class UtilParseToolCallTest { |
| 12 | + |
| 13 | + private val allToolKeys = setOf( |
| 14 | + "list_files", "read_file", "search_project", |
| 15 | + "create_file", "update_file", "add_dependency", "get_current_datetime" |
| 16 | + ) |
| 17 | + |
| 18 | + // --- Happy path: clean JSON inside <tool_call> tags --- |
| 19 | + |
| 20 | + @Test |
| 21 | + fun `parses tool call with name and empty args from tool_call tags`() { |
| 22 | + val input = """<tool_call>{"name": "get_current_datetime", "args": {}}</tool_call>""" |
| 23 | + val result = Util.parseToolCall(input, allToolKeys) |
| 24 | + assertNotNull(result) |
| 25 | + assertEquals("get_current_datetime", result!!.name) |
| 26 | + assertEquals(emptyMap<String, String>(), result.args) |
| 27 | + } |
| 28 | + |
| 29 | + @Test |
| 30 | + fun `parses tool call with args from tool_call tags`() { |
| 31 | + val input = """<tool_call>{"name": "read_file", "args": {"file_path": "/src/main/Foo.kt"}}</tool_call>""" |
| 32 | + val result = Util.parseToolCall(input, allToolKeys) |
| 33 | + assertNotNull(result) |
| 34 | + assertEquals("read_file", result!!.name) |
| 35 | + assertEquals("/src/main/Foo.kt", result.args["file_path"]) |
| 36 | + } |
| 37 | + |
| 38 | + @Test |
| 39 | + fun `parses tool call for list_files with path arg`() { |
| 40 | + val input = """<tool_call>{"name": "list_files", "args": {"path": "/src"}}</tool_call>""" |
| 41 | + val result = Util.parseToolCall(input, allToolKeys) |
| 42 | + assertNotNull(result) |
| 43 | + assertEquals("list_files", result!!.name) |
| 44 | + assertEquals("/src", result.args["path"]) |
| 45 | + } |
| 46 | + |
| 47 | + @Test |
| 48 | + fun `parses tool call for search_project`() { |
| 49 | + val input = """<tool_call>{"name": "search_project", "args": {"query": "MainActivity"}}</tool_call>""" |
| 50 | + val result = Util.parseToolCall(input, allToolKeys) |
| 51 | + assertNotNull(result) |
| 52 | + assertEquals("search_project", result!!.name) |
| 53 | + assertEquals("MainActivity", result.args["query"]) |
| 54 | + } |
| 55 | + |
| 56 | + // --- Happy path: JSON without tool_call wrapper --- |
| 57 | + |
| 58 | + @Test |
| 59 | + fun `parses bare JSON object with name field`() { |
| 60 | + val input = """{"name": "list_files", "args": {"path": "."}}""" |
| 61 | + val result = Util.parseToolCall(input, allToolKeys) |
| 62 | + assertNotNull(result) |
| 63 | + assertEquals("list_files", result!!.name) |
| 64 | + } |
| 65 | + |
| 66 | + @Test |
| 67 | + fun `parses JSON wrapped in markdown code fence`() { |
| 68 | + val input = "```json\n{\"name\": \"read_file\", \"args\": {\"file_path\": \"/README.md\"}}\n```" |
| 69 | + val result = Util.parseToolCall(input, allToolKeys) |
| 70 | + assertNotNull(result) |
| 71 | + assertEquals("read_file", result!!.name) |
| 72 | + assertEquals("/README.md", result.args["file_path"]) |
| 73 | + } |
| 74 | + |
| 75 | + @Test |
| 76 | + fun `parses JSON wrapped in markdown code fence without json label`() { |
| 77 | + val input = "```\n{\"name\": \"list_files\", \"args\": {\"path\": \"/\"}}\n```" |
| 78 | + val result = Util.parseToolCall(input, allToolKeys) |
| 79 | + assertNotNull(result) |
| 80 | + assertEquals("list_files", result!!.name) |
| 81 | + } |
| 82 | + |
| 83 | + // --- Name aliases --- |
| 84 | + |
| 85 | + @Test |
| 86 | + fun `list_dir is resolved to list_files`() { |
| 87 | + val input = """<tool_call>{"name": "list_dir", "args": {"path": "/"}}</tool_call>""" |
| 88 | + val result = Util.parseToolCall(input, allToolKeys) |
| 89 | + assertNotNull(result) |
| 90 | + assertEquals("list_files", result!!.name) |
| 91 | + } |
| 92 | + |
| 93 | + // --- tool_name alternative field --- |
| 94 | + |
| 95 | + @Test |
| 96 | + fun `accepts tool_name field as alternative to name`() { |
| 97 | + val input = """{"tool_name": "list_files", "args": {"path": "."}}""" |
| 98 | + val result = Util.parseToolCall(input, allToolKeys) |
| 99 | + assertNotNull(result) |
| 100 | + assertEquals("list_files", result!!.name) |
| 101 | + } |
| 102 | + |
| 103 | + // --- Tool-only tag (model emits just the name inside tags) --- |
| 104 | + |
| 105 | + @Test |
| 106 | + fun `parses tool-only tag with no args`() { |
| 107 | + val input = "<tool_call>get_current_datetime</tool_call>" |
| 108 | + val result = Util.parseToolCall(input, allToolKeys) |
| 109 | + assertNotNull(result) |
| 110 | + assertEquals("get_current_datetime", result!!.name) |
| 111 | + } |
| 112 | + |
| 113 | + // --- Failure cases: unknown tool --- |
| 114 | + |
| 115 | + @Test |
| 116 | + fun `returns null when tool name is not in available tools`() { |
| 117 | + val input = """{"name": "unknown_tool", "args": {}}""" |
| 118 | + val result = Util.parseToolCall(input, allToolKeys) |
| 119 | + assertNull(result) |
| 120 | + } |
| 121 | + |
| 122 | + @Test |
| 123 | + fun `returns null when name field is missing`() { |
| 124 | + val input = """{"args": {"path": "/"}}""" |
| 125 | + val result = Util.parseToolCall(input, allToolKeys) |
| 126 | + assertNull(result) |
| 127 | + } |
| 128 | + |
| 129 | + @Test |
| 130 | + fun `returns null when name field is blank`() { |
| 131 | + val input = """{"name": "", "args": {}}""" |
| 132 | + val result = Util.parseToolCall(input, allToolKeys) |
| 133 | + assertNull(result) |
| 134 | + } |
| 135 | + |
| 136 | + // --- Failure cases: empty or unrecognizable input --- |
| 137 | + |
| 138 | + @Test |
| 139 | + fun `returns null for empty string`() { |
| 140 | + val result = Util.parseToolCall("", allToolKeys) |
| 141 | + assertNull(result) |
| 142 | + } |
| 143 | + |
| 144 | + @Test |
| 145 | + fun `returns null for plain prose`() { |
| 146 | + val result = Util.parseToolCall("Sure! I will list the files for you.", allToolKeys) |
| 147 | + assertNull(result) |
| 148 | + } |
| 149 | + |
| 150 | + @Test |
| 151 | + fun `returns null when no tools are provided and response has a tool call`() { |
| 152 | + val input = """{"name": "list_files", "args": {}}""" |
| 153 | + val result = Util.parseToolCall(input, emptySet()) |
| 154 | + assertNull(result) |
| 155 | + } |
| 156 | + |
| 157 | + // --- JSON with trailing prose (model hallucinates follow-on text) --- |
| 158 | + |
| 159 | + @Test |
| 160 | + fun `extracts first JSON object and ignores trailing text`() { |
| 161 | + val input = """{"name": "read_file", "args": {"file_path": "/Foo.kt"}} Let me read that file now.""" |
| 162 | + val result = Util.parseToolCall(input, allToolKeys) |
| 163 | + assertNotNull(result) |
| 164 | + assertEquals("read_file", result!!.name) |
| 165 | + } |
| 166 | + |
| 167 | + // --- Args with multiple values --- |
| 168 | + |
| 169 | + @Test |
| 170 | + fun `parses multiple args correctly`() { |
| 171 | + val input = """<tool_call>{"name": "create_file", "args": {"path": "/new.kt", "content": "class Foo {}"}}</tool_call>""" |
| 172 | + val result = Util.parseToolCall(input, allToolKeys) |
| 173 | + assertNotNull(result) |
| 174 | + assertEquals("create_file", result!!.name) |
| 175 | + assertEquals("/new.kt", result.args["path"]) |
| 176 | + assertEquals("class Foo {}", result.args["content"]) |
| 177 | + } |
| 178 | + |
| 179 | + // --- Empty tool set --- |
| 180 | + |
| 181 | + @Test |
| 182 | + fun `returns null when tool set is empty and input has JSON`() { |
| 183 | + val input = """{"name": "list_files", "args": {}}""" |
| 184 | + val result = Util.parseToolCall(input, emptySet()) |
| 185 | + assertNull(result) |
| 186 | + } |
| 187 | +} |
0 commit comments