diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 48a9508e9e..472d81b8cc 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -30,9 +30,8 @@ import { ConfigNormalize } from "./config/normalize.js" import { WellKnown } from "./wellknown.js" export function latest(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 { diff --git a/packages/core/src/config/plugin/compaction.ts b/packages/core/src/config/plugin/compaction.ts index c265e138fa..40a5a1287a 100644 --- a/packages/core/src/config/plugin/compaction.ts +++ b/packages/core/src/config/plugin/compaction.ts @@ -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 }), }) } }) diff --git a/packages/core/src/config/plugin/entry-observer.ts b/packages/core/src/config/plugin/entry-observer.ts new file mode 100644 index 0000000000..dd9a832c0b --- /dev/null +++ b/packages/core/src/config/plugin/entry-observer.ts @@ -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, +) { + 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 +}) diff --git a/packages/core/src/config/plugin/formatter.ts b/packages/core/src/config/plugin/formatter.ts index 85f8e8707e..151be859d4 100644 --- a/packages/core/src/config/plugin/formatter.ts +++ b/packages/core/src/config/plugin/formatter.ts @@ -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") diff --git a/packages/core/src/config/plugin/image.ts b/packages/core/src/config/plugin/image.ts index 4ba9361560..5595591a9a 100644 --- a/packages/core/src/config/plugin/image.ts +++ b/packages/core/src/config/plugin/image.ts @@ -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 diff --git a/packages/core/src/config/plugin/location-watcher.ts b/packages/core/src/config/plugin/location-watcher.ts index c6f4c91a7a..539227fe43 100644 --- a/packages/core/src/config/plugin/location-watcher.ts +++ b/packages/core/src/config/plugin/location-watcher.ts @@ -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 diff --git a/packages/core/src/config/plugin/policy.ts b/packages/core/src/config/plugin/policy.ts index 7509e2b9b9..f2a69b0886 100644 --- a/packages/core/src/config/plugin/policy.ts +++ b/packages/core/src/config/plugin/policy.ts @@ -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 }), - ) }), }) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 6d89cb6b7a..8d51e78cfc 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -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 }), - ) }), }) diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index 9b8b444962..58f8e7a777 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -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() 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 }), - ) }), }) diff --git a/packages/core/src/config/plugin/shell.ts b/packages/core/src/config/plugin/shell.ts index 61e77eb0e3..08a0bb55f2 100644 --- a/packages/core/src/config/plugin/shell.ts +++ b/packages/core/src/config/plugin/shell.ts @@ -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) diff --git a/packages/core/src/config/plugin/snapshot.ts b/packages/core/src/config/plugin/snapshot.ts index ae98db54ed..d045dcc16b 100644 --- a/packages/core/src/config/plugin/snapshot.ts +++ b/packages/core/src/config/plugin/snapshot.ts @@ -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 diff --git a/packages/core/src/config/plugin/tool-output.ts b/packages/core/src/config/plugin/tool-output.ts index cfbb49e5cc..c8edd58151 100644 --- a/packages/core/src/config/plugin/tool-output.ts +++ b/packages/core/src/config/plugin/tool-output.ts @@ -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 diff --git a/packages/core/src/config/plugin/websearch.ts b/packages/core/src/config/plugin/websearch.ts index 8bfef38665..502dd234eb 100644 --- a/packages/core/src/config/plugin/websearch.ts +++ b/packages/core/src/config/plugin/websearch.ts @@ -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 }), - ) }), }) diff --git a/packages/core/test/config/entry-observer.test.ts b/packages/core/test/config/entry-observer.test.ts new file mode 100644 index 0000000000..d6743a192b --- /dev/null +++ b/packages/core/test/config/entry-observer.test.ts @@ -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>() + const reloaded = yield* Deferred.make() + 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: {} } +} diff --git a/packages/core/test/config/reload.test.ts b/packages/core/test/config/reload.test.ts index 13e1f1d128..eba5bc4153 100644 --- a/packages/core/test/config/reload.test.ts +++ b/packages/core/test/config/reload.test.ts @@ -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 diff --git a/packages/core/test/config/websearch.test.ts b/packages/core/test/config/websearch.test.ts new file mode 100644 index 0000000000..ddd21e7c01 --- /dev/null +++ b/packages/core/test/config/websearch.test.ts @@ -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) { + 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")) +})