refactor(core): refine layer node replacements (#34377)
This commit is contained in:
parent
01a5c69244
commit
84336e4f91
38 changed files with 346 additions and 341 deletions
|
|
@ -1,24 +1,23 @@
|
||||||
import { Layer } from "effect"
|
|
||||||
import { buildLocationServiceMap } from "../location-services"
|
import { buildLocationServiceMap } from "../location-services"
|
||||||
import { LocationServiceMap } from "../location-service-map"
|
import { LocationServiceMap } from "../location-service-map"
|
||||||
import { LayerNode } from "./layer-node"
|
import { LayerNode } from "./layer-node"
|
||||||
import { makeGlobalNode } from "./app-node"
|
import { makeGlobalNode } from "./app-node"
|
||||||
|
|
||||||
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) {
|
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
|
||||||
const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
|
let allReplacements = replacements
|
||||||
|
|
||||||
if (!LayerNode.hasUnbound(root, LocationServiceMap.node)) {
|
// Only build the location service map if it's actually needed
|
||||||
// If the location service map is not needed, we shouldn't pull it
|
if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) {
|
||||||
// in. Compile the graph normally
|
const locationMap = buildLocationServiceMap(replacements)
|
||||||
return LayerNode.compile(root, replacementMap)
|
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||||
|
allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]])
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationMap = buildLocationServiceMap(replacementMap)
|
return LayerNode.compile(root, allReplacements)
|
||||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
}
|
||||||
|
|
||||||
const app = LayerNode.bind(root, LocationServiceMap.node, locationMapNode)
|
function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node<unknown, unknown, any>) {
|
||||||
|
return replacements.some(([source]) => source.name === node.name)
|
||||||
return LayerNode.compile(app, replacementMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export * as AppNodeBuilder from "./app-node-builder"
|
export * as AppNodeBuilder from "./app-node-builder"
|
||||||
|
|
|
||||||
1
packages/core/src/effect/dfdf
Normal file
1
packages/core/src/effect/dfdf
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/
|
||||||
|
|
@ -111,20 +111,47 @@ export function group<const Items extends readonly AnyNode[]>(
|
||||||
return { kind: "group", name: "group", dependencies }
|
return { kind: "group", name: "group", dependencies }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Replacement = {
|
export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any]
|
||||||
readonly source: Layer.Any
|
export type Replacements = readonly Replacement[]
|
||||||
readonly replacement: Layer.Any
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
||||||
? unknown
|
? unknown
|
||||||
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
||||||
|
|
||||||
export function replace<A, E, R, E2>(
|
type CheckReplacement<Item> = Item extends readonly [Node<infer A, infer E, infer T>, infer Replacement]
|
||||||
source: Layer.Layer<A, E, R>,
|
? Replacement extends Node<NoInfer<A>, infer E2, T>
|
||||||
replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
|
? CheckReplacementErrors<E, NoInfer<E2>>
|
||||||
): Replacement {
|
: Replacement extends Layer.Layer<NoInfer<A>, infer E2, never>
|
||||||
return { source, replacement }
|
? CheckReplacementErrors<E, NoInfer<E2>>
|
||||||
|
: { readonly "Invalid replacement": Replacement }
|
||||||
|
: { readonly "Invalid replacement": Item }
|
||||||
|
|
||||||
|
type CheckReplacements<Items extends Replacements> = {
|
||||||
|
readonly [K in keyof Items]: CheckReplacement<Items[K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidReplacements<Items extends Replacements> = Items & CheckReplacements<Items>
|
||||||
|
|
||||||
|
function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) {
|
||||||
|
const replacementNode = isNode(replacement)
|
||||||
|
? replacement
|
||||||
|
: make({ ...nodeMakeIdentity(source), layer: replacement as Layer.Layer<unknown, unknown>, deps: [], tag: source.tag })
|
||||||
|
if (source.name !== replacementNode.name) {
|
||||||
|
throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`)
|
||||||
|
}
|
||||||
|
if (source.tag !== replacementNode.tag) {
|
||||||
|
throw new Error(`Cannot replace ${source.name} across tags`)
|
||||||
|
}
|
||||||
|
return replacementNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeMakeIdentity(node: AnyNode): NodeIdentity {
|
||||||
|
if (node.service !== undefined) return { service: node.service }
|
||||||
|
return { name: node.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNode(input: Layer.Any | AnyNode): input is AnyNode {
|
||||||
|
return "kind" in input && "dependencies" in input
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tree -----------------------------------------------------------------------
|
// Tree -----------------------------------------------------------------------
|
||||||
|
|
@ -176,32 +203,38 @@ function walk<Result>(
|
||||||
return recur(root)
|
return recur(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hoist<A, E, T extends Tag>(
|
export function hoist<A, E, T extends Tag, const Items extends Replacements = readonly []>(
|
||||||
root: Node<A, E, any>,
|
root: Node<A, E, any>,
|
||||||
tag: T,
|
tag: T,
|
||||||
|
replacements?: ValidReplacements<Items>,
|
||||||
): {
|
): {
|
||||||
readonly node: Node<A, E>
|
readonly node: Node<A, E>
|
||||||
readonly hoisted: Node<unknown, E>
|
readonly hoisted: Node<unknown, E>
|
||||||
} {
|
} {
|
||||||
const hoisted = new Map<string, AnyNode>()
|
const hoisted = new Map<string, AnyNode>()
|
||||||
|
const replacementMap = replacementMapFrom(replacements)
|
||||||
|
|
||||||
const node = walk<AnyNode>(root, (node, context) => {
|
const node = walk<AnyNode>(
|
||||||
if (node.kind === "group") {
|
root,
|
||||||
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
(node, context) => {
|
||||||
}
|
if (node.kind === "group") {
|
||||||
if (node.tag === tag) {
|
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
||||||
const existing = hoisted.get(node.name)
|
|
||||||
if (existing && existing !== node) {
|
|
||||||
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
|
||||||
}
|
}
|
||||||
hoisted.set(node.name, node)
|
if (node.tag === tag) {
|
||||||
return group([])
|
const existing = hoisted.get(node.name)
|
||||||
}
|
if (existing && existing !== node) {
|
||||||
if (node.kind === "unbound") {
|
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
||||||
return node
|
}
|
||||||
}
|
hoisted.set(node.name, node)
|
||||||
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
return group([])
|
||||||
})
|
}
|
||||||
|
if (node.kind === "unbound") {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
||||||
|
},
|
||||||
|
{ resolve: (node) => replacementMap.get(node.name) ?? node },
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: node as Node<A, E>,
|
node: node as Node<A, E>,
|
||||||
|
|
@ -209,10 +242,11 @@ export function hoist<A, E, T extends Tag>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compile<A, E>(
|
export function compile<A, E, const Items extends Replacements = readonly []>(
|
||||||
root: Node<A, E, any>,
|
root: Node<A, E, any>,
|
||||||
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
replacements?: ValidReplacements<Items>,
|
||||||
): Layer.Layer<A, E> {
|
): Layer.Layer<A, E> {
|
||||||
|
const replacementMap = replacementMapFrom(replacements)
|
||||||
const cache = new Map<AnyNode, RuntimeLayer>()
|
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||||
const compileNode = (node: AnyNode) =>
|
const compileNode = (node: AnyNode) =>
|
||||||
walk<RuntimeLayer>(
|
walk<RuntimeLayer>(
|
||||||
|
|
@ -220,18 +254,22 @@ export function compile<A, E>(
|
||||||
(node, context) => {
|
(node, context) => {
|
||||||
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
|
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
|
||||||
const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
|
const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
|
||||||
const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
|
const implementation = node.implementation! as RuntimeLayer
|
||||||
return dependencies.length === 0
|
return dependencies.length === 0
|
||||||
? implementation
|
? implementation
|
||||||
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
||||||
},
|
},
|
||||||
{ cache },
|
{ cache, resolve: (node) => replacementMap.get(node.name) ?? node },
|
||||||
)
|
)
|
||||||
const layers = flatten(root).map((node) => compileNode(node))
|
const layers = flatten(root).map((node) => compileNode(node))
|
||||||
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
|
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
|
||||||
return layer as Layer.Layer<A, E>
|
return layer as Layer.Layer<A, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replacementMapFrom(replacements?: Replacements) {
|
||||||
|
return new Map(replacements?.map(([source, replacement]) => [source.name, replacementNode(source, replacement)]))
|
||||||
|
}
|
||||||
|
|
||||||
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
|
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
|
||||||
if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
|
if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
|
||||||
return walk<boolean>(root, (node, context) => {
|
return walk<boolean>(root, (node, context) => {
|
||||||
|
|
@ -240,32 +278,6 @@ export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode):
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bind<A, E, T extends Tag | undefined>(
|
|
||||||
root: Node<A, E, T>,
|
|
||||||
source: AnyNode,
|
|
||||||
replacement: AnyNode,
|
|
||||||
): Node<A, E, T> {
|
|
||||||
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
|
|
||||||
if (source.name !== replacement.name) {
|
|
||||||
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
|
|
||||||
}
|
|
||||||
if (source.tag !== replacement.tag) {
|
|
||||||
throw new Error(`Cannot bind ${source.name} across tags`)
|
|
||||||
}
|
|
||||||
return walk<AnyNode>(
|
|
||||||
root,
|
|
||||||
(target, context) => {
|
|
||||||
if (target.kind === "unbound") return target
|
|
||||||
const dependencies: AnyNode[] = []
|
|
||||||
const clone = { ...target, dependencies }
|
|
||||||
context.cache.set(target, clone)
|
|
||||||
dependencies.push(...target.dependencies.map(context.visit))
|
|
||||||
return clone
|
|
||||||
},
|
|
||||||
{ detectCycles: false, resolve: (node) => (node === source ? replacement : node) },
|
|
||||||
) as Node<A, E, T>
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatten(node: AnyNode): readonly AnyNode[] {
|
function flatten(node: AnyNode): readonly AnyNode[] {
|
||||||
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,17 +82,16 @@ export type LocationServices = LayerNode.Output<typeof locationServices>
|
||||||
export type LocationError = LayerNode.Error<typeof locationServices>
|
export type LocationError = LayerNode.Error<typeof locationServices>
|
||||||
|
|
||||||
export function buildLocationServiceMap(
|
export function buildLocationServiceMap(
|
||||||
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
replacements: LayerNode.Replacements = [],
|
||||||
): Layer.Layer<LocationServiceMap.Service> {
|
): Layer.Layer<LocationServiceMap.Service> {
|
||||||
return Layer.effect(
|
return Layer.effect(
|
||||||
LocationServiceMap.Service,
|
LocationServiceMap.Service,
|
||||||
LayerMap.make(
|
LayerMap.make(
|
||||||
(ref: Location.Ref) => {
|
(ref: Location.Ref) => {
|
||||||
const location = LayerNode.hoist(
|
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||||
LayerNode.bind(locationServices, Location.node, Location.boundNode(ref)),
|
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||||
Node.tags.values.global,
|
|
||||||
)
|
return LayerNode.compile(location.node).pipe(
|
||||||
return LayerNode.compile(location.node, replacements).pipe(
|
|
||||||
Layer.fresh,
|
Layer.fresh,
|
||||||
Layer.tap(() =>
|
Layer.tap(() =>
|
||||||
Effect.logInfo("booting location services", {
|
Effect.logInfo("booting location services", {
|
||||||
|
|
@ -100,7 +99,7 @@ export function buildLocationServiceMap(
|
||||||
workspaceID: ref.workspaceID,
|
workspaceID: ref.workspaceID,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Layer.provide(LayerNode.compile(location.hoisted, replacements)),
|
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ idleTimeToLive: "60 minutes" },
|
{ idleTimeToLive: "60 minutes" },
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,8 @@ export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] })
|
||||||
|
|
||||||
|
export const nodeWithoutConfig = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node] })
|
||||||
|
|
||||||
/** Runs retention scanning once globally rather than once per active Location. */
|
/** Runs retention scanning once globally rather than once per active Location. */
|
||||||
export const cleanupLayer = Layer.effectDiscard(
|
export const cleanupLayer = Layer.effectDiscard(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
|
@ -9,19 +10,14 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
import { executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { Tools } from "@opencode-ai/core/tool/tools"
|
import { Tools } from "@opencode-ai/core/tool/tools"
|
||||||
import { Deferred, Effect, Exit, Fiber, Layer, Schema, Scope } from "effect"
|
import { Deferred, Effect, Exit, Fiber, Schema, Scope } from "effect"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const permission = Layer.mock(PermissionV2.Service, {
|
const it = testEffect(
|
||||||
assert: () => Effect.void,
|
AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, ToolRegistry.node, ToolRegistry.toolsNode]), [
|
||||||
})
|
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||||
const applications = ApplicationTools.layer
|
]),
|
||||||
const registry = ToolRegistry.layer.pipe(
|
|
||||||
Layer.provide(permission),
|
|
||||||
Layer.provide(applications),
|
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
|
||||||
)
|
)
|
||||||
const it = testEffect(Layer.mergeAll(applications, registry))
|
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_application_tool")
|
const sessionID = SessionV2.ID.make("ses_application_tool")
|
||||||
const agent = AgentV2.ID.make("build")
|
const agent = AgentV2.ID.make("build")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { Effect, Fiber, Layer, Stream } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
@ -21,12 +23,12 @@ const locationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||||
)
|
)
|
||||||
|
const catalogLayer = AppNodeBuilder.build(
|
||||||
|
LayerNode.group([Catalog.node, EventV2.node, Credential.node, Integration.node, Policy.node]),
|
||||||
|
[[Location.node, locationLayer]],
|
||||||
|
)
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
catalogLayer,
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
|
||||||
Layer.provideMerge(locationLayer),
|
|
||||||
Layer.provideMerge(Credential.defaultLayer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("CatalogV2", () => {
|
describe("CatalogV2", () => {
|
||||||
|
|
@ -47,11 +49,11 @@ describe("CatalogV2", () => {
|
||||||
|
|
||||||
it.effect("derives availability from active credentials without changing provider state", () => {
|
it.effect("derives availability from active credentials without changing provider state", () => {
|
||||||
const integrationID = Integration.ID.make("test")
|
const integrationID = Integration.ID.make("test")
|
||||||
const layer = Catalog.locationLayer.pipe(
|
const localCatalogLayer = Layer.fresh(
|
||||||
Layer.fresh,
|
AppNodeBuilder.build(
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
LayerNode.group([Catalog.node, Credential.node]),
|
||||||
Layer.provideMerge(locationLayer),
|
[[Location.node, locationLayer]],
|
||||||
Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
|
@ -73,17 +75,17 @@ describe("CatalogV2", () => {
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(localCatalogLayer))
|
||||||
})
|
})
|
||||||
|
|
||||||
it.effect("derives availability from a provider's integration", () => {
|
it.effect("derives availability from a provider's integration", () => {
|
||||||
const integrationID = Integration.ID.make("gateway")
|
const integrationID = Integration.ID.make("gateway")
|
||||||
const providerID = ProviderV2.ID.make("remote")
|
const providerID = ProviderV2.ID.make("remote")
|
||||||
const layer = Catalog.locationLayer.pipe(
|
const localCatalogLayer = Layer.fresh(
|
||||||
Layer.fresh,
|
AppNodeBuilder.build(
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
LayerNode.group([Catalog.node, Credential.node, Integration.node]),
|
||||||
Layer.provideMerge(locationLayer),
|
[[Location.node, locationLayer]],
|
||||||
Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
|
@ -102,7 +104,7 @@ describe("CatalogV2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(localCatalogLayer))
|
||||||
})
|
})
|
||||||
|
|
||||||
it.effect("projects environment connections without a catalog plugin", () =>
|
it.effect("projects environment connections without a catalog plugin", () =>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
|
@ -12,7 +14,7 @@ import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { agentHost, host } from "../plugin/host"
|
import { agentHost, host } from "../plugin/host"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node])))
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
|
||||||
describe("ConfigAgentPlugin.Plugin", () => {
|
describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { CommandV2 } from "@opencode-ai/core/command"
|
import { CommandV2 } from "@opencode-ai/core/command"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
|
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
|
@ -13,7 +15,7 @@ import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { host } from "../plugin/host"
|
import { host } from "../plugin/host"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([CommandV2.node, FSUtil.node])))
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
|
||||||
describe("ConfigCommandPlugin.Plugin", () => {
|
describe("ConfigCommandPlugin.Plugin", () => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import { Effect, Layer, Schema } from "effect"
|
||||||
import { FastCheck } from "effect/testing"
|
import { FastCheck } from "effect/testing"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||||
|
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
||||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
|
|
@ -25,21 +28,24 @@ function testLayer(
|
||||||
projectDirectory = directory,
|
projectDirectory = directory,
|
||||||
vcs?: Project.Vcs,
|
vcs?: Project.Vcs,
|
||||||
) {
|
) {
|
||||||
return Config.locationLayer.pipe(
|
const configNode = makeLocationNode({
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
service: Config.Service,
|
||||||
Layer.provide(Global.layerWith({ config: globalDirectory })),
|
layer: Layer.fresh(Config.layer.pipe(Layer.provide(Global.layerWith({ config: globalDirectory })))),
|
||||||
Layer.provide(
|
deps: [FSUtil.node, Location.node, Policy.node],
|
||||||
Layer.succeed(
|
})
|
||||||
Location.Service,
|
const locationLayer = Layer.succeed(
|
||||||
Location.Service.of(
|
Location.Service,
|
||||||
location(
|
Location.Service.of(
|
||||||
{ directory: AbsolutePath.make(directory) },
|
location(
|
||||||
{ projectDirectory: AbsolutePath.make(projectDirectory), vcs },
|
{ directory: AbsolutePath.make(directory) },
|
||||||
),
|
{ projectDirectory: AbsolutePath.make(projectDirectory), vcs },
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
return AppNodeBuilder.build(
|
||||||
|
LayerNode.group([configNode, Policy.node]),
|
||||||
|
[[Location.node, locationLayer]],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Credential.defaultLayer)
|
const it = testEffect(LayerNode.compile(Credential.node))
|
||||||
|
|
||||||
describe("Credential", () => {
|
describe("Credential", () => {
|
||||||
it.effect("stores, updates, lists, and removes credentials", () =>
|
it.effect("stores, updates, lists, and removes credentials", () =>
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,22 @@ const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
||||||
void checkClosed
|
void checkClosed
|
||||||
void checkError
|
void checkError
|
||||||
|
|
||||||
LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
|
LayerNode.compile(a, [[a, Layer.succeed(A, A.of({}))]])
|
||||||
|
LayerNode.compile(a, [[a, make({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })]])
|
||||||
|
|
||||||
// @ts-expect-error Replacement must provide A
|
// @ts-expect-error Replacement must provide A
|
||||||
LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
|
LayerNode.compile(a, [[a, Layer.succeed(B, B.of({}))]])
|
||||||
|
|
||||||
|
// @ts-expect-error Node replacement must provide A
|
||||||
|
const invalidNodeReplacement = () => LayerNode.compile(a, [[a, b]])
|
||||||
|
void invalidNodeReplacement
|
||||||
|
|
||||||
// @ts-expect-error Replacement cannot introduce a new error
|
// @ts-expect-error Replacement cannot introduce a new error
|
||||||
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
|
LayerNode.compile(a, [[a, Layer.effect(A, Effect.fail(new OtherError()))]])
|
||||||
|
|
||||||
// @ts-expect-error Replacement must be closed
|
// @ts-expect-error Node replacement cannot introduce a new error
|
||||||
LayerNode.replace(bLayer, bLayer)
|
const invalidNodeErrorReplacement = () => LayerNode.compile(a, [[a, make({ service: A, layer: Layer.effect(A, Effect.fail(new OtherError())), deps: [] })]])
|
||||||
|
void invalidNodeErrorReplacement
|
||||||
|
|
||||||
class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
|
class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
|
||||||
class TagB extends Context.Service<TagB, {}>()("test/TagB") {}
|
class TagB extends Context.Service<TagB, {}>()("test/TagB") {}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }
|
||||||
const tags = LayerNode.tags({ app: [] })
|
const tags = LayerNode.tags({ app: [] })
|
||||||
const make = tags.make("app")
|
const make = tags.make("app")
|
||||||
const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
|
const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
|
||||||
LayerNode.compile(root, new Map(replacements?.map((item) => [item.source, item.replacement]))) as Layer.Layer<A, E>
|
LayerNode.compile(root, replacements) as Layer.Layer<A, E>
|
||||||
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
||||||
const greetingLayer = Layer.effect(
|
const greetingLayer = Layer.effect(
|
||||||
Greeting,
|
Greeting,
|
||||||
|
|
@ -63,21 +63,20 @@ describe("layer node", () => {
|
||||||
expect(await Effect.runPromise(program)).toEqual(["first", "second"])
|
expect(await Effect.runPromise(program)).toEqual(["first", "second"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("requires unbound nodes to be bound before compilation", async () => {
|
test("requires unbound nodes to be replaced before compilation", async () => {
|
||||||
const unbound = LayerNode.unbound(Value, tags.values.app)
|
const unbound = LayerNode.unbound(Value, tags.values.app)
|
||||||
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
|
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
|
||||||
const tree = LayerNode.group([greeting])
|
const tree = LayerNode.group([greeting])
|
||||||
expect(() => LayerNode.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
|
expect(() => LayerNode.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
|
||||||
const bound = LayerNode.bind(tree, unbound, value)
|
const layer = LayerNode.compile(tree, [[unbound, value]]) as Layer.Layer<Greeting>
|
||||||
const layer = LayerNode.compile(bound) as Layer.Layer<Greeting>
|
|
||||||
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
|
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
|
||||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("replaces a layer by identity", async () => {
|
test("replaces a node with a closed layer", async () => {
|
||||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(valueLayer, replacement)])),
|
Effect.provide(build(LayerNode.group([greeting]), [[value, replacement]])),
|
||||||
)
|
)
|
||||||
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
||||||
})
|
})
|
||||||
|
|
@ -94,7 +93,7 @@ describe("layer node", () => {
|
||||||
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
||||||
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
||||||
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
||||||
const layer = build(LayerNode.group([left, right]), [LayerNode.replace(valueLayer, replacement)])
|
const layer = build(LayerNode.group([left, right]), [[value, replacement]])
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
return [(yield* Left).value, (yield* Right).value]
|
return [(yield* Left).value, (yield* Right).value]
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
@ -103,22 +102,47 @@ describe("layer node", () => {
|
||||||
|
|
||||||
test("does not acquire an unused replacement", async () => {
|
test("does not acquire an unused replacement", async () => {
|
||||||
let acquisitions = 0
|
let acquisitions = 0
|
||||||
const other = Layer.succeed(Value, Value.of({ value: "other" }))
|
const other = make({ service: Left, layer: Layer.succeed(Left, Left.of({ value: "other" })), deps: [] })
|
||||||
const replacement = Layer.effect(
|
const replacement = Layer.effect(
|
||||||
Value,
|
Left,
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
acquisitions++
|
acquisitions++
|
||||||
return Value.of({ value: "replacement" })
|
return Left.of({ value: "replacement" })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.map(Greeting, (item) => item.value).pipe(
|
Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(other, replacement)])),
|
Effect.provide(build(LayerNode.group([greeting]), [[other, replacement]])),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
expect(acquisitions).toBe(0)
|
expect(acquisitions).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("replaces a node without acquiring its dependencies", async () => {
|
||||||
|
let acquisitions = 0
|
||||||
|
const dependencyLayer = Layer.effect(
|
||||||
|
Value,
|
||||||
|
Effect.sync(() => {
|
||||||
|
acquisitions++
|
||||||
|
return Value.of({ value: "dependency" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const dependency = make({ service: Value, layer: dependencyLayer, deps: [] })
|
||||||
|
const original = make({ service: Greeting, layer: greetingLayer, deps: [dependency] })
|
||||||
|
const replacement = make({
|
||||||
|
service: Greeting,
|
||||||
|
layer: Layer.succeed(Greeting, Greeting.of({ value: "replacement" })),
|
||||||
|
deps: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
|
Effect.provide(build(LayerNode.group([original]), [[original, replacement]])),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await Effect.runPromise(program)).toBe("replacement")
|
||||||
|
expect(acquisitions).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
test("hoists and compiles tagged graphs", async () => {
|
test("hoists and compiles tagged graphs", async () => {
|
||||||
const tags = LayerNode.tags({ location: ["global"], global: [] })
|
const tags = LayerNode.tags({ location: ["global"], global: [] })
|
||||||
const global = tags.make("global")
|
const global = tags.make("global")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Context, Effect, Layer, LayerMap, Option } from "effect"
|
import { Context, Effect, Layer, LayerMap, Option } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { Node } from "@opencode-ai/core/effect/app-node"
|
import { Node } from "@opencode-ai/core/effect/app-node"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||||
import type { LocationError, LocationServices } from "@opencode-ai/core/location-services"
|
import type { LocationError, LocationServices } from "@opencode-ai/core/location-services"
|
||||||
|
|
@ -31,7 +31,7 @@ describe("node build", () => {
|
||||||
expect(await Effect.runPromise(program)).toBe("plain")
|
expect(await Effect.runPromise(program)).toBe("plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects cycles through a bound location service map", () => {
|
test("detects cycles through a replaced location service map", async () => {
|
||||||
const a = Node.makeGlobalNode({
|
const a = Node.makeGlobalNode({
|
||||||
service: CycleA,
|
service: CycleA,
|
||||||
layer: Layer.effect(CycleA, Effect.as(LocationServiceMap.Service, CycleA.of({}))),
|
layer: Layer.effect(CycleA, Effect.as(LocationServiceMap.Service, CycleA.of({}))),
|
||||||
|
|
@ -45,26 +45,28 @@ describe("node build", () => {
|
||||||
),
|
),
|
||||||
deps: [a],
|
deps: [a],
|
||||||
})
|
})
|
||||||
const mapEffect = Effect.gen(function* () {
|
const mapLayer = Layer.effect(
|
||||||
const service = yield* CycleB
|
LocationServiceMap.Service,
|
||||||
return yield* LayerMap.make(
|
Effect.gen(function* () {
|
||||||
(ref: Location.Ref) =>
|
const service = yield* CycleB
|
||||||
Layer.succeed(
|
return yield* LayerMap.make(
|
||||||
Location.Service,
|
(ref: Location.Ref) =>
|
||||||
Location.Service.of({
|
Layer.succeed(
|
||||||
directory: ref.directory,
|
Location.Service,
|
||||||
workspaceID: ref.workspaceID,
|
Location.Service.of({
|
||||||
project: { id: Project.ID.global, directory: service.directory },
|
directory: ref.directory,
|
||||||
}),
|
workspaceID: ref.workspaceID,
|
||||||
),
|
project: { id: Project.ID.global, directory: service.directory },
|
||||||
{ idleTimeToLive: "1 minute" },
|
}),
|
||||||
)
|
),
|
||||||
}) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>
|
{ idleTimeToLive: "1 minute" },
|
||||||
const mapLayer = Layer.effect(LocationServiceMap.Service, mapEffect)
|
)
|
||||||
|
}) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>,
|
||||||
|
)
|
||||||
const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] })
|
const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] })
|
||||||
const graph = LayerNode.bind(LayerNode.group([a]), LocationServiceMap.node, map)
|
expect(() => AppNodeBuilder.build(LayerNode.group([a]), [[LocationServiceMap.node, map]])).toThrow(
|
||||||
|
"Cycle detected in layer tree",
|
||||||
expect(() => AppNodeBuilder.build(graph)).toThrow("Cycle detected in layer tree")
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shares top-level project with location services", async () => {
|
test("shares top-level project with location services", async () => {
|
||||||
|
|
@ -81,10 +83,10 @@ describe("node build", () => {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
|
|
||||||
LayerNode.replace(Project.layer, projectLayer),
|
|
||||||
])
|
|
||||||
const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
|
const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
|
||||||
|
const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
|
||||||
|
[Project.node, projectLayer],
|
||||||
|
])
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
yield* Project.Service
|
yield* Project.Service
|
||||||
const locations = yield* LocationServiceMap.Service
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { describe, test, expect } from "bun:test"
|
import { describe, test, expect } from "bun:test"
|
||||||
import { Effect, Layer, FileSystem } from "effect"
|
import { Effect, FileSystem } from "effect"
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const live = Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer)
|
const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]))
|
||||||
const { effect: it } = testEffect(live)
|
const { effect: it } = testEffect(live)
|
||||||
|
|
||||||
describe("FSUtil", () => {
|
describe("FSUtil", () => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Layer } from "effect"
|
import { Effect, Exit, Layer } from "effect"
|
||||||
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
|
@ -14,18 +13,13 @@ import { it } from "./lib/effect"
|
||||||
const provide = (directory: string) =>
|
const provide = (directory: string) =>
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
LayerNode.compile(
|
LayerNode.compile(
|
||||||
LayerNode.bind(
|
FileSystem.node,
|
||||||
FileSystem.node,
|
[
|
||||||
Location.node,
|
[
|
||||||
makeLocationNode({
|
Location.node,
|
||||||
service: Location.Service,
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
|
||||||
layer: Layer.succeed(
|
],
|
||||||
Location.Service,
|
],
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
|
|
||||||
),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||||
|
|
@ -14,18 +13,13 @@ import { it } from "./lib/effect"
|
||||||
function provide(directory: string) {
|
function provide(directory: string) {
|
||||||
return Effect.provide(
|
return Effect.provide(
|
||||||
LayerNode.compile(
|
LayerNode.compile(
|
||||||
LayerNode.bind(
|
LocationMutation.node,
|
||||||
LocationMutation.node,
|
[
|
||||||
Location.node,
|
[
|
||||||
makeLocationNode({
|
Location.node,
|
||||||
service: Location.Service,
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
|
||||||
layer: Layer.succeed(
|
],
|
||||||
Location.Service,
|
],
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
|
|
||||||
),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
||||||
import { Effect, Layer, Ref } from "effect"
|
import { Effect, Layer, Ref } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises"
|
import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
@ -90,10 +91,10 @@ const buildLayer = (state: Ref.Ref<MockState>) =>
|
||||||
// Layer.fresh is required: ModelsDev.layer is a module-level Layer constant,
|
// Layer.fresh is required: ModelsDev.layer is a module-level Layer constant,
|
||||||
// and Effect.provide uses a process-global MemoMap by default — without fresh,
|
// and Effect.provide uses a process-global MemoMap by default — without fresh,
|
||||||
// every test would reuse the cachedInvalidateWithTTL state from the first run.
|
// every test would reuse the cachedInvalidateWithTTL state from the first run.
|
||||||
Layer.fresh(ModelsDev.layer).pipe(
|
Layer.fresh(
|
||||||
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeMockClient(state))),
|
AppNodeBuilder.build(ModelsDev.node, [
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
[LayerNodePlatform.httpClient, Layer.succeed(HttpClient.HttpClient, makeMockClient(state))],
|
||||||
Layer.provide(EventV2.defaultLayer),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const writeCacheText = (text: string, mtimeMs?: number) =>
|
const writeCacheText = (text: string, mtimeMs?: number) =>
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,26 @@ import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||||
import { Policy } from "@opencode-ai/core/policy"
|
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "../fixture/location"
|
import { location } from "../fixture/location"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { catalogHost, host, integrationHost } from "./host"
|
import { catalogHost, host, integrationHost } from "./host"
|
||||||
|
|
||||||
const events = EventV2.defaultLayer
|
|
||||||
const locationLayer = Layer.succeed(
|
const locationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||||
)
|
)
|
||||||
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
const layer = AppNodeBuilder.build(
|
||||||
const connections = Credential.defaultLayer.pipe(Layer.fresh)
|
LayerNode.group([Catalog.node, Integration.node, EventV2.node]),
|
||||||
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
[[Location.node, locationLayer]],
|
||||||
const catalog = Catalog.layer.pipe(
|
|
||||||
Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)),
|
|
||||||
)
|
)
|
||||||
const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer)
|
|
||||||
const it = testEffect(layer)
|
const it = testEffect(layer)
|
||||||
|
|
||||||
describe("ModelsDevPlugin", () => {
|
describe("ModelsDevPlugin", () => {
|
||||||
|
|
@ -65,7 +61,7 @@ describe("ModelsDevPlugin", () => {
|
||||||
connections: [],
|
connections: [],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
|
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
||||||
(previous) =>
|
(previous) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
Flag.OPENCODE_MODELS_PATH = previous.path
|
Flag.OPENCODE_MODELS_PATH = previous.path
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { Policy } from "@opencode-ai/core/policy"
|
|
||||||
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
|
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
|
@ -14,20 +12,15 @@ import { location } from "../fixture/location"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { catalogHost, host } from "./host"
|
import { catalogHost, host } from "./host"
|
||||||
|
|
||||||
const events = EventV2.defaultLayer
|
|
||||||
const locationLayer = Layer.succeed(
|
const locationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||||
)
|
)
|
||||||
const connections = Credential.defaultLayer.pipe(Layer.fresh)
|
|
||||||
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
|
||||||
const catalog = Catalog.layer.pipe(
|
|
||||||
Layer.provide(
|
|
||||||
Layer.mergeAll(events, locationLayer, Policy.layer.pipe(Layer.provide(locationLayer)), connections, integrations),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer),
|
AppNodeBuilder.build(
|
||||||
|
Catalog.node,
|
||||||
|
[[Location.node, locationLayer]],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("VariantPlugin", () => {
|
describe("VariantPlugin", () => {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import { $ } from "bun"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
import { Effect, Fiber, Stream } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
||||||
import { Git } from "@opencode-ai/core/git"
|
import { Git } from "@opencode-ai/core/git"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
|
@ -16,15 +17,8 @@ import { ProjectDirectories } from "@opencode-ai/core/project/directories"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const copyLayer = ProjectCopy.layer.pipe(
|
|
||||||
Layer.provide(Database.defaultLayer),
|
|
||||||
Layer.provide(ProjectDirectories.defaultLayer),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
|
||||||
Layer.provide(Git.defaultLayer),
|
|
||||||
)
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.mergeAll(copyLayer, Database.defaultLayer, EventV2.defaultLayer, ProjectDirectories.defaultLayer),
|
AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, EventV2.node, ProjectDirectories.node])),
|
||||||
)
|
)
|
||||||
|
|
||||||
function abs(input: string) {
|
function abs(input: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Layer, Scope } from "effect"
|
import { Effect, Exit, Layer, Scope } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
import { Repository } from "@opencode-ai/core/repository"
|
import { Repository } from "@opencode-ai/core/repository"
|
||||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
const cache = Layer.mock(RepositoryCache.Service, {
|
const cache = Layer.mock(RepositoryCache.Service, {
|
||||||
ensure: () => Effect.die("unexpected Git materialization"),
|
ensure: () => Effect.die("unexpected Git materialization"),
|
||||||
})
|
})
|
||||||
|
const referenceLayer = AppNodeBuilder.build(Reference.node, [[RepositoryCache.node, cache]])
|
||||||
|
|
||||||
describe("Reference", () => {
|
describe("Reference", () => {
|
||||||
it.effect("registers normalized sources for the owning scope", () =>
|
it.effect("registers normalized sources for the owning scope", () =>
|
||||||
|
|
@ -32,12 +34,7 @@ describe("Reference", () => {
|
||||||
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
yield* Scope.close(scope, Exit.void)
|
||||||
expect(yield* references.list()).toEqual([])
|
expect(yield* references.list()).toEqual([])
|
||||||
}).pipe(
|
}).pipe(Effect.provide(referenceLayer)),
|
||||||
Effect.provide(Reference.layer),
|
|
||||||
Effect.provide(cache),
|
|
||||||
Effect.provide(EventV2.defaultLayer),
|
|
||||||
Effect.provide(Global.defaultLayer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("derives Git paths without exposing cache operations", () =>
|
it.effect("derives Git paths without exposing cache operations", () =>
|
||||||
|
|
@ -54,13 +51,7 @@ describe("Reference", () => {
|
||||||
source,
|
source,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}).pipe(
|
}).pipe(Effect.scoped, Effect.provide(referenceLayer)),
|
||||||
Effect.scoped,
|
|
||||||
Effect.provide(Reference.layer),
|
|
||||||
Effect.provide(cache),
|
|
||||||
Effect.provide(EventV2.defaultLayer),
|
|
||||||
Effect.provide(Global.defaultLayer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("preserves configured Git descriptions", () =>
|
it.effect("preserves configured Git descriptions", () =>
|
||||||
|
|
@ -82,12 +73,6 @@ describe("Reference", () => {
|
||||||
source,
|
source,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}).pipe(
|
}).pipe(Effect.scoped, Effect.provide(referenceLayer)),
|
||||||
Effect.scoped,
|
|
||||||
Effect.provide(Reference.layer),
|
|
||||||
Effect.provide(cache),
|
|
||||||
Effect.provide(EventV2.defaultLayer),
|
|
||||||
Effect.provide(Global.defaultLayer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { Git } from "@opencode-ai/core/git"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Repository } from "@opencode-ai/core/repository"
|
import { Repository } from "@opencode-ai/core/repository"
|
||||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
|
||||||
import { git, gitRemote } from "./fixture/git"
|
import { git, gitRemote } from "./fixture/git"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
@ -89,15 +88,9 @@ describe("RepositoryCache", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function cacheLayer(root: string) {
|
function cacheLayer(root: string) {
|
||||||
const dependencies = Layer.mergeAll(
|
return AppNodeBuilder.build(RepositoryCache.node, [
|
||||||
Global.layerWith({ state: path.join(root, "state"), repos: path.join(root, "repos") }),
|
[Global.node, Global.layerWith({ state: path.join(root, "state"), repos: path.join(root, "repos") })],
|
||||||
FSUtil.defaultLayer,
|
])
|
||||||
)
|
|
||||||
return RepositoryCache.layer.pipe(
|
|
||||||
Layer.provide(EffectFlock.layer.pipe(Layer.provide(dependencies))),
|
|
||||||
Layer.provide(Git.defaultLayer),
|
|
||||||
Layer.provide(dependencies),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function withRemote<A, E, R>(body: (fixture: Awaited<ReturnType<typeof gitRemote>>) => Effect.Effect<A, E, R>) {
|
function withRemote<A, E, R>(body: (fixture: Awaited<ReturnType<typeof gitRemote>>) => Effect.Effect<A, E, R>) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { asc, eq } from "drizzle-orm"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
@ -26,11 +25,8 @@ import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||||
|
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node])))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node])))
|
||||||
const sessionsLayer = AppNodeBuilder.build(
|
const sessionsLayer = AppNodeBuilder.build(
|
||||||
LayerNode.bind(
|
SessionV2.node,
|
||||||
SessionV2.node,
|
[[SessionExecution.node, SessionExecution.noopLayer]],
|
||||||
SessionExecution.node,
|
|
||||||
makeGlobalNode({ service: SessionExecution.Service, layer: SessionExecution.noopLayer, deps: [] }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const sessionID = SessionV2.ID.make("ses_projector_test")
|
const sessionID = SessionV2.ID.make("ses_projector_test")
|
||||||
const created = DateTime.makeUnsafe(0)
|
const created = DateTime.makeUnsafe(0)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { asc, eq } from "drizzle-orm"
|
import { asc, eq } from "drizzle-orm"
|
||||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
import { DateTime, Effect, Schema } from "effect"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
@ -16,7 +17,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionProjector.defaultLayer))
|
const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, EventV2.node, SessionProjector.node])))
|
||||||
const timestamp = DateTime.makeUnsafe(1)
|
const timestamp = DateTime.makeUnsafe(1)
|
||||||
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
|
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import path from "path"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
@ -27,15 +29,14 @@ async function pull(skills: unknown[], files: Record<string, string> = {}, cache
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const layer = SkillDiscovery.layer.pipe(
|
const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [
|
||||||
Layer.provide(http),
|
[LayerNodePlatform.httpClient, http],
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
[Global.node, Global.layerWith({ cache: tmp.path })],
|
||||||
Layer.provide(Global.layerWith({ cache: tmp.path })),
|
])
|
||||||
)
|
|
||||||
const directories = await Effect.runPromise(
|
const directories = await Effect.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
return yield* (yield* SkillDiscovery.Service).pull(base)
|
return yield* (yield* SkillDiscovery.Service).pull(base)
|
||||||
}).pipe(Effect.provide(layer)),
|
}).pipe(Effect.provide(skillDiscoveryLayer)),
|
||||||
)
|
)
|
||||||
return { tmp, requests, directories }
|
return { tmp, requests, directories }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,7 @@ const discovery = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node]), [
|
AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node]), [[SkillDiscovery.node, discovery]]),
|
||||||
LayerNode.replace(SkillDiscovery.layer, discovery),
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function write(directory: string, name: string, description: string) {
|
function write(directory: string, name: string, description: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import * as TestClock from "effect/testing/TestClock"
|
import * as TestClock from "effect/testing/TestClock"
|
||||||
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
|
@ -28,14 +27,12 @@ const locationLayer = Layer.succeed(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const locationNode = makeLocationNode({ service: Location.Service, layer: locationLayer, deps: [] })
|
const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, SystemContextRegistry.node])
|
||||||
const builtInsNode = LayerNode.bind(
|
|
||||||
LayerNode.group([SystemContextBuiltIns.node, SystemContextRegistry.node]),
|
|
||||||
Location.node,
|
|
||||||
locationNode,
|
|
||||||
)
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(builtInsNode, [LayerNode.replace(Global.layer, Global.layerWith({ config: "/global" }))]),
|
AppNodeBuilder.build(builtInsNode, [
|
||||||
|
[Location.node, locationLayer],
|
||||||
|
[Global.node, Global.layerWith({ config: "/global" })],
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
const instructionFS = Layer.effect(
|
const instructionFS = Layer.effect(
|
||||||
FSUtil.Service,
|
FSUtil.Service,
|
||||||
|
|
@ -51,8 +48,9 @@ const instructionFS = Layer.effect(
|
||||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||||
const itWithInstructions = testEffect(
|
const itWithInstructions = testEffect(
|
||||||
AppNodeBuilder.build(builtInsNode, [
|
AppNodeBuilder.build(builtInsNode, [
|
||||||
LayerNode.replace(FSUtil.layer, instructionFS),
|
[Location.node, locationLayer],
|
||||||
LayerNode.replace(Global.layer, Global.layerWith({ config: "/global" })),
|
[FSUtil.node, instructionFS],
|
||||||
|
[Global.node, Global.layerWith({ config: "/global" })],
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Cause, Effect, Exit, Fiber, Layer, Option } from "effect"
|
import { Cause, Effect, Exit, Fiber, Layer, Option } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
|
@ -28,14 +30,14 @@ const withStore = <A, E, R>(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: Layer.empty
|
: Layer.empty
|
||||||
const store = ToolOutputStore.layer.pipe(
|
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
const store = AppNodeBuilder.build(LayerNode.group([ToolOutputStore.node, FSUtil.node]), [
|
||||||
Layer.provide(global),
|
[Global.node, global],
|
||||||
Layer.provide(configured),
|
[Config.node, configured],
|
||||||
)
|
])
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service })
|
return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service })
|
||||||
}).pipe(Effect.provide(Layer.mergeAll(store, FSUtil.defaultLayer)))
|
}).pipe(Effect.provide(store))
|
||||||
},
|
},
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
)
|
)
|
||||||
|
|
@ -185,11 +187,11 @@ describe("ToolOutputStore", () => {
|
||||||
writeFileString: () => Effect.never,
|
writeFileString: () => Effect.never,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(FSUtil.defaultLayer))
|
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||||
const store = ToolOutputStore.layer.pipe(
|
const store = AppNodeBuilder.build(ToolOutputStore.nodeWithoutConfig, [
|
||||||
Layer.provide(blockedFilesystem),
|
[Global.node, Global.layerWith({ data: root.path })],
|
||||||
Layer.provide(Global.layerWith({ data: root.path })),
|
[FSUtil.node, blockedFilesystem],
|
||||||
)
|
])
|
||||||
const exit = yield* Effect.gen(function* () {
|
const exit = yield* Effect.gen(function* () {
|
||||||
const service = yield* ToolOutputStore.Service
|
const service = yield* ToolOutputStore.Service
|
||||||
const fiber = yield* service
|
const fiber = yield* service
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Fiber, Layer } from "effect"
|
import { Effect, Exit, Fiber, Layer } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { QuestionTool } from "@opencode-ai/core/tool/question"
|
import { QuestionTool } from "@opencode-ai/core/tool/question"
|
||||||
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||||
|
|
||||||
|
|
@ -28,7 +31,10 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
const registry = AppNodeBuilder.build(
|
||||||
|
LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode]),
|
||||||
|
[[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig]],
|
||||||
|
)
|
||||||
const question = Layer.succeed(
|
const question = Layer.succeed(
|
||||||
QuestionV2.Service,
|
QuestionV2.Service,
|
||||||
QuestionV2.Service.of({
|
QuestionV2.Service.of({
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, FileSystem, Layer } from "effect"
|
import { Effect, FileSystem } from "effect"
|
||||||
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
|
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer))
|
const it = testEffect(LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])))
|
||||||
const fixture = Effect.gen(function* () {
|
const fixture = Effect.gen(function* () {
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const files = yield* FileSystem.FileSystem
|
const files = yield* FileSystem.FileSystem
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
|
@ -9,6 +11,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||||
import { SkillTool } from "@opencode-ai/core/tool/skill"
|
import { SkillTool } from "@opencode-ai/core/tool/skill"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||||
|
|
@ -63,11 +66,14 @@ describe("SkillTool", () => {
|
||||||
list: () => Effect.succeed(current),
|
list: () => Effect.succeed(current),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
const registry = AppNodeBuilder.build(
|
||||||
|
LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode]),
|
||||||
|
[[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig]],
|
||||||
|
)
|
||||||
const tool = SkillTool.layer.pipe(
|
const tool = SkillTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(permission),
|
Layer.provide(permission),
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
Layer.provide(LayerNode.compile(FSUtil.node)),
|
||||||
Layer.provide(skills),
|
Layer.provide(skills),
|
||||||
)
|
)
|
||||||
const layer = Layer.mergeAll(permission, skills, registry, tool)
|
const layer = Layer.mergeAll(permission, skills, registry, tool)
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import { spawn } from "child_process"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { Cause, Effect, Exit, Layer } from "effect"
|
import { Cause, Effect, Exit } from "effect"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Hash } from "@opencode-ai/core/util/hash"
|
import { Hash } from "@opencode-ai/core/util/hash"
|
||||||
|
|
@ -109,7 +110,7 @@ const testGlobal = Global.layerWith({
|
||||||
log: os.tmpdir(),
|
log: os.tmpdir(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(FSUtil.defaultLayer))
|
const testLayer = AppNodeBuilder.build(EffectFlock.node, [[Global.node, testGlobal]])
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
|
|
|
||||||
|
|
@ -176,12 +176,12 @@ const root = LayerNode.group([
|
||||||
CrossSpawnSpawner.node,
|
CrossSpawnSpawner.node,
|
||||||
])
|
])
|
||||||
const replacements = [
|
const replacements = [
|
||||||
LayerNode.replace(SessionSummary.layer, summary),
|
[SessionSummary.node, summary],
|
||||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
||||||
]
|
] as const
|
||||||
const env = LayerNode.compile(
|
const env = LayerNode.compile(
|
||||||
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
|
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
|
||||||
new Map(replacements.map((item) => [item.source, item.replacement])),
|
replacements,
|
||||||
)
|
)
|
||||||
|
|
||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
@ -208,9 +208,7 @@ const providerErrorLLM = Layer.succeed(
|
||||||
)
|
)
|
||||||
const providerErrorEnv = LayerNode.compile(
|
const providerErrorEnv = LayerNode.compile(
|
||||||
root,
|
root,
|
||||||
new Map(
|
[...replacements, [LLM.node, providerErrorLLM]],
|
||||||
[...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)].map((item) => [item.source, item.replacement]),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const itProviderError = testEffect(providerErrorEnv)
|
const itProviderError = testEffect(providerErrorEnv)
|
||||||
|
|
||||||
|
|
@ -230,9 +228,7 @@ const fragmentFailureLLM = Layer.succeed(
|
||||||
)
|
)
|
||||||
const fragmentFailureEnv = LayerNode.compile(
|
const fragmentFailureEnv = LayerNode.compile(
|
||||||
root,
|
root,
|
||||||
new Map(
|
[...replacements, [LLM.node, fragmentFailureLLM]],
|
||||||
[...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)].map((item) => [item.source, item.replacement]),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,11 @@ const root = LayerNode.group([
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
LayerNode.compile(
|
LayerNode.compile(
|
||||||
root,
|
root,
|
||||||
new Map(
|
[
|
||||||
[
|
[MCP.node, mcp],
|
||||||
LayerNode.replace(MCP.layer, mcp),
|
[LSP.node, lsp],
|
||||||
LayerNode.replace(LSP.layer, lsp),
|
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
||||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
],
|
||||||
].map((item) => [item.source, item.replacement]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { beforeEach, describe, expect } from "bun:test"
|
import { beforeEach, describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Layer, Option } from "effect"
|
import { Effect, Exit, Layer, Option } from "effect"
|
||||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
|
|
@ -34,15 +34,15 @@ const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unkno
|
||||||
const none = HttpClient.make(() => Effect.die("unexpected http call"))
|
const none = HttpClient.make(() => Effect.die("unexpected http call"))
|
||||||
|
|
||||||
function requestLayer(client: HttpClient.HttpClient) {
|
function requestLayer(client: HttpClient.HttpClient) {
|
||||||
const replacement = LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))
|
const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
|
||||||
return LayerNode.compile(
|
return LayerNode.compile(
|
||||||
LayerNode.group([ShareNext.node, AccountRepo.node]),
|
LayerNode.group([ShareNext.node, AccountRepo.node]),
|
||||||
new Map([[replacement.source, replacement.replacement]]),
|
[replacement],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function integrationLayer(client: HttpClient.HttpClient) {
|
function integrationLayer(client: HttpClient.HttpClient) {
|
||||||
const replacement = LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))
|
const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
|
||||||
return LayerNode.compile(
|
return LayerNode.compile(
|
||||||
LayerNode.group([
|
LayerNode.group([
|
||||||
ShareNext.node,
|
ShareNext.node,
|
||||||
|
|
@ -52,7 +52,7 @@ function integrationLayer(client: HttpClient.HttpClient) {
|
||||||
AccountRepo.node,
|
AccountRepo.node,
|
||||||
Database.node,
|
Database.node,
|
||||||
]),
|
]),
|
||||||
new Map([[replacement.source, replacement.replacement]]),
|
[replacement],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,20 +50,15 @@ const brokenPluginLayer = Layer.succeed(
|
||||||
|
|
||||||
const root = LayerNode.group([ToolRegistry.node, Agent.node])
|
const root = LayerNode.group([ToolRegistry.node, Agent.node])
|
||||||
const replacements = [
|
const replacements = [
|
||||||
LayerNode.replace(Config.layer, configLayer),
|
[Config.node, configLayer],
|
||||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer()),
|
[RuntimeFlags.node, RuntimeFlags.layer()],
|
||||||
]
|
] as const
|
||||||
|
|
||||||
const it = testEffect(LayerNode.compile(root, new Map(replacements.map((item) => [item.source, item.replacement]))))
|
const it = testEffect(LayerNode.compile(root, replacements))
|
||||||
const withBrokenPlugin = testEffect(
|
const withBrokenPlugin = testEffect(
|
||||||
LayerNode.compile(
|
LayerNode.compile(
|
||||||
root,
|
root,
|
||||||
new Map(
|
[...replacements, [Plugin.node, brokenPluginLayer]],
|
||||||
[...replacements, LayerNode.replace(Plugin.layer, brokenPluginLayer)].map((item) => [
|
|
||||||
item.source,
|
|
||||||
item.replacement,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ export function createEmbeddedRoutes() {
|
||||||
|
|
||||||
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
|
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
|
||||||
const serviceLayer = AppNodeBuilder.build(
|
const serviceLayer = AppNodeBuilder.build(
|
||||||
LayerNode.bind(applicationServices, SessionExecution.node, SessionExecutionLocal.node),
|
applicationServices,
|
||||||
|
[[SessionExecution.node, SessionExecutionLocal.node]],
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue