mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-05 02:44:51 +00:00
Update the Core compile options and Server replacement values to the current LayerNode API. Preserve test expectations, replacement targets, and layer lifetimes.
329 lines
15 KiB
TypeScript
329 lines
15 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { App } from "@opencode-ai/core/app"
|
|
import { Bus } from "@opencode-ai/core/bus"
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
|
|
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
|
import { Form } from "@opencode-ai/core/form"
|
|
import { Instance } from "@opencode-ai/core/instance"
|
|
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
|
import { Permission } from "@opencode-ai/core/permission"
|
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
|
import { Session } from "@opencode-ai/core/session"
|
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|
import { Plugin } from "@opencode-ai/plugin/effect"
|
|
import { Agent } from "@opencode-ai/schema/agent"
|
|
import { Location } from "@opencode-ai/schema/location"
|
|
import { AbsolutePath } from "@opencode-ai/schema/schema"
|
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|
import { Global } from "@opencode-ai/util/global"
|
|
import { Context, Duration, Effect, Layer, LayerMap, Option, RcMap, Schema, Scope } from "effect"
|
|
import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
|
import { LanguageModel, LLMClient } from "../../ai/src"
|
|
import { OpenAIChat } from "../../ai/src/protocols/openai-chat"
|
|
import { TestLLM } from "../../ai/src/testing"
|
|
import { tempGlobalLayer } from "../../core/test/fixture/global"
|
|
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
|
|
import { it } from "../../core/test/lib/effect"
|
|
import { createEmbeddedRoutes } from "../src/routes"
|
|
|
|
it.live(
|
|
"isolates same-directory Session tools, hooks, commands, and HTTP requests through one selector",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const directory = yield* tmpdirScoped()
|
|
const location = Location.Ref.make({ directory: AbsolutePath.make(directory.path) })
|
|
const first = { id: Session.ID.make("ses_instance_first"), tool: "instance_first", temperature: 0.1 }
|
|
const second = { id: Session.ID.make("ses_instance_second"), tool: "instance_second", temperature: 0.2 }
|
|
const configs = [first, second]
|
|
const boots: Session.ID[] = []
|
|
const executed: Session.ID[] = []
|
|
const commands: Session.ID[] = []
|
|
const llm = yield* TestLLM.Test.pipe(Effect.provide(TestLLM.testLayer()))
|
|
const model = SessionRunnerModel.resolved(
|
|
LanguageModel.make({ id: "instance-model", provider: "test", route: OpenAIChat.route }),
|
|
{
|
|
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
|
cost: [],
|
|
limit: { context: 200_000, output: 8_192 },
|
|
},
|
|
)
|
|
const cell = PluginRuntime.makeCell()
|
|
// Host and private instances must reuse the same global layer identities.
|
|
const replacements: LayerNode.Replacements = [
|
|
Global.node.replace(tempGlobalLayer),
|
|
Database.node.replace(Database.node),
|
|
Bus.node.replace(Bus.node),
|
|
App.node.replace(App.node),
|
|
ModelsDev.node.replace(ModelsDev.configured({ fetch: false })),
|
|
Watcher.node.replace(Watcher.configured({ enabled: false })),
|
|
PluginRuntime.node.replace(PluginRuntime.layerWithCell(cell)),
|
|
PluginRuntime.providerNode.replace(PluginRuntime.providerNodeWithCell(cell)),
|
|
llmClient.replace(Layer.succeed(LLMClient.Service, llm)),
|
|
SessionRunnerModel.node.replace(
|
|
Layer.succeed(SessionRunnerModel.Service, { resolve: () => Effect.succeed(model) }),
|
|
),
|
|
Instance.byLocationNode.replace(
|
|
Layer.effect(
|
|
Instance.Service,
|
|
Effect.gen(function* () {
|
|
const instances = yield* LayerMap.make(
|
|
(id: Session.ID) => {
|
|
const config = configs.find((config) => config.id === id)
|
|
if (!config) throw new Error(`No instance configuration for ${id}`)
|
|
return Instance.layer(location, {
|
|
discovery: false,
|
|
replacements,
|
|
plugins: [
|
|
Plugin.define({
|
|
id: "session-instance",
|
|
effect: Effect.fnUntraced(function* (ctx) {
|
|
boots.push(config.id)
|
|
yield* ctx.agent.transform((draft) =>
|
|
draft.update("build", (agent) => {
|
|
agent.permissions = [{ action: "*", resource: "*", effect: "allow" }]
|
|
}),
|
|
)
|
|
yield* ctx.session.hook("prompt", (event) =>
|
|
Effect.sync(() => {
|
|
event.prompt.text += ` [${config.tool}]`
|
|
}),
|
|
)
|
|
yield* ctx.session.hook("context", (event) =>
|
|
Effect.sync(() => {
|
|
event.generation.temperature = config.temperature
|
|
}),
|
|
)
|
|
yield* ctx.permission.hook("evaluate", (event) =>
|
|
Effect.sync(() => {
|
|
event.effect = event.action === "instance-test" ? "ask" : "allow"
|
|
if (event.action === "instance-test") event.message = config.tool
|
|
}),
|
|
)
|
|
yield* ctx.tool.transform((draft) =>
|
|
draft.add({
|
|
name: config.tool,
|
|
description: `Tool for ${config.id}`,
|
|
input: Schema.Struct({}),
|
|
output: Schema.String,
|
|
options: { codemode: false },
|
|
execute: () =>
|
|
Effect.sync(() => {
|
|
executed.push(config.id)
|
|
return { output: config.id, content: config.id }
|
|
}),
|
|
}),
|
|
)
|
|
yield* ctx.command.transform((draft) =>
|
|
draft.add({
|
|
name: "instance-check",
|
|
execute: (input) =>
|
|
ctx.session
|
|
.prompt({
|
|
sessionID: input.sessionID,
|
|
text: `command ${config.tool}`,
|
|
delivery: input.delivery,
|
|
resume: false,
|
|
})
|
|
.pipe(
|
|
Effect.tap(() => Effect.sync(() => commands.push(config.id))),
|
|
Effect.asVoid,
|
|
),
|
|
}),
|
|
)
|
|
}),
|
|
}),
|
|
],
|
|
})
|
|
},
|
|
{ idleTimeToLive: Duration.infinity },
|
|
)
|
|
return Instance.Service.of({
|
|
provide: (session) => Effect.provide(instances.get(session.id)),
|
|
provideIfLoaded: (session) => (effect) =>
|
|
Effect.scopedWith((scope) =>
|
|
Effect.gen(function* () {
|
|
const context = yield* instances.contextEffectOption(session.id).pipe(Scope.provide(scope))
|
|
if (Option.isNone(context)) return Option.none()
|
|
return Option.some(yield* effect.pipe(Effect.provide(context.value)))
|
|
}),
|
|
),
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
]
|
|
const context = yield* Layer.build(
|
|
createEmbeddedRoutes({}, replacements).pipe(Layer.provide(HttpServer.layerServices)),
|
|
)
|
|
const sessions = Context.get(context, Session.Service)
|
|
const instances = Context.get(context, Instance.Service)
|
|
const locations = Context.get(context, LocationServiceMap.Service)
|
|
const handler = Context.get(context, HttpRouter.HttpRouter)
|
|
.asHttpEffect()
|
|
.pipe(HttpEffect.toWebHandlerWith(context))
|
|
const request = (route: string, body?: unknown) =>
|
|
Effect.promise(() =>
|
|
handler(
|
|
new Request(
|
|
`http://opencode.local${route}`,
|
|
body === undefined
|
|
? undefined
|
|
: { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) },
|
|
),
|
|
),
|
|
)
|
|
|
|
yield* Effect.forEach(configs, (config) =>
|
|
sessions.create({
|
|
id: config.id,
|
|
title: config.tool,
|
|
agent: Agent.ID.make("build"),
|
|
model: model.ref,
|
|
location,
|
|
}),
|
|
)
|
|
expect((yield* sessions.list()).data.map((session) => session.location)).toEqual([location, location])
|
|
expect(yield* sessions.messages({ sessionID: first.id })).toEqual([])
|
|
yield* sessions.get(second.id)
|
|
for (const config of configs) {
|
|
for (const resource of ["permission", "form"]) {
|
|
const response = yield* request(`/api/session/${config.id}/${resource}`)
|
|
expect(response.status).toBe(200)
|
|
expect(yield* Effect.promise<unknown>(() => response.json())).toEqual({ data: [] })
|
|
}
|
|
}
|
|
expect(boots).toEqual([])
|
|
|
|
for (const config of configs) {
|
|
const admitted = yield* sessions.prompt({ sessionID: config.id, text: "run", resume: false })
|
|
expect(admitted.payload.text).toBe(`run [${config.tool}]`)
|
|
}
|
|
expect(boots).toEqual([first.id, second.id])
|
|
|
|
for (const config of configs) {
|
|
yield* llm.push(TestLLM.tool(`call_${config.tool}`, config.tool, {}), TestLLM.text(config.id, config.tool))
|
|
yield* sessions.resume(config.id)
|
|
}
|
|
expect(executed).toEqual([first.id, second.id])
|
|
|
|
for (const config of configs) {
|
|
yield* llm.push(TestLLM.text(`generated ${config.id}`, config.tool))
|
|
expect(yield* sessions.generate({ sessionID: config.id, prompt: "summarize" })).toBe(`generated ${config.id}`)
|
|
yield* sessions.command({ sessionID: config.id, command: "instance-check", text: "" })
|
|
expect(yield* sessions.inbox(config.id)).toMatchObject([
|
|
{ type: "user", payload: { text: `command ${config.tool} [${config.tool}]` } },
|
|
])
|
|
}
|
|
expect(commands).toEqual([first.id, second.id])
|
|
expect(
|
|
(yield* llm.requests()).map((request) => ({
|
|
temperature: request.generation?.temperature,
|
|
tools: request.tools?.filter((tool) => tool.name.startsWith("instance_")).map((tool) => tool.name),
|
|
})),
|
|
).toEqual(
|
|
[first, first, second, second, first, second].map((config) => ({
|
|
temperature: config.temperature,
|
|
tools: [config.tool],
|
|
})),
|
|
)
|
|
|
|
// Seed through Core, then use HTTP to reach those exact private instances.
|
|
const pending = yield* Effect.forEach(configs, (config) =>
|
|
Effect.gen(function* () {
|
|
const session = yield* sessions.get(config.id)
|
|
return yield* Effect.gen(function* () {
|
|
const forms = yield* Form.Service
|
|
const permissions = yield* Permission.Service
|
|
const form = yield* forms.create({
|
|
sessionID: config.id,
|
|
title: config.tool,
|
|
fields: [{ key: "answer", type: "string" }],
|
|
})
|
|
const permission = {
|
|
id: Permission.ID.create(),
|
|
sessionID: config.id,
|
|
action: "instance-test",
|
|
resources: [config.tool],
|
|
}
|
|
expect(yield* permissions.ask(permission)).toEqual({ id: permission.id, effect: "ask" })
|
|
const foreignID = config.id === first.id ? second.id : first.id
|
|
const foreignForm = yield* forms.create({
|
|
sessionID: foreignID,
|
|
title: "Foreign form",
|
|
fields: [{ key: "answer", type: "string" }],
|
|
})
|
|
const foreignPermission = yield* permissions.ask({
|
|
...permission,
|
|
id: Permission.ID.create(),
|
|
sessionID: foreignID,
|
|
})
|
|
return {
|
|
session,
|
|
form,
|
|
permission: { ...permission, message: config.tool },
|
|
foreignForm,
|
|
foreignPermission,
|
|
}
|
|
}).pipe(instances.provide(session))
|
|
}),
|
|
)
|
|
for (const entry of pending) {
|
|
const forms = yield* request(`/api/session/${entry.session.id}/form`)
|
|
expect(forms.status).toBe(200)
|
|
expect(yield* Effect.promise<unknown>(() => forms.json())).toEqual({ data: [entry.form] })
|
|
const permissions = yield* request(`/api/session/${entry.session.id}/permission`)
|
|
expect(permissions.status).toBe(200)
|
|
expect(yield* Effect.promise<unknown>(() => permissions.json())).toEqual({ data: [entry.permission] })
|
|
|
|
// These IDs exist in the selected instance, but belong to the other Session.
|
|
expect((yield* request(`/api/session/${entry.session.id}/form/${entry.foreignForm.id}`)).status).toBe(404)
|
|
expect(
|
|
(yield* request(`/api/session/${entry.session.id}/permission/${entry.foreignPermission.id}`)).status,
|
|
).toBe(404)
|
|
expect(
|
|
(yield* request(`/api/session/${entry.session.id}/form/${entry.foreignForm.id}/reply`, {
|
|
answer: { answer: "wrong" },
|
|
})).status,
|
|
).toBe(404)
|
|
expect(
|
|
(yield* request(`/api/session/${entry.session.id}/permission/${entry.foreignPermission.id}/reply`, {
|
|
reply: "once",
|
|
})).status,
|
|
).toBe(404)
|
|
yield* Effect.gen(function* () {
|
|
const forms = yield* Form.Service
|
|
const permissions = yield* Permission.Service
|
|
expect(yield* forms.state(entry.foreignForm.id)).toEqual({ status: "pending" })
|
|
expect(yield* permissions.get(entry.foreignPermission.id)).toMatchObject({
|
|
sessionID: entry.foreignForm.sessionID,
|
|
})
|
|
}).pipe(instances.provide(entry.session))
|
|
}
|
|
for (const entry of pending) {
|
|
expect(
|
|
(yield* request(`/api/session/${entry.session.id}/form/${entry.form.id}/reply`, {
|
|
answer: { answer: entry.session.id },
|
|
})).status,
|
|
).toBe(204)
|
|
expect(
|
|
(yield* request(`/api/session/${entry.session.id}/permission/${entry.permission.id}/reply`, {
|
|
reply: "once",
|
|
})).status,
|
|
).toBe(204)
|
|
yield* Effect.gen(function* () {
|
|
const forms = yield* Form.Service
|
|
const permissions = yield* Permission.Service
|
|
expect(yield* forms.state(entry.form.id)).toEqual({
|
|
status: "answered",
|
|
answer: { answer: entry.session.id },
|
|
})
|
|
expect(yield* permissions.get(entry.permission.id)).toBeUndefined()
|
|
}).pipe(instances.provide(entry.session))
|
|
}
|
|
expect(boots).toEqual([first.id, second.id])
|
|
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toEqual([])
|
|
}),
|
|
15_000,
|
|
)
|