| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754 |
- import fs from "fs/promises"
- import path from "path"
- import { describe, expect } from "bun:test"
- import { Config } from "@opencode-ai/schema/config"
- import { Money } from "@opencode-ai/schema/money"
- import { DateTime, Deferred, Effect, Equal, Fiber, Hash, RcMap, Schema, Stream } from "effect"
- import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
- import { Agent } from "@opencode-ai/core/agent"
- import { Catalog } from "@opencode-ai/core/catalog"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@opencode-ai/util/effect/layer-node"
- import { LocationServiceMap } from "@opencode-ai/core/location-services"
- import { Location } from "@opencode-ai/core/location"
- import { Plugin } from "@opencode-ai/core/plugin"
- import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
- import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
- import { Model } from "@opencode-ai/core/model"
- import { Project } from "@opencode-ai/core/project"
- import { Provider } from "@opencode-ai/core/provider"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { Session } from "@opencode-ai/core/session"
- import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
- import { tmpdir } from "./fixture/tmpdir"
- import { testEffect } from "./lib/effect"
- import { toolDefinitions, waitForTool } from "./lib/tool"
- import { Database } from "../src/database/database"
- import { Bus } from "../src/bus"
- import { Reference } from "../src/reference"
- import { Tool } from "../src/tool"
- const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, LocationServiceMap.node])))
- const itWithSdk = testEffect(
- AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])),
- )
- describe("LocationServiceMap", () => {
- itWithSdk.live("preserves embedded SDK plugins after Location eviction", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const sdk = yield* SdkPlugins.Service
- const locations = yield* LocationServiceMap.Service
- const id = Agent.ID.make("persistent-sdk-agent")
- const plugin = EffectPlugin.define({
- id: "persistent-sdk-plugin",
- effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
- })
- yield* sdk.register(plugin)
- const ref = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
- const read = Effect.gen(function* () {
- const supervisor = yield* PluginSupervisor.Service
- yield* supervisor.flush
- const agents = yield* Agent.Service
- return yield* agents.get(id)
- })
- expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
- yield* locations.invalidate(ref)
- expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
- }),
- ),
- ),
- )
- itWithSdk.live("waits for explorer activation to complete", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const started = yield* Deferred.make<void>()
- const release = yield* Deferred.make<void>()
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "blocked-initial-activation",
- effect: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(release))),
- }),
- )
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* Deferred.await(started)
- const flushFiber = yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.forkChild,
- )
- expect(flushFiber.pollUnsafe()).toBeUndefined()
- yield* Deferred.succeed(release, undefined)
- yield* Fiber.join(flushFiber)
- yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.timeout("1 second"),
- )
- const explorer = yield* Effect.gen(function* () {
- const agents = yield* Agent.Service
- return yield* agents.resolve("explore")
- }).pipe(Effect.provide(context))
- expect(explorer).toBeDefined()
- expect(explorer?.permissions.length).toBeGreaterThan(0)
- }),
- ),
- ),
- )
- itWithSdk.live("reruns activation for SDK plugins registered during startup", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const firstStarted = yield* Deferred.make<void>()
- const releaseFirst = yield* Deferred.make<void>()
- const secondStarted = yield* Deferred.make<void>()
- const releaseSecond = yield* Deferred.make<void>()
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "fixed-target-first-plugin",
- effect: () =>
- Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))),
- }),
- )
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* Deferred.await(firstStarted)
- const flushFiber = yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Effect.yieldNow
- yield* sdk.register(
- EffectPlugin.define({
- id: "fixed-target-second-plugin",
- effect: () =>
- Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseSecond))),
- }),
- )
- yield* Deferred.succeed(releaseFirst, undefined)
- yield* Deferred.await(secondStarted)
- expect(flushFiber.pollUnsafe()).toBeUndefined()
- yield* Deferred.succeed(releaseSecond, undefined)
- yield* Fiber.join(flushFiber)
- }),
- ),
- ),
- )
- itWithSdk.live("reruns activation for Config updates during startup", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const activations = { count: 0 }
- const file = path.join(dir.path, "opencode.json")
- yield* Effect.promise(() => fs.writeFile(file, "{}"))
- const firstStarted = yield* Deferred.make<void>()
- const releaseFirst = yield* Deferred.make<void>()
- const secondStarted = yield* Deferred.make<void>()
- const releaseSecond = yield* Deferred.make<void>()
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "blocked-config-reload",
- effect: () =>
- Effect.sync(() => ++activations.count).pipe(
- Effect.flatMap((activation) =>
- activation === 1
- ? Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst)))
- : Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseSecond))),
- ),
- ),
- }),
- )
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* Deferred.await(firstStarted)
- const bus = yield* Bus.Service
- const updated = yield* bus.subscribe(Config.Event.Updated).pipe(
- Stream.filter((event) => event.location?.directory === dir.path),
- Stream.runHead,
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Effect.promise(() =>
- fs.writeFile(
- file,
- JSON.stringify({ plugins: [path.join(import.meta.dir, "plugin/fixtures/config-effect-plugin.ts")] }),
- ),
- )
- yield* Fiber.join(updated)
- const flushFiber = yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.forkChild,
- )
- yield* Deferred.succeed(releaseFirst, undefined)
- yield* Deferred.await(secondStarted)
- expect(flushFiber.pollUnsafe()).toBeUndefined()
- yield* Deferred.succeed(releaseSecond, undefined)
- yield* Fiber.join(flushFiber)
- expect(activations.count).toBe(2)
- }),
- ),
- ),
- )
- itWithSdk.live("keeps flush pending while startup updates continue", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- const flushFiber = yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.forkChild({ startImmediately: true }),
- )
- const bus = yield* Bus.Service
- yield* Effect.forEach(
- Array.from({ length: 5 }),
- () => bus.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))),
- { discard: true },
- )
- expect(flushFiber.pollUnsafe()).toBeUndefined()
- yield* Fiber.join(flushFiber)
- }),
- ),
- ),
- )
- itWithSdk.live("does not reload plugins when config updates leave plugin operations unchanged", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const activations = { count: 0 }
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "unchanged-config-plugin",
- effect: () => Effect.sync(() => ++activations.count).pipe(Effect.asVoid),
- }),
- )
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(context))
- expect(activations.count).toBe(1)
- yield* Bus.Service.use((bus) => bus.publish(Config.Event.Updated, {})).pipe(Effect.provide(context))
- yield* Effect.sleep("200 millis")
- expect(activations.count).toBe(1)
- }),
- ),
- ),
- )
- itWithSdk.live("keeps flush open while later hot reload runs", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(context))
- const started = yield* Deferred.make<void>()
- const release = yield* Deferred.make<void>()
- const completed = yield* Deferred.make<void>()
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "post-ready-plugin",
- effect: () =>
- Deferred.succeed(started, undefined).pipe(
- Effect.andThen(Deferred.await(release)),
- Effect.andThen(Deferred.succeed(completed, undefined)),
- ),
- }),
- )
- yield* Deferred.await(started)
- yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.timeout("1 second"),
- )
- yield* Deferred.succeed(release, undefined)
- yield* Deferred.await(completed)
- }),
- ),
- ),
- )
- itWithSdk.live("does not cancel activation when a flush waiter is interrupted", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const started = yield* Deferred.make<void>()
- const release = yield* Deferred.make<void>()
- const completed = yield* Deferred.make<void>()
- const sdk = yield* SdkPlugins.Service
- yield* sdk.register(
- EffectPlugin.define({
- id: "interrupted-waiter-plugin",
- effect: () =>
- Deferred.succeed(started, undefined).pipe(
- Effect.andThen(Deferred.await(release)),
- Effect.andThen(Deferred.succeed(completed, undefined)),
- ),
- }),
- )
- const locations = yield* LocationServiceMap.Service
- const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
- yield* Deferred.await(started)
- const flushFiber = yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.forkChild({ startImmediately: true }),
- )
- yield* Fiber.interrupt(flushFiber)
- yield* Deferred.succeed(release, undefined)
- yield* Deferred.await(completed)
- yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(
- Effect.provide(context),
- Effect.timeout("500 millis"),
- )
- }),
- ),
- ),
- )
- it.live("applies ordered plugin config operations during boot", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- fs.writeFile(path.join(dir.path, "opencode.json"), JSON.stringify({ plugins: ["-*", "opencode.agent"] })),
- )
- const plugins = yield* Effect.gen(function* () {
- const plugins = yield* Plugin.Service
- yield* (yield* PluginSupervisor.Service).flush
- return yield* plugins.list()
- }).pipe(
- Effect.scoped,
- Effect.provide(
- LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })),
- ),
- )
- expect(plugins.map((plugin) => plugin.id)).toEqual([Plugin.ID.make("opencode.agent")])
- }),
- ),
- ),
- )
- it.live("reloads the plugin generation after config updates", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const file = path.join(dir.path, "opencode.json")
- yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] })))
- yield* Effect.gen(function* () {
- const registry = yield* Plugin.Service
- const supervisor = yield* PluginSupervisor.Service
- yield* supervisor.flush
- expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"])
- yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.command"] })))
- for (let attempt = 0; attempt < 100; attempt++) {
- if ((yield* registry.list()).some((plugin) => plugin.id === "opencode.command")) break
- yield* Effect.sleep("20 millis")
- }
- expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.command"])
- yield* Effect.promise(() =>
- fs.writeFile(
- file,
- JSON.stringify({
- plugins: ["-*", path.join(import.meta.dir, "plugin/fixtures/failing-plugin.ts")],
- }),
- ),
- )
- for (let attempt = 0; attempt < 100; attempt++) {
- if ((yield* registry.list()).length === 0) break
- yield* Effect.sleep("20 millis")
- }
- expect(yield* registry.list()).toEqual([])
- yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] })))
- for (let attempt = 0; attempt < 100; attempt++) {
- if ((yield* registry.list()).some((plugin) => plugin.id === "opencode.agent")) break
- yield* Effect.sleep("20 millis")
- }
- expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"])
- }).pipe(
- Effect.scoped,
- Effect.provide(
- LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })),
- ),
- )
- }),
- ),
- ),
- )
- it.live("routes located events only to their location", () =>
- Effect.acquireRelease(
- Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
- (dirs) => Effect.promise(() => Promise.all(dirs.map((dir) => dir[Symbol.asyncDispose]())).then(() => undefined)),
- ).pipe(
- Effect.flatMap(([first, second]) =>
- Effect.scoped(
- Effect.gen(function* () {
- const locations = yield* LocationServiceMap.Service
- const bus = yield* Bus.Service
- const firstRef = Location.Ref.make({ directory: AbsolutePath.make(first.path) })
- const secondRef = Location.Ref.make({ directory: AbsolutePath.make(second.path) })
- const firstContext = yield* locations.contextEffect(firstRef)
- const secondContext = yield* locations.contextEffect(secondRef)
- const received = { first: 0, second: 0 }
- yield* bus.subscribe(Config.Event.Updated).pipe(
- Stream.runForEach(() => Effect.sync(() => received.first++)),
- Effect.provideContext(firstContext),
- Effect.forkScoped({ startImmediately: true }),
- )
- yield* bus.subscribe(Config.Event.Updated).pipe(
- Stream.runForEach(() => Effect.sync(() => received.second++)),
- Effect.provideContext(secondContext),
- Effect.forkScoped({ startImmediately: true }),
- )
- yield* Effect.sleep("10 millis")
- yield* bus.publish(Config.Event.Updated, {}, { location: firstRef })
- yield* Effect.sleep("10 millis")
- expect(received).toEqual({ first: 1, second: 0 })
- }),
- ),
- ),
- ),
- )
- it.live("reuses cached services for constructed and decoded location refs", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.scoped(
- Effect.gen(function* () {
- const locations = yield* LocationServiceMap.Service
- const directory = AbsolutePath.make(dir.path)
- const constructed = Location.Ref.make({ directory })
- const decoded = Schema.decodeUnknownSync(Location.Ref)({ directory })
- expect(constructed).toEqual({ directory, workspaceID: undefined })
- expect(decoded).toEqual(constructed)
- expect(Equal.equals(constructed, decoded)).toBe(true)
- expect(Hash.hash(constructed)).toBe(Hash.hash(decoded))
- expect(yield* locations.contextEffect(constructed)).toBe(yield* locations.contextEffect(decoded))
- }),
- ),
- ),
- ),
- )
- it.live("normalizes ref key shapes to one cached location graph", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.scoped(
- Effect.gen(function* () {
- const locations = yield* LocationServiceMap.Service
- const directory = AbsolutePath.make(dir.path)
- const absent = Location.Ref.make({ directory })
- const present = Location.Ref.make({ directory, workspaceID: undefined })
- // The two shapes are not structurally Equal: own-key sets differ.
- expect(Object.keys(absent)).toEqual(["directory"])
- expect(Object.keys(present)).toEqual(["directory", "workspaceID"])
- expect(Equal.equals(absent, present)).toBe(false)
- const first = yield* locations.contextEffect(absent)
- expect(yield* locations.contextEffect(present)).toBe(first)
- expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(1)
- // Invalidating with the shape opposite to the one that booted must evict.
- yield* locations.invalidate(present)
- expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(0)
- }),
- ),
- ),
- ),
- )
- it.live("isolates catalog state by location", () =>
- Effect.acquireRelease(
- Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
- (dirs) => Effect.promise(() => Promise.all(dirs.map((dir) => dir[Symbol.asyncDispose]())).then(() => undefined)),
- ).pipe(
- Effect.flatMap(([blocked, allowed]) =>
- Effect.gen(function* () {
- const update = (directory: string, providerID: Provider.ID) =>
- Effect.gen(function* () {
- yield* Reference.Service
- const catalog = yield* Catalog.Service
- yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
- const registry = yield* Tool.Service
- // Tool plugins register during the forked PluginSupervisor boot; wait for
- // every expected tool rather than relying on batch ordering.
- yield* Effect.forEach(
- [
- "edit",
- "glob",
- "grep",
- "question",
- "read",
- "shell",
- "skill",
- "subagent",
- "webfetch",
- "websearch",
- "write",
- ],
- (name) => waitForTool(registry, name),
- )
- return {
- providers: yield* catalog.provider.all(),
- tools: yield* toolDefinitions(registry),
- }
- }).pipe(
- Effect.scoped,
- Effect.provide(
- LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(directory) })),
- ),
- )
- const blockedID = Provider.ID.make("blocked-location")
- const allowedID = Provider.ID.make("allowed-location")
- const blockedState = yield* update(blocked.path, blockedID)
- expect(blockedState.providers.some((provider) => provider.id === blockedID)).toBe(true)
- expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false)
- const blockedTools = blockedState.tools.map((tool) => tool.name)
- expect(blockedTools.filter((name) => name !== "execute").sort()).toEqual([
- "edit",
- "glob",
- "grep",
- "patch",
- "question",
- "read",
- "shell",
- "skill",
- "subagent",
- "webfetch",
- "websearch",
- "write",
- ])
- const allowedState = yield* update(allowed.path, allowedID)
- expect(allowedState.providers.some((provider) => provider.id === allowedID)).toBe(true)
- expect(allowedState.providers.some((provider) => provider.id === blockedID)).toBe(false)
- const allowedTools = allowedState.tools.map((tool) => tool.name)
- expect(blockedTools.includes("execute")).toBe(allowedTools.includes("execute"))
- expect(allowedTools.filter((name) => name !== "execute").sort()).toEqual([
- "edit",
- "glob",
- "grep",
- "patch",
- "question",
- "read",
- "shell",
- "skill",
- "subagent",
- "webfetch",
- "websearch",
- "write",
- ])
- }),
- ),
- ),
- )
- it.live("rejects an unavailable selected model during location model resolution", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
- yield* Effect.promise(() =>
- fs.writeFile(
- path.join(dir.path, "opencode.json"),
- JSON.stringify({
- providers: {
- unavailable: {
- name: "Unavailable",
- package: "test-provider",
- models: { chat: { disabled: true } },
- },
- },
- }),
- ),
- )
- const failure = yield* SessionRunnerModel.Service.use((models) =>
- models.resolve(
- Session.Info.make({
- id: Session.ID.make("ses_unavailable_model"),
- projectID: Project.ID.global,
- title: "test",
- model: {
- id: Model.ID.make("chat"),
- providerID: Provider.ID.make("unavailable"),
- },
- cost: Money.USD.zero,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location,
- }),
- ),
- ).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip)
- expect(failure).toMatchObject({
- _tag: "SessionRunnerModel.ModelUnavailableError",
- providerID: "unavailable",
- modelID: "chat",
- })
- }),
- ),
- ),
- )
- it.live("preserves the selected catalog identity when the package model id differs", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
- const resolved = yield* Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- yield* catalog.transform((editor) => {
- editor.provider.update(Provider.ID.make("aliased"), (provider) => {
- provider.package = Provider.aisdk("@ai-sdk/openai")
- })
- editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => {
- // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
- model.modelID = Model.ID.make("base")
- model.variants = [{ id: Model.VariantID.make("high") }]
- })
- })
- const models = yield* SessionRunnerModel.Service
- return yield* models.resolve(
- Session.Info.make({
- id: Session.ID.make("ses_aliased_model"),
- projectID: Project.ID.global,
- title: "test",
- model: {
- id: Model.ID.make("fast"),
- providerID: Provider.ID.make("aliased"),
- variant: Model.VariantID.make("high"),
- },
- cost: Money.USD.zero,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location,
- }),
- )
- }).pipe(Effect.provide(LocationServiceMap.Service.get(location)))
- expect(resolved.ref).toEqual(
- Model.Ref.make({
- id: Model.ID.make("fast"),
- providerID: Provider.ID.make("aliased"),
- variant: Model.VariantID.make("high"),
- }),
- )
- expect(String(resolved.model.id)).toBe("base")
- }),
- ),
- ),
- )
- it.live("installs public plugins into a location", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((dir) =>
- Effect.gen(function* () {
- const plugins = yield* Plugin.Service
- const reviewer = EffectPlugin.define({
- id: "reviewer",
- effect: (ctx) =>
- ctx.agent
- .transform((agent) => {
- agent.update("reviewer", (item) => {
- item.description = "Reviews code"
- item.mode = "subagent"
- })
- })
- .pipe(Effect.asVoid),
- })
- yield* plugins.activate([{ ...reviewer, version: "1" }])
- expect(yield* (yield* Agent.Service).get(Agent.ID.make("reviewer"))).toMatchObject({
- description: "Reviews code",
- mode: "subagent",
- })
- }).pipe(
- Effect.scoped,
- Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
- ),
- ),
- ),
- )
- })
|