|
6 | 6 | filterOutHeadlessToolInterrupts, |
7 | 7 | flushPendingHeadlessToolInterrupts, |
8 | 8 | headlessToolResumeCommand, |
| 9 | + parseHeadlessToolInterruptPayload, |
9 | 10 | } from "../headless-tools.js"; |
10 | 11 |
|
11 | 12 | async function flushMicrotasks(count = 4) { |
@@ -142,6 +143,54 @@ describe("headless tool interrupt helpers", () => { |
142 | 143 | ]); |
143 | 144 | }); |
144 | 145 |
|
| 146 | + it("treats Python snake_case tool_call as headless for filtering", () => { |
| 147 | + const interrupts = [ |
| 148 | + { |
| 149 | + id: "tool-int", |
| 150 | + value: { |
| 151 | + type: "tool" as const, |
| 152 | + tool_call: { |
| 153 | + id: "call-1", |
| 154 | + name: "geolocation_get", |
| 155 | + args: { high_accuracy: null }, |
| 156 | + }, |
| 157 | + }, |
| 158 | + }, |
| 159 | + { |
| 160 | + id: "hitl-int", |
| 161 | + value: { |
| 162 | + action_requests: [ |
| 163 | + { action_name: "approve", args: {}, description: "" }, |
| 164 | + ], |
| 165 | + }, |
| 166 | + }, |
| 167 | + ]; |
| 168 | + |
| 169 | + expect(filterOutHeadlessToolInterrupts(interrupts)).toEqual([ |
| 170 | + interrupts[1], |
| 171 | + ]); |
| 172 | + }); |
| 173 | + |
| 174 | + it("normalizes Python tool_call via parseHeadlessToolInterruptPayload", () => { |
| 175 | + expect( |
| 176 | + parseHeadlessToolInterruptPayload({ |
| 177 | + type: "tool", |
| 178 | + tool_call: { |
| 179 | + id: "call_heTfkJwAH7gjuxHXMANzQKTJ", |
| 180 | + name: "geolocation_get", |
| 181 | + args: { high_accuracy: null }, |
| 182 | + }, |
| 183 | + }) |
| 184 | + ).toEqual({ |
| 185 | + type: "tool", |
| 186 | + toolCall: { |
| 187 | + id: "call_heTfkJwAH7gjuxHXMANzQKTJ", |
| 188 | + name: "geolocation_get", |
| 189 | + args: { high_accuracy: null }, |
| 190 | + }, |
| 191 | + }); |
| 192 | + }); |
| 193 | + |
145 | 194 | it("builds a keyed resume command for tool call results", () => { |
146 | 195 | expect( |
147 | 196 | headlessToolResumeCommand({ |
@@ -237,4 +286,46 @@ describe("headless tool interrupt helpers", () => { |
237 | 286 |
|
238 | 287 | expect(resumeSubmit).not.toHaveBeenCalled(); |
239 | 288 | }); |
| 289 | + |
| 290 | + it("flushes headless tool interrupts serialized with Python tool_call", async () => { |
| 291 | + const handled = new Set<string>(); |
| 292 | + const onTool = vi.fn(); |
| 293 | + const resumeSubmit = vi.fn(); |
| 294 | + |
| 295 | + flushPendingHeadlessToolInterrupts( |
| 296 | + { |
| 297 | + __interrupt__: [ |
| 298 | + { |
| 299 | + id: "py-headless", |
| 300 | + value: { |
| 301 | + type: "tool", |
| 302 | + tool_call: { |
| 303 | + id: "call-1", |
| 304 | + name: "get_location", |
| 305 | + args: { high_accuracy: false }, |
| 306 | + }, |
| 307 | + }, |
| 308 | + }, |
| 309 | + ], |
| 310 | + }, |
| 311 | + [ |
| 312 | + { |
| 313 | + tool: { name: "get_location" }, |
| 314 | + execute: async () => ({ latitude: 1, longitude: 2 }), |
| 315 | + }, |
| 316 | + ], |
| 317 | + handled, |
| 318 | + { onTool, resumeSubmit } |
| 319 | + ); |
| 320 | + |
| 321 | + await flushMicrotasks(); |
| 322 | + |
| 323 | + expect(resumeSubmit).toHaveBeenCalledWith({ |
| 324 | + resume: { |
| 325 | + "call-1": { latitude: 1, longitude: 2 }, |
| 326 | + }, |
| 327 | + }); |
| 328 | + expect(handled.has("py-headless")).toBe(true); |
| 329 | + expect(onTool).toHaveBeenCalledTimes(2); |
| 330 | + }); |
240 | 331 | }); |
0 commit comments