TelemetryClient.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. // npx vitest run src/__tests__/TelemetryClient.test.ts
  3. import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
  4. import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"
  5. const mockFetch = vi.fn()
  6. global.fetch = mockFetch as any
  7. describe("TelemetryClient", () => {
  8. const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
  9. return instance[propertyName]
  10. }
  11. let mockAuthService: any
  12. let mockSettingsService: any
  13. beforeEach(() => {
  14. vi.clearAllMocks()
  15. // Create a mock AuthService instead of using the singleton
  16. mockAuthService = {
  17. getSessionToken: vi.fn().mockReturnValue("mock-token"),
  18. getState: vi.fn().mockReturnValue("active-session"),
  19. isAuthenticated: vi.fn().mockReturnValue(true),
  20. hasActiveSession: vi.fn().mockReturnValue(true),
  21. }
  22. // Create a mock SettingsService
  23. mockSettingsService = {
  24. getSettings: vi.fn().mockReturnValue({
  25. cloudSettings: {
  26. recordTaskMessages: true,
  27. },
  28. }),
  29. }
  30. mockFetch.mockResolvedValue({
  31. ok: true,
  32. json: vi.fn().mockResolvedValue({}),
  33. })
  34. vi.spyOn(console, "info").mockImplementation(() => {})
  35. vi.spyOn(console, "error").mockImplementation(() => {})
  36. })
  37. afterEach(() => {
  38. vi.restoreAllMocks()
  39. })
  40. describe("isEventCapturable", () => {
  41. it("should return true for events not in exclude list", () => {
  42. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  43. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  44. client,
  45. "isEventCapturable",
  46. ).bind(client)
  47. expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
  48. expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
  49. expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
  50. expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
  51. })
  52. it("should return false for events in exclude list", () => {
  53. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  54. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  55. client,
  56. "isEventCapturable",
  57. ).bind(client)
  58. expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
  59. })
  60. it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => {
  61. mockSettingsService.getSettings.mockReturnValue({
  62. cloudSettings: {
  63. recordTaskMessages: true,
  64. },
  65. })
  66. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  67. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  68. client,
  69. "isEventCapturable",
  70. ).bind(client)
  71. expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
  72. })
  73. it("should return false for TASK_MESSAGE events when recordTaskMessages is false", () => {
  74. mockSettingsService.getSettings.mockReturnValue({
  75. cloudSettings: {
  76. recordTaskMessages: false,
  77. },
  78. })
  79. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  80. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  81. client,
  82. "isEventCapturable",
  83. ).bind(client)
  84. expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
  85. })
  86. it("should return false for TASK_MESSAGE events when recordTaskMessages is undefined", () => {
  87. mockSettingsService.getSettings.mockReturnValue({
  88. cloudSettings: {},
  89. })
  90. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  91. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  92. client,
  93. "isEventCapturable",
  94. ).bind(client)
  95. expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
  96. })
  97. it("should return false for TASK_MESSAGE events when cloudSettings is undefined", () => {
  98. mockSettingsService.getSettings.mockReturnValue({})
  99. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  100. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  101. client,
  102. "isEventCapturable",
  103. ).bind(client)
  104. expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
  105. })
  106. it("should return false for TASK_MESSAGE events when getSettings returns undefined", () => {
  107. mockSettingsService.getSettings.mockReturnValue(undefined)
  108. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  109. const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
  110. client,
  111. "isEventCapturable",
  112. ).bind(client)
  113. expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
  114. })
  115. })
  116. describe("getEventProperties", () => {
  117. it("should merge provider properties with event properties", async () => {
  118. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  119. const mockProvider: TelemetryPropertiesProvider = {
  120. getTelemetryProperties: vi.fn().mockResolvedValue({
  121. appVersion: "1.0.0",
  122. vscodeVersion: "1.60.0",
  123. platform: "darwin",
  124. editorName: "vscode",
  125. language: "en",
  126. mode: "code",
  127. }),
  128. }
  129. client.setProvider(mockProvider)
  130. const getEventProperties = getPrivateProperty<
  131. (event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
  132. >(client, "getEventProperties").bind(client)
  133. const result = await getEventProperties({
  134. event: TelemetryEventName.TASK_CREATED,
  135. properties: {
  136. customProp: "value",
  137. mode: "override", // This should override the provider's mode.
  138. },
  139. })
  140. expect(result).toEqual({
  141. appVersion: "1.0.0",
  142. vscodeVersion: "1.60.0",
  143. platform: "darwin",
  144. editorName: "vscode",
  145. language: "en",
  146. mode: "override", // Event property takes precedence.
  147. customProp: "value",
  148. })
  149. expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
  150. })
  151. it("should handle errors from provider gracefully", async () => {
  152. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  153. const mockProvider: TelemetryPropertiesProvider = {
  154. getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
  155. }
  156. const consoleErrorSpy = vi.spyOn(console, "error")
  157. client.setProvider(mockProvider)
  158. const getEventProperties = getPrivateProperty<
  159. (event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
  160. >(client, "getEventProperties").bind(client)
  161. const result = await getEventProperties({
  162. event: TelemetryEventName.TASK_CREATED,
  163. properties: { customProp: "value" },
  164. })
  165. expect(result).toEqual({ customProp: "value" })
  166. expect(consoleErrorSpy).toHaveBeenCalledWith(
  167. expect.stringContaining("Error getting telemetry properties: Provider error"),
  168. )
  169. })
  170. it("should return event properties when no provider is set", async () => {
  171. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  172. const getEventProperties = getPrivateProperty<
  173. (event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
  174. >(client, "getEventProperties").bind(client)
  175. const result = await getEventProperties({
  176. event: TelemetryEventName.TASK_CREATED,
  177. properties: { customProp: "value" },
  178. })
  179. expect(result).toEqual({ customProp: "value" })
  180. })
  181. })
  182. describe("capture", () => {
  183. it("should not capture events that are not capturable", async () => {
  184. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  185. await client.capture({
  186. event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
  187. properties: { test: "value" },
  188. })
  189. expect(mockFetch).not.toHaveBeenCalled()
  190. })
  191. it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => {
  192. mockSettingsService.getSettings.mockReturnValue({
  193. cloudSettings: {
  194. recordTaskMessages: false,
  195. },
  196. })
  197. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  198. await client.capture({
  199. event: TelemetryEventName.TASK_MESSAGE,
  200. properties: {
  201. taskId: "test-task-id",
  202. message: {
  203. ts: 1,
  204. type: "say",
  205. say: "text",
  206. text: "test message",
  207. },
  208. },
  209. })
  210. expect(mockFetch).not.toHaveBeenCalled()
  211. })
  212. it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => {
  213. mockSettingsService.getSettings.mockReturnValue({
  214. cloudSettings: {},
  215. })
  216. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  217. await client.capture({
  218. event: TelemetryEventName.TASK_MESSAGE,
  219. properties: {
  220. taskId: "test-task-id",
  221. message: {
  222. ts: 1,
  223. type: "say",
  224. say: "text",
  225. text: "test message",
  226. },
  227. },
  228. })
  229. expect(mockFetch).not.toHaveBeenCalled()
  230. })
  231. it("should not send request when schema validation fails", async () => {
  232. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  233. await client.capture({
  234. event: TelemetryEventName.TASK_CREATED,
  235. properties: { test: "value" },
  236. })
  237. expect(mockFetch).not.toHaveBeenCalled()
  238. expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
  239. })
  240. it("should send request when event is capturable and validation passes", async () => {
  241. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  242. const providerProperties = {
  243. appName: "roo-code",
  244. appVersion: "1.0.0",
  245. vscodeVersion: "1.60.0",
  246. platform: "darwin",
  247. editorName: "vscode",
  248. language: "en",
  249. mode: "code",
  250. }
  251. const eventProperties = {
  252. taskId: "test-task-id",
  253. }
  254. const mockValidatedData = {
  255. type: TelemetryEventName.TASK_CREATED,
  256. properties: {
  257. ...providerProperties,
  258. taskId: "test-task-id",
  259. },
  260. }
  261. const mockProvider: TelemetryPropertiesProvider = {
  262. getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
  263. }
  264. client.setProvider(mockProvider)
  265. await client.capture({
  266. event: TelemetryEventName.TASK_CREATED,
  267. properties: eventProperties,
  268. })
  269. expect(mockFetch).toHaveBeenCalledWith(
  270. "https://app.roocode.com/api/events",
  271. expect.objectContaining({
  272. method: "POST",
  273. body: JSON.stringify(mockValidatedData),
  274. }),
  275. )
  276. })
  277. it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => {
  278. mockSettingsService.getSettings.mockReturnValue({
  279. cloudSettings: {
  280. recordTaskMessages: true,
  281. },
  282. })
  283. const eventProperties = {
  284. appName: "roo-code",
  285. appVersion: "1.0.0",
  286. vscodeVersion: "1.60.0",
  287. platform: "darwin",
  288. editorName: "vscode",
  289. language: "en",
  290. mode: "code",
  291. taskId: "test-task-id",
  292. message: {
  293. ts: 1,
  294. type: "say",
  295. say: "text",
  296. text: "test message",
  297. },
  298. }
  299. const mockValidatedData = {
  300. type: TelemetryEventName.TASK_MESSAGE,
  301. properties: eventProperties,
  302. }
  303. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  304. await client.capture({
  305. event: TelemetryEventName.TASK_MESSAGE,
  306. properties: eventProperties,
  307. })
  308. expect(mockFetch).toHaveBeenCalledWith(
  309. "https://app.roocode.com/api/events",
  310. expect.objectContaining({
  311. method: "POST",
  312. body: JSON.stringify(mockValidatedData),
  313. }),
  314. )
  315. })
  316. it("should handle fetch errors gracefully", async () => {
  317. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  318. mockFetch.mockRejectedValue(new Error("Network error"))
  319. await expect(
  320. client.capture({
  321. event: TelemetryEventName.TASK_CREATED,
  322. properties: { test: "value" },
  323. }),
  324. ).resolves.not.toThrow()
  325. })
  326. })
  327. describe("telemetry state methods", () => {
  328. it("should always return true for isTelemetryEnabled", () => {
  329. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  330. expect(client.isTelemetryEnabled()).toBe(true)
  331. })
  332. it("should have empty implementations for updateTelemetryState and shutdown", async () => {
  333. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  334. client.updateTelemetryState(true)
  335. await client.shutdown()
  336. })
  337. })
  338. describe("backfillMessages", () => {
  339. it("should not send request when not authenticated", async () => {
  340. mockAuthService.isAuthenticated.mockReturnValue(false)
  341. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  342. const messages = [
  343. {
  344. ts: 1,
  345. type: "say" as const,
  346. say: "text" as const,
  347. text: "test message",
  348. },
  349. ]
  350. await client.backfillMessages(messages, "test-task-id")
  351. expect(mockFetch).not.toHaveBeenCalled()
  352. })
  353. it("should not send request when no session token available", async () => {
  354. mockAuthService.getSessionToken.mockReturnValue(null)
  355. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  356. const messages = [
  357. {
  358. ts: 1,
  359. type: "say" as const,
  360. say: "text" as const,
  361. text: "test message",
  362. },
  363. ]
  364. await client.backfillMessages(messages, "test-task-id")
  365. expect(mockFetch).not.toHaveBeenCalled()
  366. expect(console.error).toHaveBeenCalledWith(
  367. "[TelemetryClient#backfillMessages] Unauthorized: No session token available.",
  368. )
  369. })
  370. it("should send FormData request with correct structure when authenticated", async () => {
  371. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  372. const providerProperties = {
  373. appName: "roo-code",
  374. appVersion: "1.0.0",
  375. vscodeVersion: "1.60.0",
  376. platform: "darwin",
  377. editorName: "vscode",
  378. language: "en",
  379. mode: "code",
  380. }
  381. const mockProvider: TelemetryPropertiesProvider = {
  382. getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
  383. }
  384. client.setProvider(mockProvider)
  385. const messages = [
  386. {
  387. ts: 1,
  388. type: "say" as const,
  389. say: "text" as const,
  390. text: "test message 1",
  391. },
  392. {
  393. ts: 2,
  394. type: "ask" as const,
  395. ask: "followup" as const,
  396. text: "test question",
  397. },
  398. ]
  399. await client.backfillMessages(messages, "test-task-id")
  400. expect(mockFetch).toHaveBeenCalledWith(
  401. "https://app.roocode.com/api/events/backfill",
  402. expect.objectContaining({
  403. method: "POST",
  404. headers: {
  405. Authorization: "Bearer mock-token",
  406. },
  407. body: expect.any(FormData),
  408. }),
  409. )
  410. // Verify FormData contents
  411. const call = mockFetch.mock.calls[0]
  412. const formData = call?.[1]?.body as FormData
  413. expect(formData.get("taskId")).toBe("test-task-id")
  414. // Parse and compare properties as objects since JSON.stringify order can vary
  415. const propertiesJson = formData.get("properties") as string
  416. const parsedProperties = JSON.parse(propertiesJson)
  417. expect(parsedProperties).toEqual({
  418. taskId: "test-task-id",
  419. ...providerProperties,
  420. })
  421. // The messages are stored as a File object under the "file" key
  422. const fileField = formData.get("file") as File
  423. expect(fileField).toBeInstanceOf(File)
  424. expect(fileField.name).toBe("task.json")
  425. expect(fileField.type).toBe("application/json")
  426. // Read the file content to verify the messages
  427. const fileContent = await fileField.text()
  428. expect(fileContent).toBe(JSON.stringify(messages))
  429. })
  430. it("should handle provider errors gracefully", async () => {
  431. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  432. const mockProvider: TelemetryPropertiesProvider = {
  433. getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
  434. }
  435. client.setProvider(mockProvider)
  436. const messages = [
  437. {
  438. ts: 1,
  439. type: "say" as const,
  440. say: "text" as const,
  441. text: "test message",
  442. },
  443. ]
  444. await client.backfillMessages(messages, "test-task-id")
  445. expect(mockFetch).toHaveBeenCalledWith(
  446. "https://app.roocode.com/api/events/backfill",
  447. expect.objectContaining({
  448. method: "POST",
  449. headers: {
  450. Authorization: "Bearer mock-token",
  451. },
  452. body: expect.any(FormData),
  453. }),
  454. )
  455. // Verify FormData contents - should still work with just taskId
  456. const call = mockFetch.mock.calls[0]
  457. const formData = call?.[1]?.body as FormData
  458. expect(formData.get("taskId")).toBe("test-task-id")
  459. expect(formData.get("properties")).toBe(
  460. JSON.stringify({
  461. taskId: "test-task-id",
  462. }),
  463. )
  464. // The messages are stored as a File object under the "file" key
  465. const fileField = formData.get("file") as File
  466. expect(fileField).toBeInstanceOf(File)
  467. expect(fileField.name).toBe("task.json")
  468. expect(fileField.type).toBe("application/json")
  469. // Read the file content to verify the messages
  470. const fileContent = await fileField.text()
  471. expect(fileContent).toBe(JSON.stringify(messages))
  472. })
  473. it("should work without provider set", async () => {
  474. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  475. const messages = [
  476. {
  477. ts: 1,
  478. type: "say" as const,
  479. say: "text" as const,
  480. text: "test message",
  481. },
  482. ]
  483. await client.backfillMessages(messages, "test-task-id")
  484. expect(mockFetch).toHaveBeenCalledWith(
  485. "https://app.roocode.com/api/events/backfill",
  486. expect.objectContaining({
  487. method: "POST",
  488. headers: {
  489. Authorization: "Bearer mock-token",
  490. },
  491. body: expect.any(FormData),
  492. }),
  493. )
  494. // Verify FormData contents - should work with just taskId
  495. const call = mockFetch.mock.calls[0]
  496. const formData = call?.[1]?.body as FormData
  497. expect(formData.get("taskId")).toBe("test-task-id")
  498. expect(formData.get("properties")).toBe(
  499. JSON.stringify({
  500. taskId: "test-task-id",
  501. }),
  502. )
  503. // The messages are stored as a File object under the "file" key
  504. const fileField = formData.get("file") as File
  505. expect(fileField).toBeInstanceOf(File)
  506. expect(fileField.name).toBe("task.json")
  507. expect(fileField.type).toBe("application/json")
  508. // Read the file content to verify the messages
  509. const fileContent = await fileField.text()
  510. expect(fileContent).toBe(JSON.stringify(messages))
  511. })
  512. it("should handle fetch errors gracefully", async () => {
  513. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  514. mockFetch.mockRejectedValue(new Error("Network error"))
  515. const messages = [
  516. {
  517. ts: 1,
  518. type: "say" as const,
  519. say: "text" as const,
  520. text: "test message",
  521. },
  522. ]
  523. await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow()
  524. expect(console.error).toHaveBeenCalledWith(
  525. expect.stringContaining(
  526. "[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error",
  527. ),
  528. )
  529. })
  530. it("should handle HTTP error responses", async () => {
  531. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  532. mockFetch.mockResolvedValue({
  533. ok: false,
  534. status: 404,
  535. statusText: "Not Found",
  536. })
  537. const messages = [
  538. {
  539. ts: 1,
  540. type: "say" as const,
  541. say: "text" as const,
  542. text: "test message",
  543. },
  544. ]
  545. await client.backfillMessages(messages, "test-task-id")
  546. expect(console.error).toHaveBeenCalledWith(
  547. "[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found",
  548. )
  549. })
  550. it("should log debug information when debug is enabled", async () => {
  551. const client = new TelemetryClient(mockAuthService, mockSettingsService, true)
  552. const messages = [
  553. {
  554. ts: 1,
  555. type: "say" as const,
  556. say: "text" as const,
  557. text: "test message",
  558. },
  559. ]
  560. await client.backfillMessages(messages, "test-task-id")
  561. expect(console.info).toHaveBeenCalledWith(
  562. "[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
  563. )
  564. expect(console.info).toHaveBeenCalledWith(
  565. "[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
  566. )
  567. })
  568. it("should handle empty messages array", async () => {
  569. const client = new TelemetryClient(mockAuthService, mockSettingsService)
  570. await client.backfillMessages([], "test-task-id")
  571. expect(mockFetch).toHaveBeenCalledWith(
  572. "https://app.roocode.com/api/events/backfill",
  573. expect.objectContaining({
  574. method: "POST",
  575. headers: {
  576. Authorization: "Bearer mock-token",
  577. },
  578. body: expect.any(FormData),
  579. }),
  580. )
  581. // Verify FormData contents
  582. const call = mockFetch.mock.calls[0]
  583. const formData = call?.[1]?.body as FormData
  584. // The messages are stored as a File object under the "file" key
  585. const fileField = formData.get("file") as File
  586. expect(fileField).toBeInstanceOf(File)
  587. expect(fileField.name).toBe("task.json")
  588. expect(fileField.type).toBe("application/json")
  589. // Read the file content to verify the empty messages array
  590. const fileContent = await fileField.text()
  591. expect(fileContent).toBe("[]")
  592. })
  593. })
  594. })