websearch.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Exit, Scope } from "effect"
  3. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  4. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  5. import { Bus } from "@opencode-ai/core/bus"
  6. import { WebSearch } from "@opencode-ai/core/websearch"
  7. import { testEffect } from "./lib/effect"
  8. const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, Bus.node])))
  9. const register = (id: string) =>
  10. Effect.gen(function* () {
  11. const websearch = yield* WebSearch.Service
  12. const providerID = WebSearch.ID.make(id)
  13. const calls: WebSearch.ProviderInput[] = []
  14. yield* websearch.transform((draft) => {
  15. draft.add({
  16. id: providerID,
  17. name: id.toUpperCase(),
  18. execute: (input) =>
  19. Effect.sync(() => {
  20. calls.push(input)
  21. return [
  22. {
  23. url: `https://${id}.example.com`,
  24. title: input.query,
  25. content: `${id}: ${input.query}`,
  26. time: {},
  27. },
  28. ]
  29. }),
  30. })
  31. })
  32. return { providerID, calls }
  33. })
  34. describe("WebSearch", () => {
  35. it.effect("executes an explicit provider without changing the default", () =>
  36. Effect.gen(function* () {
  37. yield* register("exa")
  38. const parallel = yield* register("parallel")
  39. const websearch = yield* WebSearch.Service
  40. expect(yield* websearch.query({ query: "effect", providerID: parallel.providerID })).toEqual(
  41. new WebSearch.Response({
  42. providerID: parallel.providerID,
  43. results: [
  44. {
  45. url: "https://parallel.example.com",
  46. title: "effect",
  47. content: "parallel: effect",
  48. time: {},
  49. },
  50. ],
  51. }),
  52. )
  53. expect((yield* websearch.query({ query: "default" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired")
  54. expect(parallel.calls).toEqual([{ query: "effect" }])
  55. }),
  56. )
  57. it.effect("requires a provider when no default is set", () =>
  58. Effect.gen(function* () {
  59. yield* register("exa")
  60. yield* register("parallel")
  61. const websearch = yield* WebSearch.Service
  62. expect((yield* websearch.query({ query: "layers" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired")
  63. }),
  64. )
  65. it.effect("uses the default set by a transform", () =>
  66. Effect.gen(function* () {
  67. yield* register("exa")
  68. const parallel = yield* register("parallel")
  69. const websearch = yield* WebSearch.Service
  70. yield* websearch.transform((draft) => draft.default.set(parallel.providerID))
  71. expect((yield* websearch.query({ query: "configured" })).providerID).toBe(parallel.providerID)
  72. }),
  73. )
  74. it.effect("chooses a registered provider for random selection", () =>
  75. Effect.gen(function* () {
  76. yield* register("exa")
  77. yield* register("parallel")
  78. const websearch = yield* WebSearch.Service
  79. yield* websearch.transform((draft) => draft.default.set("random"))
  80. expect(["exa", "parallel"]).toContain((yield* websearch.query({ query: "random" })).providerID)
  81. }),
  82. )
  83. it.effect("fails when web search is explicitly disabled", () =>
  84. Effect.gen(function* () {
  85. yield* register("exa")
  86. const websearch = yield* WebSearch.Service
  87. yield* websearch.transform((draft) => draft.default.set(false))
  88. expect((yield* websearch.query({ query: "disabled" }).pipe(Effect.flip))._tag).toBe("WebSearch.Disabled")
  89. }),
  90. )
  91. it.effect("falls back when the configured default is unavailable", () =>
  92. Effect.gen(function* () {
  93. yield* register("exa")
  94. const websearch = yield* WebSearch.Service
  95. yield* websearch.transform((draft) => draft.default.set(WebSearch.ID.make("missing")))
  96. expect((yield* websearch.query({ query: "fallback" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired")
  97. }),
  98. )
  99. it.effect("removes scoped provider registrations", () =>
  100. Effect.gen(function* () {
  101. const websearch = yield* WebSearch.Service
  102. const scope = yield* Scope.fork(yield* Scope.Scope)
  103. const provider = yield* register("temporary").pipe(Scope.provide(scope))
  104. expect(yield* websearch.providers()).toContainEqual({ id: provider.providerID, name: "TEMPORARY" })
  105. yield* Scope.close(scope, Exit.void)
  106. expect(yield* websearch.providers()).not.toContainEqual({ id: provider.providerID, name: "TEMPORARY" })
  107. }),
  108. )
  109. })