websearch.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { beforeEach, describe, expect } from "bun:test"
  2. import { Effect } from "effect"
  3. import { Integration } from "@opencode-ai/core/integration"
  4. import { WebSearch } from "@opencode-ai/core/websearch"
  5. import { WebSearchExa } from "@opencode-ai/core/plugin/websearch/exa"
  6. import { WebSearchParallel } from "@opencode-ai/core/plugin/websearch/parallel"
  7. import { host, integrationHost, webSearchHost } from "./host"
  8. import { requests, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture"
  9. beforeEach(() => {
  10. resetWebSearchFixture(
  11. `event: message\ndata: ${JSON.stringify({
  12. jsonrpc: "2.0",
  13. id: 1,
  14. result: {
  15. content: [
  16. {
  17. type: "text",
  18. text: "Title: Effect\nURL: https://effect.website\nPublished: 2026-07-25T00:00:00.000Z\nAuthor: N/A\nHighlights:\nEffect documentation",
  19. _meta: { searchTime: 123 },
  20. },
  21. ],
  22. },
  23. })}\n\n`,
  24. )
  25. })
  26. const it = webSearchIntegrationTest
  27. describe("built-in web search providers", () => {
  28. it.effect("registers a provider without an integration", () =>
  29. Effect.gen(function* () {
  30. const integrations = yield* Integration.Service
  31. const websearch = yield* WebSearch.Service
  32. const registration = yield* webSearchHost(websearch).transform((draft) => {
  33. draft.add({
  34. id: "test-websearch",
  35. name: "Test Web Search",
  36. execute: (input) => Effect.succeed([{ url: "https://example.com", content: input.query, time: {} }]),
  37. })
  38. })
  39. expect(yield* integrations.get(Integration.ID.make("test-websearch"))).toBeUndefined()
  40. expect(yield* websearch.providers()).toContainEqual({
  41. id: WebSearch.ID.make("test-websearch"),
  42. name: "Test Web Search",
  43. })
  44. yield* registration.dispose
  45. expect(yield* websearch.providers()).not.toContainEqual({
  46. id: WebSearch.ID.make("test-websearch"),
  47. name: "Test Web Search",
  48. })
  49. }),
  50. )
  51. it.effect("registers Exa with its MCP schema", () =>
  52. Effect.gen(function* () {
  53. const integrations = yield* Integration.Service
  54. const websearch = yield* WebSearch.Service
  55. yield* WebSearchExa.Plugin.effect(
  56. host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }),
  57. )
  58. const info = yield* integrations.get(Integration.ID.make("exa"))
  59. expect(info).toMatchObject({
  60. id: "exa",
  61. name: "Exa",
  62. methods: [{ type: "key" }, { type: "env", names: ["EXA_API_KEY"] }],
  63. })
  64. yield* integrations.connection.key({ integrationID: Integration.ID.make("exa"), key: "exa secret" })
  65. expect(yield* websearch.query({ query: "effect typescript", providerID: WebSearch.ID.make("exa") })).toEqual(
  66. new WebSearch.Response({
  67. providerID: WebSearch.ID.make("exa"),
  68. results: [
  69. {
  70. url: "https://effect.website",
  71. title: "Effect",
  72. content: "Effect documentation",
  73. time: { published: Date.parse("2026-07-25T00:00:00.000Z") },
  74. },
  75. ],
  76. }),
  77. )
  78. expect(requests).toEqual([
  79. {
  80. url: `${WebSearchExa.endpoint}?exaApiKey=exa+secret`,
  81. headers: expect.any(Object),
  82. body: {
  83. jsonrpc: "2.0",
  84. id: 1,
  85. method: "tools/call",
  86. params: {
  87. name: "web_search_exa",
  88. arguments: { query: "effect typescript", numResults: 8 },
  89. },
  90. },
  91. },
  92. ])
  93. }),
  94. )
  95. it.effect("registers Parallel and keeps its credential in the authorization header", () =>
  96. Effect.gen(function* () {
  97. resetWebSearchFixture(
  98. JSON.stringify({
  99. jsonrpc: "2.0",
  100. id: 1,
  101. result: {
  102. content: [{ type: "text", text: "search results" }],
  103. structuredContent: {
  104. search_id: "search_1",
  105. results: [
  106. {
  107. url: "https://effect.website",
  108. title: "Effect",
  109. publish_date: null,
  110. excerpts: ["Effect documentation"],
  111. },
  112. ],
  113. warnings: null,
  114. usage: [{ name: "sku_search", count: 1 }],
  115. session_id: "ses_parallel",
  116. },
  117. },
  118. }),
  119. )
  120. const integrations = yield* Integration.Service
  121. const websearch = yield* WebSearch.Service
  122. yield* WebSearchParallel.Plugin.effect(
  123. host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }),
  124. )
  125. yield* integrations.connection.key({ integrationID: Integration.ID.make("parallel"), key: "parallel-secret" })
  126. const output = yield* websearch.query({
  127. query: "effect layers",
  128. providerID: WebSearch.ID.make("parallel"),
  129. })
  130. expect(output).toEqual(
  131. new WebSearch.Response({
  132. providerID: WebSearch.ID.make("parallel"),
  133. results: [
  134. {
  135. url: "https://effect.website",
  136. title: "Effect",
  137. content: "Effect documentation",
  138. time: {},
  139. },
  140. ],
  141. }),
  142. )
  143. expect(requests[0]).toMatchObject({
  144. url: WebSearchParallel.endpoint,
  145. headers: { authorization: "Bearer parallel-secret" },
  146. body: {
  147. jsonrpc: "2.0",
  148. id: 1,
  149. method: "tools/call",
  150. params: {
  151. name: "web_search",
  152. arguments: {
  153. objective: "effect layers",
  154. search_queries: ["effect layers"],
  155. },
  156. },
  157. },
  158. })
  159. expect(JSON.stringify(output)).not.toContain("parallel-secret")
  160. }),
  161. )
  162. })