websearch.test.ts 4.6 KB

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