webfetch.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { Effect, Layer } from "effect"
  4. import { FetchHttpClient } from "effect/unstable/http"
  5. import { Agent } from "../../src/agent/agent"
  6. import { Truncate } from "../../src/tool/truncate"
  7. import { Instance } from "../../src/project/instance"
  8. import { WebFetchTool } from "../../src/tool/webfetch"
  9. import { SessionID, MessageID } from "../../src/session/schema"
  10. const projectRoot = path.join(import.meta.dir, "../..")
  11. const ctx = {
  12. sessionID: SessionID.make("ses_test"),
  13. messageID: MessageID.make("message"),
  14. callID: "",
  15. agent: "build",
  16. abort: AbortSignal.any([]),
  17. messages: [],
  18. metadata: () => Effect.void,
  19. ask: () => Effect.void,
  20. }
  21. async function withFetch(fetch: (req: Request) => Response | Promise<Response>, fn: (url: URL) => Promise<void>) {
  22. using server = Bun.serve({ port: 0, fetch })
  23. await fn(server.url)
  24. }
  25. function exec(args: { url: string; format: "text" | "markdown" | "html" }) {
  26. return WebFetchTool.pipe(
  27. Effect.flatMap((info) => info.init()),
  28. Effect.flatMap((tool) => tool.execute(args, ctx)),
  29. Effect.provide(Layer.mergeAll(FetchHttpClient.layer, Truncate.defaultLayer, Agent.defaultLayer)),
  30. Effect.runPromise,
  31. )
  32. }
  33. describe("tool.webfetch", () => {
  34. test("returns image responses as file attachments", async () => {
  35. const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
  36. await withFetch(
  37. () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
  38. async (url) => {
  39. await Instance.provide({
  40. directory: projectRoot,
  41. fn: async () => {
  42. const result = await exec({ url: new URL("/image.png", url).toString(), format: "markdown" })
  43. expect(result.output).toBe("Image fetched successfully")
  44. expect(result.attachments).toBeDefined()
  45. expect(result.attachments?.length).toBe(1)
  46. expect(result.attachments?.[0].type).toBe("file")
  47. expect(result.attachments?.[0].mime).toBe("image/png")
  48. expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
  49. expect(result.attachments?.[0]).not.toHaveProperty("id")
  50. expect(result.attachments?.[0]).not.toHaveProperty("sessionID")
  51. expect(result.attachments?.[0]).not.toHaveProperty("messageID")
  52. },
  53. })
  54. },
  55. )
  56. })
  57. test("keeps svg as text output", async () => {
  58. const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
  59. await withFetch(
  60. () =>
  61. new Response(svg, {
  62. status: 200,
  63. headers: { "content-type": "image/svg+xml; charset=UTF-8" },
  64. }),
  65. async (url) => {
  66. await Instance.provide({
  67. directory: projectRoot,
  68. fn: async () => {
  69. const result = await exec({ url: new URL("/image.svg", url).toString(), format: "html" })
  70. expect(result.output).toContain("<svg")
  71. expect(result.attachments).toBeUndefined()
  72. },
  73. })
  74. },
  75. )
  76. })
  77. test("keeps text responses as text output", async () => {
  78. await withFetch(
  79. () =>
  80. new Response("hello from webfetch", {
  81. status: 200,
  82. headers: { "content-type": "text/plain; charset=utf-8" },
  83. }),
  84. async (url) => {
  85. await Instance.provide({
  86. directory: projectRoot,
  87. fn: async () => {
  88. const result = await exec({ url: new URL("/file.txt", url).toString(), format: "text" })
  89. expect(result.output).toBe("hello from webfetch")
  90. expect(result.attachments).toBeUndefined()
  91. },
  92. })
  93. },
  94. )
  95. })
  96. })