207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
|
import { Form } from "@opencode-ai/core/form"
|
|
import { KV } from "@opencode-ai/core/kv"
|
|
import { WebSearch } from "@opencode-ai/core/websearch"
|
|
import { SessionV2 } from "@opencode-ai/core/session"
|
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|
import { WebSearchTool } from "@opencode-ai/core/tool/websearch"
|
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
import { Image } from "@opencode-ai/core/image"
|
|
import { testEffect } from "./lib/effect"
|
|
import { imagePassthrough } from "./lib/image"
|
|
import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool"
|
|
import { webSearchHost } from "./plugin/host"
|
|
|
|
const webSearchToolNode = makeLocationNode({
|
|
name: "test/websearch-tool-plugin",
|
|
layer: Layer.effectDiscard(
|
|
Effect.gen(function* () {
|
|
const websearch = yield* WebSearch.Service
|
|
yield* registerToolPlugin(WebSearchTool.Plugin, { websearch: webSearchHost(websearch) })
|
|
}),
|
|
),
|
|
deps: [ToolRegistry.toolsNode, PermissionV2.node, WebSearch.node, Form.node, KV.node],
|
|
})
|
|
|
|
const sessionID = SessionV2.ID.make("ses_websearch_test")
|
|
const assertions: PermissionV2.AssertInput[] = []
|
|
const queries: WebSearch.Input[] = []
|
|
let result = new WebSearch.Response({
|
|
providerID: WebSearch.ID.make("exa"),
|
|
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
|
})
|
|
|
|
beforeEach(() => {
|
|
assertions.length = 0
|
|
queries.length = 0
|
|
result = new WebSearch.Response({
|
|
providerID: WebSearch.ID.make("exa"),
|
|
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
|
})
|
|
})
|
|
|
|
const permission = Layer.succeed(
|
|
PermissionV2.Service,
|
|
PermissionV2.Service.of({
|
|
assert: (input) => Effect.sync(() => assertions.push(input)),
|
|
ask: () => Effect.die("unused"),
|
|
reply: () => Effect.die("unused"),
|
|
get: () => Effect.die("unused"),
|
|
forSession: () => Effect.die("unused"),
|
|
list: () => Effect.die("unused"),
|
|
}),
|
|
)
|
|
const websearch = Layer.succeed(
|
|
WebSearch.Service,
|
|
WebSearch.Service.of({
|
|
transform: () => Effect.die("unused"),
|
|
reload: () => Effect.die("unused"),
|
|
providers: () => Effect.succeed([]),
|
|
default: () => Effect.succeed(undefined),
|
|
query: (input) =>
|
|
Effect.sync(() => {
|
|
queries.push(input)
|
|
return result
|
|
}),
|
|
}),
|
|
)
|
|
const form = Layer.succeed(
|
|
Form.Service,
|
|
Form.Service.of({
|
|
create: () => Effect.die("unused"),
|
|
ask: () => Effect.die("unused"),
|
|
get: () => Effect.die("unused"),
|
|
list: () => Effect.die("unused"),
|
|
state: () => Effect.die("unused"),
|
|
reply: () => Effect.die("unused"),
|
|
cancel: () => Effect.die("unused"),
|
|
}),
|
|
)
|
|
const kv = Layer.succeed(
|
|
KV.Service,
|
|
KV.Service.of({
|
|
get: () => Effect.succeed(undefined),
|
|
set: () => Effect.void,
|
|
remove: () => Effect.void,
|
|
}),
|
|
)
|
|
const it = testEffect(
|
|
AppNodeBuilder.build(
|
|
LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearch.node, webSearchToolNode]),
|
|
[
|
|
[PermissionV2.node, permission],
|
|
[WebSearch.node, websearch],
|
|
[Form.node, form],
|
|
[KV.node, kv],
|
|
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
|
[Image.node, imagePassthrough],
|
|
],
|
|
),
|
|
)
|
|
|
|
describe("WebSearchTool registration", () => {
|
|
it.effect("asserts permission before delegating to WebSearch", () =>
|
|
Effect.gen(function* () {
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"])
|
|
expect(
|
|
yield* executeTool(registry, {
|
|
sessionID,
|
|
...toolIdentity,
|
|
call: {
|
|
type: "tool-call",
|
|
id: "call-search",
|
|
name: "websearch",
|
|
input: { query: "effect typescript" },
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
status: "completed",
|
|
content: [{ type: "text", text: "## [Search results](https://example.com)\n\nsearch results" }],
|
|
})
|
|
expect(assertions).toMatchObject([
|
|
{
|
|
sessionID,
|
|
action: "websearch",
|
|
resources: ["effect typescript"],
|
|
save: ["*"],
|
|
metadata: { query: "effect typescript" },
|
|
},
|
|
])
|
|
expect(queries).toEqual([
|
|
{
|
|
query: "effect typescript",
|
|
},
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("keeps normalized results in structured output", () =>
|
|
Effect.gen(function* () {
|
|
result = new WebSearch.Response({
|
|
providerID: WebSearch.ID.make("parallel"),
|
|
results: [
|
|
{
|
|
url: "https://effect.website",
|
|
title: "Effect",
|
|
content: "parallel results",
|
|
time: { published: Date.parse("2026-07-25T00:00:00.000Z") },
|
|
},
|
|
],
|
|
})
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
expect(
|
|
yield* executeTool(registry, {
|
|
sessionID,
|
|
...toolIdentity,
|
|
call: { type: "tool-call", id: "call-parallel", name: "websearch", input: { query: "effect layers" } },
|
|
}),
|
|
).toEqual({
|
|
status: "completed",
|
|
output: {
|
|
provider: "parallel",
|
|
results: [
|
|
{
|
|
url: "https://effect.website",
|
|
title: "Effect",
|
|
content: "parallel results",
|
|
time: { published: Date.parse("2026-07-25T00:00:00.000Z") },
|
|
},
|
|
],
|
|
},
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "## [Effect](https://effect.website)\nPublished: 2026-07-25T00:00:00.000Z\n\nparallel results",
|
|
},
|
|
],
|
|
metadata: { provider: "parallel" },
|
|
})
|
|
}),
|
|
)
|
|
|
|
it.effect("uses the concise no-results fallback", () =>
|
|
Effect.gen(function* () {
|
|
result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), results: [] })
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
expect(
|
|
yield* executeTool(registry, {
|
|
sessionID,
|
|
...toolIdentity,
|
|
call: { type: "tool-call", id: "call-empty", name: "websearch", input: { query: "nothing" } },
|
|
}),
|
|
).toMatchObject({
|
|
status: "completed",
|
|
content: [{ type: "text", text: WebSearchTool.NO_RESULTS }],
|
|
})
|
|
}),
|
|
)
|
|
})
|