mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 05:03:37 +00:00
test(core): add config and watcher test services (#39157)
This commit is contained in:
parent
f5700808c5
commit
713658c07b
20 changed files with 468 additions and 498 deletions
|
|
@ -4,7 +4,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|||
import path from "path"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Semaphore, Stream } from "effect"
|
||||
import { Context, Effect, Fiber, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
|
|
@ -176,6 +176,31 @@ export type Options = typeof Options.Type
|
|||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
|
||||
|
||||
export interface TestInterface extends Interface {
|
||||
/** Replaces the entries returned by subsequent entries() calls. */
|
||||
readonly setEntries: (entries: Entry[]) => Effect.Effect<void>
|
||||
/** Emits one filesystem update to every changes() subscriber. */
|
||||
readonly emitChange: (update: Watcher.Update) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Test extends Context.Service<Test, TestInterface>()("@opencode/Config/Test") {}
|
||||
|
||||
/** In-memory config for tests: static entries with replaceable state and a test-driven change feed. */
|
||||
export const testLayer = (initial: Entry[] = []) =>
|
||||
Layer.effectContext(
|
||||
Effect.gen(function* () {
|
||||
const entries = yield* Ref.make(initial)
|
||||
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||
const service = Test.of({
|
||||
entries: () => Ref.get(entries),
|
||||
changes: () => Stream.fromPubSub(updates),
|
||||
setEntries: (next) => Ref.set(entries, next),
|
||||
emitChange: (update) => PubSub.publish(updates, update).pipe(Effect.asVoid),
|
||||
})
|
||||
return Context.empty().pipe(Context.add(Service, service), Context.add(Test, service))
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = (options?: Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,32 @@ export type Options = typeof Options.Type
|
|||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Watcher") {}
|
||||
|
||||
export interface TestInterface extends Interface {
|
||||
/** Broadcasts one update to every subscriber. */
|
||||
readonly emit: (update: Update) => Effect.Effect<void>
|
||||
/** Returns every subscribe call observed so far, in order. */
|
||||
readonly subscriptions: () => Effect.Effect<readonly WatchInput[]>
|
||||
}
|
||||
|
||||
export class Test extends Context.Service<Test, TestInterface>()("@opencode/Watcher/Test") {}
|
||||
|
||||
/** In-memory watcher for tests: records subscribe calls and broadcasts emitted updates. */
|
||||
export const testLayer = Layer.effectContext(
|
||||
Effect.gen(function* () {
|
||||
const updates = yield* PubSub.unbounded<Update>()
|
||||
const subscriptions: WatchInput[] = []
|
||||
const service = Test.of({
|
||||
subscribe: (input) => {
|
||||
subscriptions.push(input)
|
||||
return Stream.fromPubSub(updates)
|
||||
},
|
||||
emit: (update) => PubSub.publish(updates, update).pipe(Effect.asVoid),
|
||||
subscriptions: () => Effect.sync(() => [...subscriptions]),
|
||||
})
|
||||
return Context.empty().pipe(Context.add(Service, service), Context.add(Test, service))
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = (options?: Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
it.effect("matches POSIX paths against home-relative permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const permissions = yield* loadHomePermissions("/home/test")
|
||||
expect(Permission.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(Permission.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe("allow")
|
||||
expect(Permission.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny")
|
||||
expect(Permission.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny")
|
||||
expect(Permission.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny")
|
||||
|
|
@ -66,49 +64,45 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
permissions: [{ action: "bash", resource: "*", effect: "ask" }],
|
||||
agents: {
|
||||
build: {
|
||||
permissions: [{ action: "bash", resource: "git *", effect: "allow" }],
|
||||
},
|
||||
reviewer: {
|
||||
model: "openrouter/openai/gpt-5",
|
||||
description: "Review changes",
|
||||
mode: "subagent",
|
||||
permissions: [
|
||||
{ action: "edit", resource: "*", effect: "deny" },
|
||||
{ action: "read", resource: "*", effect: "deny" },
|
||||
],
|
||||
},
|
||||
removed: { description: "Removed later" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
permissions: [{ action: "read", resource: "*", effect: "allow" }],
|
||||
agents: {
|
||||
reviewer: { model: "openrouter/openai/gpt-5#high", hidden: true },
|
||||
removed: { disabled: true },
|
||||
late: {
|
||||
permissions: [{ action: "edit", resource: "*", effect: "allow" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
permissions: [{ action: "bash", resource: "*", effect: "ask" }],
|
||||
agents: {
|
||||
build: {
|
||||
permissions: [{ action: "bash", resource: "git *", effect: "allow" }],
|
||||
},
|
||||
reviewer: {
|
||||
model: "openrouter/openai/gpt-5",
|
||||
description: "Review changes",
|
||||
mode: "subagent",
|
||||
permissions: [
|
||||
{ action: "edit", resource: "*", effect: "deny" },
|
||||
{ action: "read", resource: "*", effect: "deny" },
|
||||
],
|
||||
},
|
||||
removed: { description: "Removed later" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
permissions: [{ action: "read", resource: "*", effect: "allow" }],
|
||||
agents: {
|
||||
reviewer: { model: "openrouter/openai/gpt-5#high", hidden: true },
|
||||
removed: { disabled: true },
|
||||
late: {
|
||||
permissions: [{ action: "edit", resource: "*", effect: "allow" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provide(Config.testLayer(entries)),
|
||||
)
|
||||
|
||||
const buildAgent = yield* agents.get(build)
|
||||
|
|
@ -152,48 +146,44 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
it.effect("maps configured agent fields and preserves an unspecified model variant", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* Agent.Service
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
model: "anthropic/claude-sonnet",
|
||||
system: "Review carefully.",
|
||||
description: "Reviews changes",
|
||||
mode: "subagent",
|
||||
hidden: true,
|
||||
color: "#ff6b6b",
|
||||
steps: 12,
|
||||
request: {
|
||||
headers: { first: "one", shared: "first" },
|
||||
body: { enabled: true, profile: "review", effort: "medium" },
|
||||
},
|
||||
},
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
model: "anthropic/claude-sonnet",
|
||||
system: "Review carefully.",
|
||||
description: "Reviews changes",
|
||||
mode: "subagent",
|
||||
hidden: true,
|
||||
color: "#ff6b6b",
|
||||
steps: 12,
|
||||
request: {
|
||||
headers: { first: "one", shared: "first" },
|
||||
body: { enabled: true, profile: "review", effort: "medium" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
request: {
|
||||
headers: { shared: "last", second: "two" },
|
||||
body: { retries: 2, effort: "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
request: {
|
||||
headers: { shared: "last", second: "two" },
|
||||
body: { retries: 2, effort: "high" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provide(Config.testLayer(entries)),
|
||||
)
|
||||
|
||||
const reviewer = yield* agents.get(Agent.ID.make("reviewer"))
|
||||
|
|
@ -221,19 +211,15 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
const build = Agent.ID.make("build")
|
||||
yield* agents.transform((editor) => editor.update(build, () => {}))
|
||||
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ agents: { build: { disabled: true } } }),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ agents: { build: { disabled: true } } }),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provide(Config.testLayer(entries)),
|
||||
)
|
||||
|
||||
expect(yield* agents.get(build)).toBeUndefined()
|
||||
|
|
@ -281,20 +267,16 @@ Use native v2 fields.`,
|
|||
await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.")
|
||||
})
|
||||
const agents = yield* Agent.Service
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ agents: { reviewer: { description: "JSON description" } } }),
|
||||
}),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }),
|
||||
]),
|
||||
})
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ agents: { reviewer: { description: "JSON description" } } }),
|
||||
}),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }),
|
||||
]
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provide(Config.testLayer(entries)),
|
||||
)
|
||||
|
||||
expect(yield* agents.get(Agent.ID.make("reviewer"))).toMatchObject({
|
||||
|
|
@ -323,41 +305,37 @@ function loadHomePermissions(home: string) {
|
|||
const agents = yield* Agent.Service
|
||||
const build = Agent.ID.make("build")
|
||||
yield* agents.transform((editor) => editor.update(build, () => {}))
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
permission: {
|
||||
external_directory: {
|
||||
"~/p/**": "allow",
|
||||
"/some/~/path": "deny",
|
||||
"$HOMELESS/**": "deny",
|
||||
},
|
||||
bash: {
|
||||
"$HOME/private/**": "deny",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
build: {
|
||||
permission: {
|
||||
external_directory: {
|
||||
"~/p/**": "allow",
|
||||
"/some/~/path": "deny",
|
||||
"$HOMELESS/**": "deny",
|
||||
},
|
||||
bash: {
|
||||
"$HOME/private/**": "deny",
|
||||
"$HOME/cache/**": "deny",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
build: {
|
||||
permission: {
|
||||
external_directory: {
|
||||
"$HOME/cache/**": "deny",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]),
|
||||
})
|
||||
),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provide(Config.testLayer(entries)),
|
||||
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -68,19 +68,14 @@ Review files`,
|
|||
event: { subscribe: () => Stream.fromPubSub(updates) },
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { template: "Inline review" } } }),
|
||||
}),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }),
|
||||
]),
|
||||
}),
|
||||
Effect.provide(
|
||||
Config.testLayer([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { template: "Inline review" } } }),
|
||||
}),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }),
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function testLayer(
|
|||
globalDirectory = path.join(directory, "global"),
|
||||
projectDirectory = directory,
|
||||
vcs?: Project.Vcs,
|
||||
watcher?: Layer.Layer<Watcher.Service>,
|
||||
watcher: Layer.Layer<Watcher.Service | Watcher.Test> = Watcher.testLayer,
|
||||
credentialNode = emptyCredentialNode,
|
||||
wellknownNode = emptyWellknownNode,
|
||||
options?: Config.Options,
|
||||
|
|
@ -81,14 +81,17 @@ function testLayer(
|
|||
),
|
||||
),
|
||||
)
|
||||
return AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
|
||||
const built = AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
|
||||
[Config.node, Config.configured(options)],
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })],
|
||||
[Credential.node, credentialNode],
|
||||
[WellKnown.node, wellknownNode],
|
||||
...(watcher ? ([[Watcher.node, watcher]] as const) : []),
|
||||
[Watcher.node, watcher],
|
||||
])
|
||||
// Merge the watcher layer by reference so Watcher.Test resolves to the same
|
||||
// memoized instance the built graph uses.
|
||||
return Layer.mergeAll(built, watcher)
|
||||
}
|
||||
|
||||
const provider = {
|
||||
|
|
@ -184,32 +187,22 @@ describe("Config", () => {
|
|||
await fs.mkdir(project, { recursive: true })
|
||||
await fs.writeFile(file, JSON.stringify({ shell: "first" }))
|
||||
})
|
||||
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||
const watcher = Layer.succeed(
|
||||
Watcher.Service,
|
||||
Watcher.Service.of({
|
||||
subscribe: () => Stream.fromPubSub(updates),
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const bus = yield* Bus.Service
|
||||
const watcher = yield* Watcher.Test
|
||||
const changed = yield* bus
|
||||
.subscribe(ConfigSchema.Event.Updated)
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.sleep("10 millis")
|
||||
|
||||
yield* PubSub.publish(updates, {
|
||||
type: "update",
|
||||
path: path.join(global, "commands", "review.md"),
|
||||
} satisfies Watcher.Update)
|
||||
yield* watcher.emit({ type: "update", path: path.join(global, "commands", "review.md") })
|
||||
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ shell: "second" })))
|
||||
yield* PubSub.publish(updates, { type: "update", path: file } satisfies Watcher.Update)
|
||||
yield* watcher.emit({ type: "update", path: file })
|
||||
|
||||
expect(yield* Fiber.join(changed)).toHaveLength(1)
|
||||
expect(Config.latest(yield* config.entries(), "shell")).toBe("second")
|
||||
}).pipe(Effect.provide(testLayer(project, global, project, undefined, watcher)))
|
||||
}).pipe(Effect.provide(testLayer(project, global, project, undefined, Watcher.testLayer)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -228,32 +221,44 @@ describe("Config", () => {
|
|||
await fs.mkdir(path.join(global, "commands"), { recursive: true })
|
||||
await fs.mkdir(project, { recursive: true })
|
||||
})
|
||||
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||
const watcher = Layer.succeed(
|
||||
Watcher.Service,
|
||||
Watcher.Service.of({
|
||||
subscribe: () => Stream.fromPubSub(updates),
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const watcher = yield* Watcher.Test
|
||||
const received = yield* config
|
||||
.changes()
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
|
||||
yield* Effect.sleep("10 millis")
|
||||
|
||||
const file = path.join(global, "commands", "review.md")
|
||||
yield* PubSub.publish(updates, { type: "update", path: file } satisfies Watcher.Update)
|
||||
yield* watcher.emit({ type: "update", path: file })
|
||||
|
||||
const collected = yield* Fiber.join(received).pipe(Effect.timeout("1 second"))
|
||||
expect(Array.from(collected)).toEqual([{ type: "update", path: file }])
|
||||
}).pipe(Effect.provide(testLayer(project, global, project, undefined, watcher)))
|
||||
}).pipe(Effect.provide(testLayer(project, global, project, undefined, Watcher.testLayer)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("backs Config.Service and Config.Test with one shared test implementation", () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const test = yield* Config.Test
|
||||
expect(yield* config.entries()).toEqual([])
|
||||
|
||||
const entry = new Config.Document({ type: "document", info: new Config.Info({}) })
|
||||
yield* test.setEntries([entry])
|
||||
expect(yield* config.entries()).toEqual([entry])
|
||||
|
||||
const received = yield* config
|
||||
.changes()
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
|
||||
yield* Effect.yieldNow
|
||||
yield* test.emitChange({ type: "create", path: "/root/commands/review.md" })
|
||||
expect(Array.from(yield* Fiber.join(received))).toEqual([{ type: "create", path: "/root/commands/review.md" }])
|
||||
}).pipe(Effect.provide(Config.testLayer())),
|
||||
)
|
||||
|
||||
it.effect("returns the latest defined scalar from priority-ordered documents", () =>
|
||||
Effect.sync(() => {
|
||||
const entries = [
|
||||
|
|
@ -544,23 +549,15 @@ describe("Config", () => {
|
|||
fs.mkdir(path.join(tmp.path, ".agents"), { recursive: true }),
|
||||
]),
|
||||
)
|
||||
const targets: Watcher.WatchInput[] = []
|
||||
const watcher = Layer.succeed(
|
||||
Watcher.Service,
|
||||
Watcher.Service.of({
|
||||
subscribe: (input) => {
|
||||
targets.push(input)
|
||||
return Stream.never
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const watcher = yield* Watcher.Test
|
||||
yield* config.entries()
|
||||
|
||||
expect(targets).toEqual([{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path, undefined, undefined, undefined, watcher)))
|
||||
expect(yield* watcher.subscriptions()).toEqual([
|
||||
{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) },
|
||||
])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path, undefined, undefined, undefined, Watcher.testLayer)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,15 +24,10 @@ const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) =>
|
|||
}),
|
||||
})
|
||||
|
||||
const addPlugin = Effect.fn(function* (entries: () => Config.Entry[]) {
|
||||
const addPlugin = Effect.fn(function* (entries: Config.Entry[]) {
|
||||
const plugin = yield* Plugin.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* ConfigPolicyPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.sync(entries) }),
|
||||
),
|
||||
)
|
||||
yield* ConfigPolicyPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries)))
|
||||
})
|
||||
|
||||
describe("ConfigPolicyPlugin.Plugin", () => {
|
||||
|
|
@ -44,7 +39,7 @@ describe("ConfigPolicyPlugin.Plugin", () => {
|
|||
catalog.provider.update(Provider.ID.anthropic, () => {})
|
||||
catalog.provider.update(Provider.ID.make("company-internal"), () => {})
|
||||
})
|
||||
yield* addPlugin(() => [
|
||||
yield* addPlugin([
|
||||
policies(
|
||||
{ effect: "deny", resource: "*" },
|
||||
{ effect: "allow", resource: "anthropic" },
|
||||
|
|
@ -62,7 +57,7 @@ describe("ConfigPolicyPlugin.Plugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {}))
|
||||
yield* addPlugin(() => [
|
||||
yield* addPlugin([
|
||||
policies({ effect: "deny", resource: "openai" }),
|
||||
policies({ effect: "allow", resource: "openai" }),
|
||||
])
|
||||
|
|
@ -75,17 +70,17 @@ describe("ConfigPolicyPlugin.Plugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const bus = yield* Bus.Service
|
||||
let entries: Config.Entry[] = [policies({ effect: "deny", resource: "openai" })]
|
||||
const test = yield* Config.Test
|
||||
const plugin = yield* Plugin.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {}))
|
||||
yield* addPlugin(() => entries)
|
||||
yield* ConfigPolicyPlugin.Plugin.effect(host)
|
||||
expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined()
|
||||
|
||||
entries = [policies({ effect: "allow", resource: "openai" })]
|
||||
yield* test.setEntries([policies({ effect: "allow", resource: "openai" })])
|
||||
yield* bus.publish(ConfigSchema.Event.Updated, {})
|
||||
yield* waitUntil(
|
||||
catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined)),
|
||||
)
|
||||
}),
|
||||
yield* waitUntil(catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined)))
|
||||
}).pipe(Effect.provide(Config.testLayer([policies({ effect: "deny", resource: "openai" })]))),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import { PluginTestLayer } from "../plugin/fixture"
|
|||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* (config: Config.Interface) {
|
||||
const addPlugin = Effect.fn(function* (entries: Config.Entry[]) {
|
||||
const plugin = yield* Plugin.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config))
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries)))
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
|
|
@ -54,25 +54,21 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("chat")
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: { chat: {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: { chat: {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(config)
|
||||
yield* addPlugin(entries)
|
||||
|
||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||
expect(model.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] })
|
||||
|
|
@ -93,30 +89,26 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
model.capabilities = { tools: false, input: ["text"], output: ["text"] }
|
||||
})
|
||||
})
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
inherited: { name: "Inherited" },
|
||||
overridden: {
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
inherited: { name: "Inherited" },
|
||||
overridden: {
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(config)
|
||||
yield* addPlugin(entries)
|
||||
|
||||
expect((yield* catalog.model.get(providerID, inheritedID))?.capabilities).toEqual({
|
||||
tools: false,
|
||||
|
|
@ -136,39 +128,35 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.opencode
|
||||
const modelID = Model.ID.make("alpha-gpt-next")
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
settings: { baseURL: "https://opencode.test/v1" },
|
||||
models: {
|
||||
"alpha-gpt-next": {
|
||||
variants: [
|
||||
{
|
||||
id: "high",
|
||||
body: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
],
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
settings: { baseURL: "https://opencode.test/v1" },
|
||||
models: {
|
||||
"alpha-gpt-next": {
|
||||
variants: [
|
||||
{
|
||||
id: "high",
|
||||
body: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(config)
|
||||
yield* addPlugin(entries)
|
||||
|
||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||
expect(model.variants).toMatchObject([
|
||||
|
|
@ -189,39 +177,35 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.opencode
|
||||
const modelID = Model.ID.make("alpha-gpt-next")
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
settings: { baseURL: "https://opencode.test/v1" },
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
settings: { baseURL: "https://opencode.test/v1" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
models: {
|
||||
"alpha-gpt-next": {
|
||||
variants: [{ id: "high", body: { reasoningEffort: "high" } }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
models: {
|
||||
"alpha-gpt-next": {
|
||||
variants: [{ id: "high", body: { reasoningEffort: "high" } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(config)
|
||||
yield* addPlugin(entries)
|
||||
|
||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||
expect(model.variants?.[0]).toMatchObject({
|
||||
|
|
@ -238,88 +222,84 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||
const integrations = yield* Integration.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("chat")
|
||||
const config = Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
model: "custom/first",
|
||||
providers: {
|
||||
custom: {
|
||||
name: "Configured",
|
||||
env: ["CUSTOM_API_KEY"],
|
||||
package: "native",
|
||||
const entries = [
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
model: "custom/first",
|
||||
providers: {
|
||||
custom: {
|
||||
name: "Configured",
|
||||
env: ["CUSTOM_API_KEY"],
|
||||
package: "native",
|
||||
headers: { first: "first", shared: "first" },
|
||||
models: {
|
||||
chat: {
|
||||
name: "First",
|
||||
compatibility: { reasoningField: "vendor_reasoning" },
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
disabled: true,
|
||||
limit: { context: 100, output: 50 },
|
||||
cost: { input: 1, output: 2 },
|
||||
settings: { retained: true },
|
||||
headers: { first: "first", shared: "first" },
|
||||
models: {
|
||||
chat: {
|
||||
name: "First",
|
||||
compatibility: { reasoningField: "vendor_reasoning" },
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
disabled: true,
|
||||
limit: { context: 100, output: 50 },
|
||||
cost: { input: 1, output: 2 },
|
||||
settings: { retained: true },
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { first: "first", shared: "first" },
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { first: "first", shared: "first" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
model: "custom/default",
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:custom-sdk",
|
||||
settings: { baseURL: "https://example.test" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
model: "custom/default",
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:custom-sdk",
|
||||
settings: { baseURL: "https://example.test" },
|
||||
headers: { last: "last", shared: "last" },
|
||||
models: {
|
||||
default: {
|
||||
name: "Default",
|
||||
},
|
||||
chat: {
|
||||
modelID: "api-chat",
|
||||
name: "Last",
|
||||
limit: { output: 75 },
|
||||
headers: { last: "last", shared: "last" },
|
||||
models: {
|
||||
default: {
|
||||
name: "Default",
|
||||
},
|
||||
chat: {
|
||||
modelID: "api-chat",
|
||||
name: "Last",
|
||||
limit: { output: 75 },
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { last: "last", shared: "last" },
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { last: "last", shared: "last" },
|
||||
},
|
||||
{
|
||||
id: "slow",
|
||||
headers: { slow: "slow" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "slow",
|
||||
headers: { slow: "slow" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: { name: "Renamed" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: { name: "Renamed" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(config)
|
||||
yield* addPlugin(entries)
|
||||
|
||||
const provider = required(yield* catalog.provider.get(providerID))
|
||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ 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 { Effect, Schema, Stream } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "../plugin/fixture"
|
||||
|
||||
|
|
@ -36,16 +36,13 @@ describe("config plugin reloads", () => {
|
|||
const references = yield* Reference.Service
|
||||
const skills = yield* Skill.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
let entries: Config.Entry[] = [config("first")]
|
||||
const service = Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.sync(() => entries) })
|
||||
const setup = <R>(effect: Effect.Effect<void, never, R>) =>
|
||||
effect.pipe(Effect.provideService(Config.Service, service))
|
||||
const test = yield* Config.Test
|
||||
|
||||
yield* setup(ConfigAgentPlugin.Plugin.effect(host))
|
||||
yield* setup(ConfigCommandPlugin.Plugin.effect(host))
|
||||
yield* setup(ConfigSkillPlugin.Plugin.effect(host))
|
||||
yield* setup(ConfigReferencePlugin.Plugin.effect(host))
|
||||
yield* setup(ConfigProviderPlugin.Plugin.effect(host))
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host)
|
||||
yield* ConfigCommandPlugin.Plugin.effect(host)
|
||||
yield* ConfigSkillPlugin.Plugin.effect(host)
|
||||
yield* ConfigReferencePlugin.Plugin.effect(host)
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host)
|
||||
|
||||
expect((yield* agents.get(Agent.ID.make("first")))?.description).toBe("First agent")
|
||||
expect((yield* commands.get("first"))?.description).toBe("First command")
|
||||
|
|
@ -55,7 +52,7 @@ describe("config plugin reloads", () => {
|
|||
expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
|
||||
expect(yield* catalog.provider.get(Provider.ID.make("first"))).toBeDefined()
|
||||
|
||||
entries = [config("second")]
|
||||
yield* test.setEntries([config("second")])
|
||||
yield* bus.publish(ConfigSchema.Event.Updated, {})
|
||||
yield* waitUntil(
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -77,7 +74,10 @@ describe("config plugin reloads", () => {
|
|||
expect(
|
||||
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
|
||||
).toBe(true)
|
||||
}).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
|
||||
}).pipe(
|
||||
Effect.provide(Config.testLayer([config("first")])),
|
||||
Effect.provideService(Global.Service, Global.Service.of(Global.make())),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -41,23 +41,18 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||
).pipe(
|
||||
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })),
|
||||
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/repo/.claude") }),
|
||||
new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/repo/.agents") }),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
skills: ["./skills", "~/shared-skills", "/opt/skills", "https://example.test/skills/"],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
Effect.provide(
|
||||
Config.testLayer([
|
||||
new Config.ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/repo/.claude") }),
|
||||
new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/repo/.agents") }),
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
skills: ["./skills", "~/shared-skills", "/opt/skills", "https://example.test/skills/"],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,25 @@ type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
|||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Bus.node])))
|
||||
|
||||
const configLayer = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
)
|
||||
const configLayer = Config.testLayer()
|
||||
|
||||
describe("Watcher.testLayer", () => {
|
||||
it.effect("records subscriptions and broadcasts emitted updates through the service", () =>
|
||||
Effect.gen(function* () {
|
||||
const watcher = yield* Watcher.Service
|
||||
const test = yield* Watcher.Test
|
||||
const received = yield* watcher
|
||||
.subscribe({ path: "/root", type: "directory" })
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* test.emit({ type: "update", path: "/root/file.md" })
|
||||
|
||||
expect(Array.from(yield* Fiber.join(received))).toEqual([{ type: "update", path: "/root/file.md" }])
|
||||
expect(yield* test.subscriptions()).toEqual([{ path: "/root", type: "directory" }])
|
||||
}).pipe(Effect.provide(Watcher.testLayer)),
|
||||
)
|
||||
})
|
||||
|
||||
function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
||||
const locationLayer = Layer.succeed(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@ export const emptyMcpLayer = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
|
||||
export const emptyConfigLayer = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
)
|
||||
export const emptyConfigLayer = Config.testLayer()
|
||||
|
||||
export const testLocationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
|
|
|
|||
|
|
@ -160,28 +160,21 @@ function resourceMcpLayer(
|
|||
Layer.provideMerge(Form.layer),
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
mcp: new ConfigMCP.Info({
|
||||
servers: {
|
||||
resources:
|
||||
typeof server === "string"
|
||||
? new ConfigMCP.Remote({ type: "remote", url: server, oauth: false })
|
||||
: server,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
Config.testLayer([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
mcp: new ConfigMCP.Info({
|
||||
servers: {
|
||||
resources:
|
||||
typeof server === "string"
|
||||
? new ConfigMCP.Remote({ type: "remote", url: server, oauth: false })
|
||||
: server,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
]),
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory }))),
|
||||
Layer.mock(Bus.Service, {
|
||||
subscribe: () => Stream.never,
|
||||
|
|
|
|||
|
|
@ -60,12 +60,6 @@ export const PluginTestLayer = AppNodeBuilder.build(
|
|||
[
|
||||
[Location.node, tempLocationLayer],
|
||||
[Npm.node, npmLayer],
|
||||
[
|
||||
Config.node,
|
||||
Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
),
|
||||
],
|
||||
[Config.node, Config.testLayer()],
|
||||
],
|
||||
) as unknown as Layer.Layer<unknown, never>
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ describe("SkillPlugin.Plugin", () => {
|
|||
},
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
),
|
||||
Effect.provide(Config.testLayer()),
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||
|
|
|
|||
|
|
@ -46,10 +46,7 @@ export const webSearchIntegrationTest = testEffect(
|
|||
[
|
||||
[
|
||||
Config.node,
|
||||
Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
),
|
||||
Config.testLayer(),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -70,10 +70,7 @@ const permission = Layer.succeed(
|
|||
list: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
)
|
||||
const config = Config.testLayer()
|
||||
const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]])
|
||||
|
||||
const testLayer = AppNodeBuilder.build(
|
||||
|
|
|
|||
|
|
@ -88,10 +88,7 @@ const referenceInstructions = Layer.mock(ReferenceInstructions.Service, {
|
|||
load: () => Effect.succeed(Instructions.empty),
|
||||
})
|
||||
const mcpInstructions = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
)
|
||||
const config = Config.testLayer()
|
||||
const pluginSupervisor = Layer.succeed(PluginSupervisor.Service, PluginSupervisor.Service.of({ flush: Effect.void }))
|
||||
const promptCatalog = Layer.mock(Catalog.Service, {
|
||||
provider: {
|
||||
|
|
|
|||
|
|
@ -356,24 +356,17 @@ const referenceInstructions = Layer.mock(ReferenceInstructions.Service, {
|
|||
load: () => Effect.succeed(Instructions.empty),
|
||||
})
|
||||
const mcpInstructions = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
changes: () => Stream.empty,
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
compaction: new ConfigCompaction.Info({
|
||||
buffer: 3_000,
|
||||
keep: new ConfigCompaction.Keep({ tokens: 1_000 }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
const config = Config.testLayer([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
compaction: new ConfigCompaction.Info({
|
||||
buffer: 3_000,
|
||||
keep: new ConfigCompaction.Keep({ tokens: 1_000 }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
])
|
||||
let pluginFlushHook = Effect.void
|
||||
const pluginSupervisor = Layer.succeed(
|
||||
PluginSupervisor.Service,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ let readResult: ReadToolFileSystem.FileContent | ReadToolFileSystem.TextPage = {
|
|||
mime: "text/plain",
|
||||
}
|
||||
let readFailure: ReadToolFileSystem.ReadError | undefined
|
||||
let configEntries: Config.Entry[] = []
|
||||
const reader = Layer.succeed(
|
||||
ReadToolFileSystem.Service,
|
||||
ReadToolFileSystem.Service.of({
|
||||
|
|
@ -100,10 +99,7 @@ const permission = Layer.succeed(
|
|||
list: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed(configEntries) }),
|
||||
)
|
||||
const config = Config.testLayer()
|
||||
const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]])
|
||||
const testFileSystem = Layer.effect(
|
||||
FSUtil.Service,
|
||||
|
|
@ -161,16 +157,20 @@ const unavailableImage = Layer.succeed(
|
|||
Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }),
|
||||
)
|
||||
const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
||||
AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [
|
||||
[ReadToolFileSystem.node, reader],
|
||||
[Permission.node, permission],
|
||||
[Config.node, config],
|
||||
[Image.node, imageLayer],
|
||||
[LocationMutation.node, mutation],
|
||||
[FSUtil.node, testFileSystem],
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
||||
])
|
||||
Layer.mergeAll(
|
||||
AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [
|
||||
[ReadToolFileSystem.node, reader],
|
||||
[Permission.node, permission],
|
||||
[Config.node, config],
|
||||
[Image.node, imageLayer],
|
||||
[LocationMutation.node, mutation],
|
||||
[FSUtil.node, testFileSystem],
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
||||
]),
|
||||
// Merge by reference so Config.Test resolves to the memoized instance.
|
||||
config,
|
||||
)
|
||||
const it = testEffect(readLayer(imageLayer))
|
||||
const itWithoutResizer = testEffect(readLayer(unavailableImage))
|
||||
const sessionID = Session.ID.make("ses_read_tool_test")
|
||||
|
|
@ -192,7 +192,6 @@ describe("ReadTool", () => {
|
|||
mime: "text/plain",
|
||||
}
|
||||
readFailure = undefined
|
||||
configEntries = []
|
||||
})
|
||||
|
||||
it.effect("registers, authorizes, and reads through the location filesystem", () =>
|
||||
|
|
@ -409,7 +408,8 @@ describe("ReadTool", () => {
|
|||
encoding: "base64",
|
||||
mime: "image/png",
|
||||
}
|
||||
configEntries = [
|
||||
const configTest = yield* Config.Test
|
||||
yield* configTest.setEntries([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
|
|
@ -418,7 +418,7 @@ describe("ReadTool", () => {
|
|||
}),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
])
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
expect(
|
||||
|
|
@ -451,14 +451,15 @@ describe("ReadTool", () => {
|
|||
encoding: "base64",
|
||||
mime: "image/png",
|
||||
}
|
||||
configEntries = [
|
||||
const configTest = yield* Config.Test
|
||||
yield* configTest.setEntries([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
attachments: new ConfigAttachments.Info({ image: new ConfigAttachments.Image({ max_width: 4 }) }),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
])
|
||||
const registry = yield* Tool.Service
|
||||
const result = yield* executeTool(registry, {
|
||||
sessionID,
|
||||
|
|
@ -489,7 +490,8 @@ describe("ReadTool", () => {
|
|||
encoding: "base64",
|
||||
mime: "image/png",
|
||||
}
|
||||
configEntries = [
|
||||
const configTest = yield* Config.Test
|
||||
yield* configTest.setEntries([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
|
|
@ -498,7 +500,7 @@ describe("ReadTool", () => {
|
|||
}),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
])
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -704,10 +704,7 @@ const toolLifecycleLayer = (endpoint: string) => {
|
|||
[
|
||||
[
|
||||
Config.node,
|
||||
Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ changes: () => Stream.empty, entries: () => Effect.succeed([]) }),
|
||||
),
|
||||
Config.testLayer(),
|
||||
],
|
||||
],
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue