refactor(config): share entry observer (#43973)
This commit is contained in:
@@ -30,9 +30,8 @@ import { ConfigNormalize } from "./config/normalize.js"
|
||||
import { WellKnown } from "./wellknown.js"
|
||||
|
||||
export function latest<K extends keyof Info>(entries: readonly Entry[], key: K): Info[K] | undefined {
|
||||
return entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.findLast((entry) => entry.info[key] !== undefined)?.info[key]
|
||||
return entries.findLast((entry): entry is Document => entry.type === "document" && entry.info[key] !== undefined)
|
||||
?.info[key]
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
export * as ConfigCompactionPlugin from "./compaction.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { SessionCompaction } from "../../session/compaction.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.compaction",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(compaction.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, compaction.reload())
|
||||
yield* compaction.transform((draft) => {
|
||||
for (const entry of loaded.entries) {
|
||||
if (entry.type !== "document" || !entry.info.compaction) continue
|
||||
draft.configure({
|
||||
...(entry.info.compaction.auto === undefined ? {} : { auto: entry.info.compaction.auto }),
|
||||
...(entry.info.compaction.buffer === undefined ? {} : { buffer: entry.info.compaction.buffer }),
|
||||
...(entry.info.compaction.keep?.tokens === undefined
|
||||
? {}
|
||||
: { tokens: entry.info.compaction.keep.tokens }),
|
||||
...(entry.info.compaction.keep?.tokens === undefined ? {} : { tokens: entry.info.compaction.keep.tokens }),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export * as ConfigEntryObserver from "./entry-observer.js"
|
||||
|
||||
import type { EventDomain } from "@opencode-ai/plugin/effect/event"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
|
||||
export const observe = Effect.fnUntraced(function* (
|
||||
config: Config.Interface,
|
||||
event: EventDomain,
|
||||
reload: Effect.Effect<void>,
|
||||
) {
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const refresh = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(reload),
|
||||
)
|
||||
yield* event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => refresh),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
// Close the race between the first read and establishing the subscription.
|
||||
loaded.entries = yield* config.entries()
|
||||
return loaded
|
||||
})
|
||||
@@ -5,11 +5,12 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Formatter } from "../../formatter.js"
|
||||
import { make, type Info } from "../../formatter/builtins.js"
|
||||
import { Location } from "../../location.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.formatter",
|
||||
@@ -21,20 +22,7 @@ export const Plugin = define({
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const processes = yield* AppProcess.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(formatter.reload()),
|
||||
)
|
||||
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
// Refetch after subscribing so a config update between the first read and
|
||||
// the live subscription cannot leave the transform on a stale snapshot.
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, formatter.reload())
|
||||
|
||||
yield* formatter.transform((draft) => {
|
||||
const configured = Config.latest(loaded.entries, "formatter")
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
export * as ConfigImagePlugin from "./image.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Image } from "../../image.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.image",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const image = yield* Image.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(image.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
// Refetch after subscribing so a config update between the first read and
|
||||
// the live subscription cannot leave the transform on a stale snapshot.
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, image.reload())
|
||||
yield* image.transform((draft) => {
|
||||
for (const entry of loaded.entries) {
|
||||
if (entry.type !== "document") continue
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
export * as ConfigLocationWatcherPlugin from "./location-watcher.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { LocationWatcherPolicy } from "../../filesystem/location-watcher-policy.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.location-watcher",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const policy = yield* LocationWatcherPolicy.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(policy.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, policy.reload())
|
||||
yield* policy.transform((draft) => {
|
||||
for (const entry of loaded.entries) {
|
||||
if (entry.type !== "document" || !entry.info.watcher?.ignore) continue
|
||||
|
||||
@@ -2,15 +2,16 @@ export * as ConfigPolicyPlugin from "./policy.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Wildcard } from "../../util/wildcard.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.policy",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.catalog.reload())
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
const policies = loaded.entries
|
||||
@@ -22,15 +23,5 @@ export const Plugin = define({
|
||||
if (policy?.effect === "deny") catalog.provider.remove(record.provider.id)
|
||||
}
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() =>
|
||||
config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(ctx.catalog.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -3,15 +3,20 @@ export * as ConfigProviderPlugin from "./provider.js"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.provider",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const loaded = yield* ConfigEntryObserver.observe(
|
||||
config,
|
||||
ctx.event,
|
||||
ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload())),
|
||||
)
|
||||
yield* ctx.integration.transform((integrations) => {
|
||||
for (const [id, provider] of configuredProviders(loaded.entries)) {
|
||||
const integrationID = id
|
||||
@@ -97,17 +102,6 @@ export const Plugin = define({
|
||||
}
|
||||
}
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() =>
|
||||
config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(ctx.integration.reload()),
|
||||
Effect.andThen(ctx.catalog.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { ConfigReference } from "@opencode-ai/schema/config/reference"
|
||||
import path from "path"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Reference } from "../../reference.js"
|
||||
import { AbsolutePath } from "../../schema.js"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "../../location.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.reference",
|
||||
@@ -17,7 +18,7 @@ export const Plugin = define({
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const global = yield* Global.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.reference.reload())
|
||||
yield* ctx.reference.transform((draft) => {
|
||||
const entries = new Map<string, Reference.Source>()
|
||||
for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
|
||||
@@ -49,16 +50,6 @@ export const Plugin = define({
|
||||
}
|
||||
for (const [name, source] of entries) draft.add(name, source)
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() =>
|
||||
config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(ctx.reference.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
export * as ConfigShellPlugin from "./shell.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ShellSelect } from "../../shell/select.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.shell",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const shell = yield* ShellSelect.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(shell.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, shell.reload())
|
||||
yield* shell.transform((draft) => {
|
||||
const configured = Config.latest(loaded.entries, "shell")
|
||||
if (configured) draft.configure(configured)
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
export * as ConfigSnapshotPlugin from "./snapshot.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Snapshot } from "../../snapshot.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.snapshot",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(snapshot.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, snapshot.reload())
|
||||
yield* snapshot.transform((draft) => {
|
||||
const configured = Config.latest(loaded.entries, "snapshots")
|
||||
if (configured === undefined) return
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
export * as ConfigToolOutputPlugin from "./tool-output.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ToolOutput } from "../../tool-output.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.tool-output",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const output = yield* ToolOutput.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const reload = config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(output.reload()),
|
||||
)
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() => reload),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
loaded.entries = yield* config.entries()
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, output.reload())
|
||||
yield* output.transform((draft) => {
|
||||
const configured = Config.latest(loaded.entries, "tool_output")
|
||||
if (!configured) return
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
export * as ConfigWebSearchPlugin from "./websearch.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { ConfigEntryObserver } from "./entry-observer.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.websearch",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.websearch.reload())
|
||||
yield* ctx.websearch.transform((websearch) => {
|
||||
const selection = Config.latest(loaded.entries, "websearch")
|
||||
if (selection === false) websearch.default.set(false)
|
||||
if (selection) websearch.default.set(selection.provider)
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() =>
|
||||
config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(ctx.websearch.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Document, Info, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Deferred, Effect, PubSub, Ref, Stream } from "effect"
|
||||
import { ConfigEntryObserver } from "../../src/config/plugin/entry-observer"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
describe("ConfigEntryObserver", () => {
|
||||
it.effect("closes the startup race and reloads later updates", () =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* Ref.make([document("first")])
|
||||
const updates = yield* PubSub.unbounded<ReturnType<typeof updated>>()
|
||||
const reloaded = yield* Deferred.make<void>()
|
||||
const config = Config.Service.of({
|
||||
entries: () => Ref.get(current),
|
||||
update: () => Effect.die("unused config.update"),
|
||||
changes: () => Stream.empty,
|
||||
})
|
||||
const event = {
|
||||
subscribe: () =>
|
||||
Stream.unwrap(Ref.set(current, [document("raced")]).pipe(Effect.as(Stream.fromPubSub(updates)))),
|
||||
}
|
||||
|
||||
const loaded = yield* ConfigEntryObserver.observe(
|
||||
config,
|
||||
event,
|
||||
Deferred.succeed(reloaded, undefined).pipe(Effect.asVoid),
|
||||
)
|
||||
|
||||
expect(Config.latest(loaded.entries, "shell")).toBe("raced")
|
||||
expect(yield* Deferred.isDone(reloaded)).toBe(false)
|
||||
|
||||
yield* Ref.set(current, [document("later")])
|
||||
yield* PubSub.publish(updates, updated())
|
||||
yield* Deferred.await(reloaded)
|
||||
|
||||
expect(Config.latest(loaded.entries, "shell")).toBe("later")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
function document(shell: string): Entry {
|
||||
return new Document({ type: "document", info: new Info({ shell }) })
|
||||
}
|
||||
|
||||
function updated() {
|
||||
return { id: Event.ID.create(), created: Date.now(), type: "config.updated" as const, data: {} }
|
||||
}
|
||||
@@ -11,12 +11,13 @@ import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
|
||||
import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference"
|
||||
import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -32,6 +33,7 @@ describe("config plugin reloads", () => {
|
||||
const agents = yield* Agent.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* Command.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const bus = yield* Bus.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const references = yield* Reference.Service
|
||||
@@ -47,6 +49,7 @@ describe("config plugin reloads", () => {
|
||||
|
||||
expect((yield* agents.get(Agent.ID.make("first")))?.description).toBe("First agent")
|
||||
expect((yield* commands.get("first"))?.description).toBe("First command")
|
||||
expect(yield* integrations.get(Integration.ID.make("first"))).toBeDefined()
|
||||
expect((yield* skills.list()).some((skill) => skill.id === "first")).toBe(true)
|
||||
expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
|
||||
expect(yield* catalog.provider.get(Provider.ID.make("first"))).toBeDefined()
|
||||
@@ -61,6 +64,8 @@ describe("config plugin reloads", () => {
|
||||
(yield* agents.get(Agent.ID.make("second")))?.description === "Second agent" &&
|
||||
(yield* commands.get("first")) === undefined &&
|
||||
(yield* commands.get("second"))?.description === "Second command" &&
|
||||
(yield* integrations.get(Integration.ID.make("first"))) === undefined &&
|
||||
(yield* integrations.get(Integration.ID.make("second"))) !== undefined &&
|
||||
(yield* references.list()).some((reference) => reference.name === "second") &&
|
||||
(yield* catalog.provider.get(Provider.ID.make("first"))) === undefined &&
|
||||
(yield* catalog.provider.get(Provider.ID.make("second"))) !== undefined
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigWebSearchPlugin } from "@opencode-ai/core/config/plugin/websearch"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { WebSearch } from "@opencode-ai/core/websearch"
|
||||
import { Document, Event, Info } from "@opencode-ai/schema/config"
|
||||
import { ConfigWebSearch } from "@opencode-ai/schema/config/websearch"
|
||||
import { Effect } from "effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "../plugin/fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
describe("ConfigWebSearchPlugin.Plugin", () => {
|
||||
it.live("reloads changed default selection", () =>
|
||||
Effect.gen(function* () {
|
||||
const websearch = yield* WebSearch.Service
|
||||
const bus = yield* Bus.Service
|
||||
const config = yield* Config.Test
|
||||
const plugins = yield* Plugin.Service
|
||||
yield* websearch.transform((draft) =>
|
||||
draft.add({ id: WebSearch.ID.make("test"), name: "Test", execute: () => Effect.succeed([]) }),
|
||||
)
|
||||
yield* ConfigWebSearchPlugin.Plugin.effect(yield* PluginHost.make(plugins))
|
||||
|
||||
expect((yield* websearch.default().pipe(Effect.flip))._tag).toBe("WebSearch.Disabled")
|
||||
|
||||
yield* config.setEntries([configured(new ConfigWebSearch.Info({ provider: "random" }))])
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
yield* waitUntil(
|
||||
websearch.default().pipe(
|
||||
Effect.map((provider) => provider?.id === WebSearch.ID.make("test")),
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
),
|
||||
)
|
||||
}).pipe(Effect.provide(Config.testLayer([configured(false)]))),
|
||||
)
|
||||
})
|
||||
|
||||
function configured(websearch: ConfigWebSearch.Selection): Document {
|
||||
return new Document({ type: "document", info: new Info({ websearch }) })
|
||||
}
|
||||
|
||||
const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
|
||||
for (let attempt = 0; attempt < 200; attempt++) {
|
||||
if (yield* condition) return
|
||||
yield* Effect.sleep("10 millis")
|
||||
}
|
||||
yield* Effect.die(new Error("Timed out waiting for websearch config reload"))
|
||||
})
|
||||
Reference in New Issue
Block a user