Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 55 additions & 31 deletions packages/sdk/test/embedded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,37 +202,61 @@ it.live(
10_000,
)

it.live("embedded client exposes plugin-backed web search", () =>
withEmbedded("opencode-embedded-websearch-", (fixture) =>
Effect.gen(function* () {
const opencode = yield* fixture.sdk.OpenCode.create()
const providerID = fixture.sdk.WebSearch.ID.make("embedded-websearch")
yield* opencode.plugin({
id: `embedded-websearch-${crypto.randomUUID()}`,
effect: (ctx) =>
ctx.websearch.transform((editor) => {
editor.add({
id: providerID,
name: "Embedded web search",
execute: (input) =>
Effect.succeed([{ url: "https://example.com", content: `Found ${input.query}`, time: {} }]),
})
}),
})

const result = yield* opencode.websearch.query({
query: "opencode",
providerID,
location: location(fixture),
})

expect(result.data).toEqual({
providerID,
results: [{ url: "https://example.com", content: "Found opencode", time: {} }],
})
}),
),
)
for (const phase of ["startup", "reload"] as const) {
it.live(
`embedded web search waits for plugin activation during ${phase}`,
() =>
withEmbedded("opencode-embedded-websearch-", (fixture) =>
Effect.gen(function* () {
const opencode = yield* fixture.sdk.OpenCode.create({
config: { directory: fixture.directory, project: false, content: "{}" },
models: { fetch: false },
fs: { filewatcher: false },
})
if (phase === "reload") yield* opencode.plugin.awaitActivation({ location: location(fixture) })
const started = yield* Latch.make(false)
const release = yield* Latch.make(false)
yield* Effect.addFinalizer(() => release.open)
const providerID = fixture.sdk.WebSearch.ID.make("embedded-websearch")
yield* opencode.plugin({
id: `embedded-websearch-${crypto.randomUUID()}`,
effect: (ctx) =>
Effect.gen(function* () {
yield* started.open
yield* release.await
yield* ctx.websearch.transform((editor) => {
editor.add({
id: providerID,
name: "Embedded web search",
execute: (input) =>
Effect.succeed([{ url: "https://example.com", content: `Found ${input.query}`, time: {} }]),
})
})
}),
})

const query = yield* opencode.websearch
.query({ query: "opencode", providerID, location: location(fixture) })
.pipe(Effect.forkScoped({ startImmediately: true }))
const providers = yield* opencode.websearch
.providers({ location: location(fixture) })
.pipe(Effect.forkScoped({ startImmediately: true }))

yield* started.await
expect(query.pollUnsafe()).toBeUndefined()
expect(providers.pollUnsafe()).toBeUndefined()
yield* release.open

expect((yield* Fiber.join(query)).data).toEqual({
providerID,
results: [{ url: "https://example.com", content: "Found opencode", time: {} }],
})
expect((yield* Fiber.join(providers)).data).toContainEqual({ id: providerID, name: "Embedded web search" })
}),
),
10_000,
)
}

it.live(
"Location-owned runner events reach the ready global client",
Expand Down
5 changes: 2 additions & 3 deletions packages/sdk/test/promise.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test("Promise host uses the embedded router", async () => {
}
})

test("Promise event streams support cancellation", async () => {
test("aborting a Promise event subscription completes it", async () => {
await using directory = await tmpdir("opencode-promise-stream-")
const config = join(directory.path, "config")
await mkdir(config)
Expand All @@ -44,8 +44,7 @@ test("Promise event streams support cancellation", async () => {
expect(await events.next()).toMatchObject({ value: { type: "server.connected" }, done: false })
const pending = events.next()
controller.abort()
const error = await pending.catch((error: unknown) => error)
expect(error).toMatchObject({ name: "ClientError", reason: "Transport" })
expect(await pending).toEqual({ done: true, value: undefined })
Comment on lines 45 to +47
await events.return?.()
}
})
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/handlers/websearch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Plugin } from "@opencode-ai/core/plugin"
import { WebSearch } from "@opencode-ai/core/websearch"
import { InvalidRequestError, ServiceUnavailableError } from "@opencode-ai/protocol/errors"
import { Effect } from "effect"
Expand All @@ -11,13 +12,15 @@ export const WebSearchHandler = HttpApiBuilder.group(Api, "server.websearch", (h
.handle(
"websearch.providers",
Effect.fn("server.websearch.providers")(function* () {
yield* Plugin.awaitActivation
const websearch = yield* WebSearch.Service
return yield* response(websearch.providers())
}),
)
.handle(
"websearch.query",
Effect.fn("server.websearch.query")(function* (request) {
yield* Plugin.awaitActivation
const websearch = yield* WebSearch.Service
return yield* response(
websearch.query(request.payload).pipe(
Expand Down
Loading