newTaskTool.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. // npx vitest core/tools/__tests__/newTaskTool.spec.ts
  2. import type { AskApproval, HandleError, NativeToolArgs, ToolUse } from "../../../shared/tools"
  3. // Mock vscode module
  4. vi.mock("vscode", () => ({
  5. workspace: {
  6. getConfiguration: vi.fn(() => ({
  7. get: vi.fn(),
  8. })),
  9. },
  10. }))
  11. // Mock Package module
  12. vi.mock("../../../shared/package", () => ({
  13. Package: {
  14. name: "roo-cline",
  15. publisher: "RooVeterinaryInc",
  16. version: "1.0.0",
  17. outputChannel: "Roo-Code",
  18. },
  19. }))
  20. // Mock other modules first - these are hoisted to the top
  21. vi.mock("../../../shared/modes", () => ({
  22. getModeBySlug: vi.fn(),
  23. defaultModeSlug: "ask",
  24. }))
  25. vi.mock("../../prompts/responses", () => ({
  26. formatResponse: {
  27. toolError: vi.fn((msg: string) => `Tool Error: ${msg}`),
  28. },
  29. }))
  30. vi.mock("../updateTodoListTool", () => ({
  31. parseMarkdownChecklist: vi.fn((md: string) => {
  32. // Simple mock implementation
  33. const lines = md.split("\n").filter((line) => line.trim())
  34. return lines.map((line, index) => {
  35. let status = "pending"
  36. let content = line
  37. if (line.includes("[x]") || line.includes("[X]")) {
  38. status = "completed"
  39. content = line.replace(/^\[x\]\s*/i, "")
  40. } else if (line.includes("[-]") || line.includes("[~]")) {
  41. status = "in_progress"
  42. content = line.replace(/^\[-\]\s*/, "").replace(/^\[~\]\s*/, "")
  43. } else {
  44. content = line.replace(/^\[\s*\]\s*/, "")
  45. }
  46. return {
  47. id: `todo-${index}`,
  48. content,
  49. status,
  50. }
  51. })
  52. }),
  53. }))
  54. // Define a minimal type for the resolved value
  55. type MockClineInstance = { taskId: string }
  56. // Mock dependencies after modules are mocked
  57. const mockAskApproval = vi.fn<AskApproval>()
  58. const mockHandleError = vi.fn<HandleError>()
  59. const mockPushToolResult = vi.fn()
  60. const mockEmit = vi.fn()
  61. const mockRecordToolError = vi.fn()
  62. const mockSayAndCreateMissingParamError = vi.fn()
  63. const mockStartSubtask = vi
  64. .fn<(message: string, todoItems: any[], mode: string) => Promise<MockClineInstance>>()
  65. .mockResolvedValue({ taskId: "mock-subtask-id" })
  66. // Adapter to satisfy legacy expectations while exercising new delegation path
  67. const mockDelegateParentAndOpenChild = vi.fn(
  68. async (args: { parentTaskId: string; message: string; initialTodos: any[]; mode: string }) => {
  69. // Call legacy spy so existing expectations still pass
  70. await mockStartSubtask(args.message, args.initialTodos, args.mode)
  71. return { taskId: "child-1" }
  72. },
  73. )
  74. const mockCheckpointSave = vi.fn()
  75. // Mock the Cline instance and its methods/properties
  76. const mockCline = {
  77. ask: vi.fn(),
  78. sayAndCreateMissingParamError: mockSayAndCreateMissingParamError,
  79. emit: mockEmit,
  80. recordToolError: mockRecordToolError,
  81. consecutiveMistakeCount: 0,
  82. isPaused: false,
  83. pausedModeSlug: "ask",
  84. taskId: "mock-parent-task-id",
  85. enableCheckpoints: false,
  86. checkpointSave: mockCheckpointSave,
  87. startSubtask: mockStartSubtask,
  88. providerRef: {
  89. deref: vi.fn(() => ({
  90. getState: vi.fn(() => ({ customModes: [], mode: "ask" })),
  91. handleModeSwitch: vi.fn(),
  92. delegateParentAndOpenChild: mockDelegateParentAndOpenChild,
  93. })),
  94. },
  95. }
  96. // Import the class to test AFTER mocks are set up
  97. import { newTaskTool } from "../NewTaskTool"
  98. import { getModeBySlug } from "../../../shared/modes"
  99. import * as vscode from "vscode"
  100. const withNativeArgs = (block: ToolUse<"new_task">): ToolUse<"new_task"> => ({
  101. ...block,
  102. // Native tool calling: `nativeArgs` is the source of truth for tool execution.
  103. // These tests intentionally exercise missing-param behavior, so we allow undefined
  104. // values and let the tool's runtime validation handle it.
  105. nativeArgs: {
  106. mode: block.params.mode,
  107. message: block.params.message,
  108. todos: block.params.todos,
  109. } as unknown as NativeToolArgs["new_task"],
  110. })
  111. describe("newTaskTool", () => {
  112. beforeEach(() => {
  113. // Reset mocks before each test
  114. vi.clearAllMocks()
  115. mockAskApproval.mockResolvedValue(true) // Default to approved
  116. vi.mocked(getModeBySlug).mockReturnValue({
  117. slug: "code",
  118. name: "Code Mode",
  119. roleDefinition: "Test role definition",
  120. groups: ["command", "read", "edit"],
  121. }) // Default valid mode
  122. mockCline.consecutiveMistakeCount = 0
  123. mockCline.isPaused = false
  124. // Default: VSCode setting is disabled
  125. const mockGet = vi.fn().mockReturnValue(false)
  126. vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
  127. get: mockGet,
  128. } as any)
  129. })
  130. it("should correctly un-escape \\\\@ to \\@ in the message passed to the new task", async () => {
  131. const block: ToolUse<"new_task"> = {
  132. type: "tool_use", // Add required 'type' property
  133. name: "new_task", // Correct property name
  134. params: {
  135. mode: "code",
  136. message: "Review this: \\\\@file1.txt and also \\\\\\\\@file2.txt", // Input with \\@ and \\\\@
  137. todos: "[ ] First task\n[ ] Second task",
  138. },
  139. partial: false,
  140. }
  141. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  142. askApproval: mockAskApproval,
  143. handleError: mockHandleError,
  144. pushToolResult: mockPushToolResult,
  145. })
  146. // Verify askApproval was called
  147. expect(mockAskApproval).toHaveBeenCalled()
  148. // Verify the message passed to startSubtask reflects the code's behavior in unit tests
  149. expect(mockStartSubtask).toHaveBeenCalledWith(
  150. "Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@
  151. expect.arrayContaining([
  152. expect.objectContaining({ content: "First task" }),
  153. expect.objectContaining({ content: "Second task" }),
  154. ]),
  155. "code",
  156. )
  157. // Verify side effects
  158. expect(mockPushToolResult).not.toHaveBeenCalled()
  159. })
  160. it("should not un-escape single escaped \@", async () => {
  161. const block: ToolUse<"new_task"> = {
  162. type: "tool_use", // Add required 'type' property
  163. name: "new_task", // Correct property name
  164. params: {
  165. mode: "code",
  166. message: "This is already unescaped: \\@file1.txt",
  167. todos: "[ ] Test todo",
  168. },
  169. partial: false,
  170. }
  171. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  172. askApproval: mockAskApproval,
  173. handleError: mockHandleError,
  174. pushToolResult: mockPushToolResult,
  175. })
  176. expect(mockStartSubtask).toHaveBeenCalledWith(
  177. "This is already unescaped: \\@file1.txt", // Expected: \@ remains \@
  178. expect.any(Array),
  179. "code",
  180. )
  181. })
  182. it("should not un-escape non-escaped @", async () => {
  183. const block: ToolUse<"new_task"> = {
  184. type: "tool_use", // Add required 'type' property
  185. name: "new_task", // Correct property name
  186. params: {
  187. mode: "code",
  188. message: "A normal mention @file1.txt",
  189. todos: "[ ] Test todo",
  190. },
  191. partial: false,
  192. }
  193. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  194. askApproval: mockAskApproval,
  195. handleError: mockHandleError,
  196. pushToolResult: mockPushToolResult,
  197. })
  198. expect(mockStartSubtask).toHaveBeenCalledWith(
  199. "A normal mention @file1.txt", // Expected: @ remains @
  200. expect.any(Array),
  201. "code",
  202. )
  203. })
  204. it("should handle mixed escaping scenarios", async () => {
  205. const block: ToolUse<"new_task"> = {
  206. type: "tool_use", // Add required 'type' property
  207. name: "new_task", // Correct property name
  208. params: {
  209. mode: "code",
  210. message: "Mix: @file0.txt, \\@file1.txt, \\\\@file2.txt, \\\\\\\\@file3.txt",
  211. todos: "[ ] Test todo",
  212. },
  213. partial: false,
  214. }
  215. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  216. askApproval: mockAskApproval,
  217. handleError: mockHandleError,
  218. pushToolResult: mockPushToolResult,
  219. })
  220. expect(mockStartSubtask).toHaveBeenCalledWith(
  221. "Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@
  222. expect.any(Array),
  223. "code",
  224. )
  225. })
  226. it("should handle missing todos parameter gracefully (backward compatibility)", async () => {
  227. const block: ToolUse<"new_task"> = {
  228. type: "tool_use",
  229. name: "new_task",
  230. params: {
  231. mode: "code",
  232. message: "Test message",
  233. // todos missing - should work for backward compatibility
  234. },
  235. partial: false,
  236. }
  237. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  238. askApproval: mockAskApproval,
  239. handleError: mockHandleError,
  240. pushToolResult: mockPushToolResult,
  241. })
  242. // Should NOT error when todos is missing
  243. expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
  244. expect(mockCline.consecutiveMistakeCount).toBe(0)
  245. expect(mockCline.recordToolError).not.toHaveBeenCalledWith("new_task")
  246. // Should create task with empty todos array
  247. expect(mockStartSubtask).toHaveBeenCalledWith("Test message", [], "code")
  248. // Should complete successfully
  249. expect(mockPushToolResult).not.toHaveBeenCalled()
  250. })
  251. it("should work with todos parameter when provided", async () => {
  252. const block: ToolUse<"new_task"> = {
  253. type: "tool_use",
  254. name: "new_task",
  255. params: {
  256. mode: "code",
  257. message: "Test message with todos",
  258. todos: "[ ] First task\n[ ] Second task",
  259. },
  260. partial: false,
  261. }
  262. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  263. askApproval: mockAskApproval,
  264. handleError: mockHandleError,
  265. pushToolResult: mockPushToolResult,
  266. })
  267. // Should parse and include todos when provided
  268. expect(mockStartSubtask).toHaveBeenCalledWith(
  269. "Test message with todos",
  270. expect.arrayContaining([
  271. expect.objectContaining({ content: "First task" }),
  272. expect.objectContaining({ content: "Second task" }),
  273. ]),
  274. "code",
  275. )
  276. expect(mockPushToolResult).not.toHaveBeenCalled()
  277. })
  278. it("should error when mode parameter is missing", async () => {
  279. const block: ToolUse<"new_task"> = {
  280. type: "tool_use",
  281. name: "new_task",
  282. params: {
  283. // mode missing
  284. message: "Test message",
  285. todos: "[ ] Test todo",
  286. },
  287. partial: false,
  288. }
  289. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  290. askApproval: mockAskApproval,
  291. handleError: mockHandleError,
  292. pushToolResult: mockPushToolResult,
  293. })
  294. expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "mode")
  295. expect(mockCline.consecutiveMistakeCount).toBe(1)
  296. expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
  297. })
  298. it("should error when message parameter is missing", async () => {
  299. const block: ToolUse<"new_task"> = {
  300. type: "tool_use",
  301. name: "new_task",
  302. params: {
  303. mode: "code",
  304. // message missing
  305. todos: "[ ] Test todo",
  306. },
  307. partial: false,
  308. }
  309. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  310. askApproval: mockAskApproval,
  311. handleError: mockHandleError,
  312. pushToolResult: mockPushToolResult,
  313. })
  314. expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "message")
  315. expect(mockCline.consecutiveMistakeCount).toBe(1)
  316. expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
  317. })
  318. it("should parse todos with different statuses correctly", async () => {
  319. const block: ToolUse<"new_task"> = {
  320. type: "tool_use",
  321. name: "new_task",
  322. params: {
  323. mode: "code",
  324. message: "Test message",
  325. todos: "[ ] Pending task\n[x] Completed task\n[-] In progress task",
  326. },
  327. partial: false,
  328. }
  329. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  330. askApproval: mockAskApproval,
  331. handleError: mockHandleError,
  332. pushToolResult: mockPushToolResult,
  333. })
  334. expect(mockStartSubtask).toHaveBeenCalledWith(
  335. "Test message",
  336. expect.arrayContaining([
  337. expect.objectContaining({ content: "Pending task", status: "pending" }),
  338. expect.objectContaining({ content: "Completed task", status: "completed" }),
  339. expect.objectContaining({ content: "In progress task", status: "in_progress" }),
  340. ]),
  341. "code",
  342. )
  343. })
  344. describe("VSCode setting: newTaskRequireTodos", () => {
  345. it("should NOT require todos when VSCode setting is disabled (default)", async () => {
  346. // Ensure VSCode setting is disabled
  347. const mockGet = vi.fn().mockReturnValue(false)
  348. vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
  349. get: mockGet,
  350. } as any)
  351. const block: ToolUse<"new_task"> = {
  352. type: "tool_use",
  353. name: "new_task",
  354. params: {
  355. mode: "code",
  356. message: "Test message",
  357. // todos missing - should work when setting is disabled
  358. },
  359. partial: false,
  360. }
  361. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  362. askApproval: mockAskApproval,
  363. handleError: mockHandleError,
  364. pushToolResult: mockPushToolResult,
  365. })
  366. // Should NOT error when todos is missing and setting is disabled
  367. expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
  368. expect(mockCline.consecutiveMistakeCount).toBe(0)
  369. expect(mockCline.recordToolError).not.toHaveBeenCalledWith("new_task")
  370. // Should create task with empty todos array
  371. expect(mockStartSubtask).toHaveBeenCalledWith("Test message", [], "code")
  372. // Should complete successfully
  373. expect(mockPushToolResult).not.toHaveBeenCalled()
  374. })
  375. it("should REQUIRE todos when VSCode setting is enabled", async () => {
  376. // Enable VSCode setting
  377. const mockGet = vi.fn().mockReturnValue(true)
  378. vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
  379. get: mockGet,
  380. } as any)
  381. const block: ToolUse<"new_task"> = {
  382. type: "tool_use",
  383. name: "new_task",
  384. params: {
  385. mode: "code",
  386. message: "Test message",
  387. // todos missing - should error when setting is enabled
  388. },
  389. partial: false,
  390. }
  391. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  392. askApproval: mockAskApproval,
  393. handleError: mockHandleError,
  394. pushToolResult: mockPushToolResult,
  395. })
  396. // Should error when todos is missing and setting is enabled
  397. expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "todos")
  398. expect(mockCline.consecutiveMistakeCount).toBe(1)
  399. expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
  400. // Should NOT create task
  401. expect(mockStartSubtask).not.toHaveBeenCalled()
  402. expect(mockPushToolResult).not.toHaveBeenCalledWith(
  403. expect.stringContaining("Successfully created new task"),
  404. )
  405. })
  406. it("should work with todos when VSCode setting is enabled", async () => {
  407. // Enable VSCode setting
  408. const mockGet = vi.fn().mockReturnValue(true)
  409. vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
  410. get: mockGet,
  411. } as any)
  412. const block: ToolUse<"new_task"> = {
  413. type: "tool_use",
  414. name: "new_task",
  415. params: {
  416. mode: "code",
  417. message: "Test message",
  418. todos: "[ ] First task\n[ ] Second task",
  419. },
  420. partial: false,
  421. }
  422. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  423. askApproval: mockAskApproval,
  424. handleError: mockHandleError,
  425. pushToolResult: mockPushToolResult,
  426. })
  427. // Should NOT error when todos is provided and setting is enabled
  428. expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
  429. expect(mockCline.consecutiveMistakeCount).toBe(0)
  430. // Should create task with parsed todos
  431. expect(mockStartSubtask).toHaveBeenCalledWith(
  432. "Test message",
  433. expect.arrayContaining([
  434. expect.objectContaining({ content: "First task" }),
  435. expect.objectContaining({ content: "Second task" }),
  436. ]),
  437. "code",
  438. )
  439. // Should complete successfully
  440. expect(mockPushToolResult).not.toHaveBeenCalled()
  441. })
  442. it("should work with empty todos string when VSCode setting is enabled", async () => {
  443. // Enable VSCode setting
  444. const mockGet = vi.fn().mockReturnValue(true)
  445. vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
  446. get: mockGet,
  447. } as any)
  448. const block: ToolUse<"new_task"> = {
  449. type: "tool_use",
  450. name: "new_task",
  451. params: {
  452. mode: "code",
  453. message: "Test message",
  454. todos: "", // Empty string should be accepted
  455. },
  456. partial: false,
  457. }
  458. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  459. askApproval: mockAskApproval,
  460. handleError: mockHandleError,
  461. pushToolResult: mockPushToolResult,
  462. })
  463. // Should NOT error when todos is empty string and setting is enabled
  464. expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
  465. expect(mockCline.consecutiveMistakeCount).toBe(0)
  466. // Should create task with empty todos array
  467. expect(mockStartSubtask).toHaveBeenCalledWith("Test message", [], "code")
  468. // Should complete successfully
  469. expect(mockPushToolResult).not.toHaveBeenCalled()
  470. })
  471. it("should check VSCode setting with Package.name configuration key", async () => {
  472. const mockGet = vi.fn().mockReturnValue(false)
  473. const mockGetConfiguration = vi.fn().mockReturnValue({
  474. get: mockGet,
  475. } as any)
  476. vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
  477. const block: ToolUse<"new_task"> = {
  478. type: "tool_use",
  479. name: "new_task",
  480. params: {
  481. mode: "code",
  482. message: "Test message",
  483. },
  484. partial: false,
  485. }
  486. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  487. askApproval: mockAskApproval,
  488. handleError: mockHandleError,
  489. pushToolResult: mockPushToolResult,
  490. })
  491. // Verify that VSCode configuration was accessed with Package.name
  492. expect(mockGetConfiguration).toHaveBeenCalledWith("roo-cline")
  493. expect(mockGet).toHaveBeenCalledWith("newTaskRequireTodos", false)
  494. })
  495. it("should use current Package.name value (roo-code-nightly) when accessing VSCode configuration", async () => {
  496. // Arrange: capture calls to VSCode configuration and ensure we can assert the namespace
  497. const mockGet = vi.fn().mockReturnValue(false)
  498. const mockGetConfiguration = vi.fn().mockReturnValue({
  499. get: mockGet,
  500. } as any)
  501. vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
  502. // Mutate the mocked Package.name dynamically to simulate a different build variant
  503. const pkg = await import("../../../shared/package")
  504. ;(pkg.Package as any).name = "roo-code-nightly"
  505. const block: ToolUse<"new_task"> = {
  506. type: "tool_use",
  507. name: "new_task",
  508. params: {
  509. mode: "code",
  510. message: "Test message",
  511. },
  512. partial: false,
  513. }
  514. await newTaskTool.handle(mockCline as any, withNativeArgs(block), {
  515. askApproval: mockAskApproval,
  516. handleError: mockHandleError,
  517. pushToolResult: mockPushToolResult,
  518. })
  519. // Assert: configuration was read using the dynamic nightly namespace
  520. expect(mockGetConfiguration).toHaveBeenCalledWith("roo-code-nightly")
  521. expect(mockGet).toHaveBeenCalledWith("newTaskRequireTodos", false)
  522. })
  523. })
  524. // Add more tests for error handling (invalid mode, approval denied) if needed
  525. })
  526. describe("newTaskTool delegation flow", () => {
  527. it("delegates to provider and does not call legacy startSubtask", async () => {
  528. // Arrange: stub provider delegation
  529. const providerSpy = {
  530. getState: vi.fn().mockResolvedValue({
  531. mode: "ask",
  532. experiments: {},
  533. }),
  534. delegateParentAndOpenChild: vi.fn().mockResolvedValue({ taskId: "child-1" }),
  535. handleModeSwitch: vi.fn(),
  536. } as any
  537. // Use a fresh local cline instance to avoid cross-test interference
  538. const localStartSubtask = vi.fn()
  539. const localEmit = vi.fn()
  540. const localCline = {
  541. ask: vi.fn(),
  542. sayAndCreateMissingParamError: mockSayAndCreateMissingParamError,
  543. emit: localEmit,
  544. recordToolError: mockRecordToolError,
  545. consecutiveMistakeCount: 0,
  546. isPaused: false,
  547. pausedModeSlug: "ask",
  548. taskId: "mock-parent-task-id",
  549. enableCheckpoints: false,
  550. checkpointSave: mockCheckpointSave,
  551. startSubtask: localStartSubtask,
  552. providerRef: {
  553. deref: vi.fn(() => providerSpy),
  554. },
  555. }
  556. const block: ToolUse<"new_task"> = {
  557. type: "tool_use",
  558. name: "new_task",
  559. params: {
  560. mode: "code",
  561. message: "Do something",
  562. // no todos -> should default to []
  563. },
  564. partial: false,
  565. }
  566. // Act
  567. await newTaskTool.handle(localCline as any, withNativeArgs(block), {
  568. askApproval: mockAskApproval,
  569. handleError: mockHandleError,
  570. pushToolResult: mockPushToolResult,
  571. })
  572. // Assert: provider method called with correct params
  573. expect(providerSpy.delegateParentAndOpenChild).toHaveBeenCalledWith({
  574. parentTaskId: "mock-parent-task-id",
  575. message: "Do something",
  576. initialTodos: [],
  577. mode: "code",
  578. })
  579. // Assert: legacy path not used
  580. expect(localStartSubtask).not.toHaveBeenCalled()
  581. // Assert: no pause/unpause events emitted in delegation path
  582. const pauseEvents = (localEmit as any).mock.calls.filter(
  583. (c: any[]) => c[0] === "taskPaused" || c[0] === "taskUnpaused",
  584. )
  585. expect(pauseEvents.length).toBe(0)
  586. // Assert: tool result reflects delegation
  587. expect(mockPushToolResult).not.toHaveBeenCalled()
  588. })
  589. })