mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 18:55:37 -04:00
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
212 lines
7.9 KiB
TypeScript
212 lines
7.9 KiB
TypeScript
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { describe, expect } from "bun:test"
|
|
import { DateTime, Effect, Equal, Hash, Schema } from "effect"
|
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
|
import { AgentV2 } 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/core/effect/layer-node"
|
|
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
|
import { Location } from "@opencode-ai/core/location"
|
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
import { SessionV2 } 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 { EventV2 } from "../src/event"
|
|
import { Reference } from "../src/reference"
|
|
import { ToolRegistry } from "../src/tool/registry"
|
|
|
|
const it = testEffect(
|
|
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])),
|
|
)
|
|
|
|
describe("LocationServiceMap", () => {
|
|
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("isolates location state while sharing location policy with catalog", () =>
|
|
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* () {
|
|
yield* Effect.promise(() =>
|
|
fs.writeFile(
|
|
path.join(blocked.path, "opencode.json"),
|
|
JSON.stringify({
|
|
experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "test" }] },
|
|
}),
|
|
),
|
|
)
|
|
|
|
const update = (directory: string) =>
|
|
Effect.gen(function* () {
|
|
yield* Reference.Service
|
|
const catalog = yield* Catalog.Service
|
|
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
|
const registry = yield* ToolRegistry.Service
|
|
yield* waitForTool(registry, "shell")
|
|
yield* waitForTool(registry, "subagent")
|
|
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 blockedState = yield* update(blocked.path)
|
|
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
|
expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([
|
|
"edit",
|
|
"glob",
|
|
"grep",
|
|
"question",
|
|
"read",
|
|
"shell",
|
|
"skill",
|
|
"subagent",
|
|
"todowrite",
|
|
"webfetch",
|
|
"websearch",
|
|
"write",
|
|
])
|
|
const allowedState = yield* update(allowed.path)
|
|
expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true)
|
|
expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([
|
|
"edit",
|
|
"glob",
|
|
"grep",
|
|
"question",
|
|
"read",
|
|
"shell",
|
|
"skill",
|
|
"subagent",
|
|
"todowrite",
|
|
"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",
|
|
api: { type: "native", settings: {} },
|
|
models: { chat: { disabled: true } },
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
)
|
|
const failure = yield* SessionRunnerModel.Service.use((models) =>
|
|
models.resolve(
|
|
SessionV2.Info.make({
|
|
id: SessionV2.ID.make("ses_unavailable_model"),
|
|
projectID: ProjectV2.ID.global,
|
|
title: "test",
|
|
model: {
|
|
id: ModelV2.ID.make("chat"),
|
|
providerID: ProviderV2.ID.make("unavailable"),
|
|
},
|
|
cost: 0,
|
|
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("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* PluginV2.Service
|
|
const reviewer = 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.add(PluginV2.ID.make(reviewer.id), reviewer.effect)
|
|
|
|
expect(yield* (yield* AgentV2.Service).get(AgentV2.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) }))),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
})
|