+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
262 lines
10 KiB
TypeScript
262 lines
10 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Context, Effect, Layer } from "effect"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
|
|
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
|
|
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
|
|
class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
|
|
class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
|
|
class Database extends Context.Service<Database, { readonly name: string }>()("test/GraphDatabase") {}
|
|
class Users extends Context.Service<Users, { readonly list: Effect.Effect<string[]> }>()("test/GraphUsers") {}
|
|
class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }>()("test/GraphApp") {}
|
|
|
|
const tags = LayerNode.tags({ app: [] })
|
|
const make = tags.make("app")
|
|
const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
|
|
LayerNode.compile(root, replacements) as Layer.Layer<A, E>
|
|
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
|
const greetingLayer = Layer.effect(
|
|
Greeting,
|
|
Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
|
|
)
|
|
const value = make({ service: Value, layer: valueLayer, deps: [] })
|
|
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
|
|
|
describe("layer node", () => {
|
|
test("builds an untagged graph", async () => {
|
|
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
|
|
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
|
Effect.provide(LayerNode.compile(LayerNode.group([greeting]))),
|
|
)
|
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
|
})
|
|
|
|
test("builds a dependency graph", async () => {
|
|
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(build(LayerNode.group([greeting]))))
|
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
|
})
|
|
|
|
test("exposes roots but hides transitive dependencies", () => {
|
|
const layer = build(LayerNode.group([greeting]))
|
|
const check: Layer.Layer<Greeting> = layer
|
|
void check
|
|
})
|
|
|
|
test("preserves branch-specific implementations across roots", async () => {
|
|
const firstValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
|
|
const secondValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
|
const leftLayer = Layer.effect(
|
|
Left,
|
|
Effect.map(Value, (item) => Left.of({ value: item.value })),
|
|
)
|
|
const rightLayer = Layer.effect(
|
|
Right,
|
|
Effect.map(Value, (item) => Right.of({ value: item.value })),
|
|
)
|
|
const left = make({ service: Left, layer: leftLayer, deps: [firstValue] })
|
|
const right = make({ service: Right, layer: rightLayer, deps: [secondValue] })
|
|
const layer = build(LayerNode.group([left, right]))
|
|
const program = Effect.gen(function* () {
|
|
return [(yield* Left).value, (yield* Right).value]
|
|
}).pipe(Effect.provide(layer))
|
|
expect(await Effect.runPromise(program)).toEqual(["first", "second"])
|
|
})
|
|
|
|
test("requires unbound nodes to be replaced before compilation", async () => {
|
|
const unbound = LayerNode.unbound(Value, tags.values.app)
|
|
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
|
|
const tree = LayerNode.group([greeting])
|
|
expect(() => LayerNode.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
|
|
const layer = LayerNode.compile(tree, [[unbound, value]]) as Layer.Layer<Greeting>
|
|
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
|
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
|
})
|
|
|
|
test("replaces a node with a closed layer", async () => {
|
|
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
|
Effect.provide(build(LayerNode.group([greeting]), [[value, replacement]])),
|
|
)
|
|
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
|
})
|
|
|
|
test("replaces every use of the same layer", async () => {
|
|
const leftLayer = Layer.effect(
|
|
Left,
|
|
Effect.map(Value, (item) => Left.of({ value: item.value })),
|
|
)
|
|
const rightLayer = Layer.effect(
|
|
Right,
|
|
Effect.map(Value, (item) => Right.of({ value: item.value })),
|
|
)
|
|
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
|
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
|
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
|
const layer = build(LayerNode.group([left, right]), [[value, replacement]])
|
|
const program = Effect.gen(function* () {
|
|
return [(yield* Left).value, (yield* Right).value]
|
|
}).pipe(Effect.provide(layer))
|
|
expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
|
|
})
|
|
|
|
test("does not acquire an unused replacement", async () => {
|
|
let acquisitions = 0
|
|
const other = make({ service: Left, layer: Layer.succeed(Left, Left.of({ value: "other" })), deps: [] })
|
|
const replacement = Layer.effect(
|
|
Left,
|
|
Effect.sync(() => {
|
|
acquisitions++
|
|
return Left.of({ value: "replacement" })
|
|
}),
|
|
)
|
|
await Effect.runPromise(
|
|
Effect.map(Greeting, (item) => item.value).pipe(
|
|
Effect.provide(build(LayerNode.group([greeting]), [[other, replacement]])),
|
|
),
|
|
)
|
|
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("applies later replacements inside earlier replacement nodes", async () => {
|
|
const original = make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
|
const replacement = make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
|
Effect.provide(
|
|
build(LayerNode.group([original]), [
|
|
[original, replacement],
|
|
[value, Layer.succeed(Value, Value.of({ value: "replacement dependency" }))],
|
|
]),
|
|
),
|
|
)
|
|
|
|
expect(await Effect.runPromise(program)).toBe("hello replacement dependency")
|
|
})
|
|
|
|
test("hoists and compiles tagged graphs", async () => {
|
|
const tags = LayerNode.tags({ location: ["global"], global: [] })
|
|
const global = tags.make("global")
|
|
const location = tags.make("location")
|
|
const database = global({
|
|
service: Database,
|
|
layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
|
|
deps: [],
|
|
})
|
|
const users = location({
|
|
service: Users,
|
|
layer: Layer.effect(
|
|
Users,
|
|
Effect.gen(function* () {
|
|
const db = yield* Database
|
|
return Users.of({ list: Effect.succeed([db.name]) })
|
|
}),
|
|
),
|
|
deps: [database],
|
|
})
|
|
const app = location({
|
|
service: App,
|
|
layer: Layer.effect(
|
|
App,
|
|
Effect.gen(function* () {
|
|
const service = yield* Users
|
|
return App.of({ run: service.list })
|
|
}),
|
|
),
|
|
deps: [users],
|
|
})
|
|
|
|
const result = LayerNode.hoist(LayerNode.group([app]), tags.values.global)
|
|
expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
|
|
kind: "group",
|
|
dependencies: [],
|
|
})
|
|
expect(result.hoisted.dependencies).toEqual([database])
|
|
|
|
const layer = LayerNode.compile(result.node).pipe(
|
|
Layer.provide(LayerNode.compile(result.hoisted)),
|
|
) as unknown as Layer.Layer<App>
|
|
const program = Effect.gen(function* () {
|
|
return yield* (yield* App).run
|
|
}).pipe(Effect.provide(layer))
|
|
|
|
expect(await Effect.runPromise(program)).toEqual(["Alice"])
|
|
})
|
|
|
|
test("rejects conflicting hoisted implementations", () => {
|
|
const tags = LayerNode.tags({ location: ["global"], global: [] })
|
|
const global = tags.make("global")
|
|
const location = tags.make("location")
|
|
const first = global({
|
|
service: Database,
|
|
layer: Layer.succeed(Database, Database.of({ name: "first" })),
|
|
deps: [],
|
|
})
|
|
const second = global({
|
|
service: Database,
|
|
layer: Layer.succeed(Database, Database.of({ name: "second" })),
|
|
deps: [],
|
|
})
|
|
const left = location({
|
|
service: Users,
|
|
layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
|
|
deps: [first],
|
|
})
|
|
const right = location({
|
|
service: App,
|
|
layer: Layer.effect(App, Effect.as(Database, App.of({ run: Effect.succeed([]) }))),
|
|
deps: [second],
|
|
})
|
|
|
|
expect(() => LayerNode.hoist(LayerNode.group([left, right]), tags.values.global)).toThrow(
|
|
"Tag global has conflicting implementations for test/GraphDatabase",
|
|
)
|
|
})
|
|
|
|
test("treats dependency groups as transparent while hoisting", () => {
|
|
const tags = LayerNode.tags({ location: ["global"], global: [] })
|
|
const global = tags.make("global")
|
|
const location = tags.make("location")
|
|
const database = global({
|
|
service: Database,
|
|
layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
|
|
deps: [],
|
|
})
|
|
const users = location({
|
|
service: Users,
|
|
layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
|
|
deps: [LayerNode.group([database])],
|
|
})
|
|
const result = LayerNode.hoist(LayerNode.group([users]), tags.values.global)
|
|
|
|
expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
|
|
kind: "group",
|
|
dependencies: [],
|
|
})
|
|
})
|
|
})
|