NativeToolCallParser.spec.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { NativeToolCallParser } from "../NativeToolCallParser"
  2. describe("NativeToolCallParser", () => {
  3. beforeEach(() => {
  4. NativeToolCallParser.clearAllStreamingToolCalls()
  5. NativeToolCallParser.clearRawChunkState()
  6. })
  7. describe("parseToolCall", () => {
  8. describe("read_file tool", () => {
  9. it("should handle line_ranges as tuples (new format)", () => {
  10. const toolCall = {
  11. id: "toolu_123",
  12. name: "read_file" as const,
  13. arguments: JSON.stringify({
  14. files: [
  15. {
  16. path: "src/core/task/Task.ts",
  17. line_ranges: [
  18. [1920, 1990],
  19. [2060, 2120],
  20. ],
  21. },
  22. ],
  23. }),
  24. }
  25. const result = NativeToolCallParser.parseToolCall(toolCall)
  26. expect(result).not.toBeNull()
  27. expect(result?.type).toBe("tool_use")
  28. if (result?.type === "tool_use") {
  29. expect(result.nativeArgs).toBeDefined()
  30. const nativeArgs = result.nativeArgs as {
  31. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  32. }
  33. expect(nativeArgs.files).toHaveLength(1)
  34. expect(nativeArgs.files[0].path).toBe("src/core/task/Task.ts")
  35. expect(nativeArgs.files[0].lineRanges).toEqual([
  36. { start: 1920, end: 1990 },
  37. { start: 2060, end: 2120 },
  38. ])
  39. }
  40. })
  41. it("should handle line_ranges as strings (legacy format)", () => {
  42. const toolCall = {
  43. id: "toolu_123",
  44. name: "read_file" as const,
  45. arguments: JSON.stringify({
  46. files: [
  47. {
  48. path: "src/core/task/Task.ts",
  49. line_ranges: ["1920-1990", "2060-2120"],
  50. },
  51. ],
  52. }),
  53. }
  54. const result = NativeToolCallParser.parseToolCall(toolCall)
  55. expect(result).not.toBeNull()
  56. expect(result?.type).toBe("tool_use")
  57. if (result?.type === "tool_use") {
  58. expect(result.nativeArgs).toBeDefined()
  59. const nativeArgs = result.nativeArgs as {
  60. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  61. }
  62. expect(nativeArgs.files).toHaveLength(1)
  63. expect(nativeArgs.files[0].path).toBe("src/core/task/Task.ts")
  64. expect(nativeArgs.files[0].lineRanges).toEqual([
  65. { start: 1920, end: 1990 },
  66. { start: 2060, end: 2120 },
  67. ])
  68. }
  69. })
  70. it("should handle files without line_ranges", () => {
  71. const toolCall = {
  72. id: "toolu_123",
  73. name: "read_file" as const,
  74. arguments: JSON.stringify({
  75. files: [
  76. {
  77. path: "src/utils.ts",
  78. },
  79. ],
  80. }),
  81. }
  82. const result = NativeToolCallParser.parseToolCall(toolCall)
  83. expect(result).not.toBeNull()
  84. expect(result?.type).toBe("tool_use")
  85. if (result?.type === "tool_use") {
  86. const nativeArgs = result.nativeArgs as {
  87. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  88. }
  89. expect(nativeArgs.files).toHaveLength(1)
  90. expect(nativeArgs.files[0].path).toBe("src/utils.ts")
  91. expect(nativeArgs.files[0].lineRanges).toBeUndefined()
  92. }
  93. })
  94. it("should handle multiple files with different line_ranges", () => {
  95. const toolCall = {
  96. id: "toolu_123",
  97. name: "read_file" as const,
  98. arguments: JSON.stringify({
  99. files: [
  100. {
  101. path: "file1.ts",
  102. line_ranges: ["1-50"],
  103. },
  104. {
  105. path: "file2.ts",
  106. line_ranges: ["100-150", "200-250"],
  107. },
  108. {
  109. path: "file3.ts",
  110. },
  111. ],
  112. }),
  113. }
  114. const result = NativeToolCallParser.parseToolCall(toolCall)
  115. expect(result).not.toBeNull()
  116. expect(result?.type).toBe("tool_use")
  117. if (result?.type === "tool_use") {
  118. const nativeArgs = result.nativeArgs as {
  119. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  120. }
  121. expect(nativeArgs.files).toHaveLength(3)
  122. expect(nativeArgs.files[0].lineRanges).toEqual([{ start: 1, end: 50 }])
  123. expect(nativeArgs.files[1].lineRanges).toEqual([
  124. { start: 100, end: 150 },
  125. { start: 200, end: 250 },
  126. ])
  127. expect(nativeArgs.files[2].lineRanges).toBeUndefined()
  128. }
  129. })
  130. it("should filter out invalid line_range strings", () => {
  131. const toolCall = {
  132. id: "toolu_123",
  133. name: "read_file" as const,
  134. arguments: JSON.stringify({
  135. files: [
  136. {
  137. path: "file.ts",
  138. line_ranges: ["1-50", "invalid", "100-200", "abc-def"],
  139. },
  140. ],
  141. }),
  142. }
  143. const result = NativeToolCallParser.parseToolCall(toolCall)
  144. expect(result).not.toBeNull()
  145. expect(result?.type).toBe("tool_use")
  146. if (result?.type === "tool_use") {
  147. const nativeArgs = result.nativeArgs as {
  148. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  149. }
  150. expect(nativeArgs.files[0].lineRanges).toEqual([
  151. { start: 1, end: 50 },
  152. { start: 100, end: 200 },
  153. ])
  154. }
  155. })
  156. })
  157. })
  158. describe("processStreamingChunk", () => {
  159. describe("read_file tool", () => {
  160. it("should convert line_ranges strings to lineRanges objects during streaming", () => {
  161. const id = "toolu_streaming_123"
  162. NativeToolCallParser.startStreamingToolCall(id, "read_file")
  163. // Simulate streaming chunks
  164. const fullArgs = JSON.stringify({
  165. files: [
  166. {
  167. path: "src/test.ts",
  168. line_ranges: ["10-20", "30-40"],
  169. },
  170. ],
  171. })
  172. // Process the complete args as a single chunk for simplicity
  173. const result = NativeToolCallParser.processStreamingChunk(id, fullArgs)
  174. expect(result).not.toBeNull()
  175. expect(result?.nativeArgs).toBeDefined()
  176. const nativeArgs = result?.nativeArgs as {
  177. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  178. }
  179. expect(nativeArgs.files).toHaveLength(1)
  180. expect(nativeArgs.files[0].lineRanges).toEqual([
  181. { start: 10, end: 20 },
  182. { start: 30, end: 40 },
  183. ])
  184. })
  185. })
  186. })
  187. describe("finalizeStreamingToolCall", () => {
  188. describe("read_file tool", () => {
  189. it("should convert line_ranges strings to lineRanges objects on finalize", () => {
  190. const id = "toolu_finalize_123"
  191. NativeToolCallParser.startStreamingToolCall(id, "read_file")
  192. // Add the complete arguments
  193. NativeToolCallParser.processStreamingChunk(
  194. id,
  195. JSON.stringify({
  196. files: [
  197. {
  198. path: "finalized.ts",
  199. line_ranges: ["500-600"],
  200. },
  201. ],
  202. }),
  203. )
  204. const result = NativeToolCallParser.finalizeStreamingToolCall(id)
  205. expect(result).not.toBeNull()
  206. expect(result?.type).toBe("tool_use")
  207. if (result?.type === "tool_use") {
  208. const nativeArgs = result.nativeArgs as {
  209. files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }>
  210. }
  211. expect(nativeArgs.files[0].path).toBe("finalized.ts")
  212. expect(nativeArgs.files[0].lineRanges).toEqual([{ start: 500, end: 600 }])
  213. }
  214. })
  215. })
  216. })
  217. })