Ver código fonte

test(opencode): simplify test registry layer wiring (#31761)

James Long 2 meses atrás
pai
commit
eb70b6137b

+ 8 - 1
packages/core/src/effect/layer-node.ts

@@ -44,13 +44,20 @@ type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<Replacemen
   ? unknown
   : { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
 
-export function replace<A, E, E2>(
+export function replaceWithNode<A, E, E2>(
   source: Node<A, E>,
   replacement: Node<NoInfer<A>, E2> & CheckReplacementErrors<E, NoInfer<E2>>,
 ): Replacement<A> {
   return { source, replacement }
 }
 
+export function replace<A, E, E2>(
+  source: Node<A, E>,
+  replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
+): Replacement<A> {
+  return { source, replacement: make(replacement as Layer.Layer<A, E2>, []) }
+}
+
 export function buildLayer<A, E>(node: Node<A, E>, options?: { readonly replacements?: readonly Replacement[] }) {
   const replacements = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
   const cache = new Map<AnyNode, RuntimeLayer>()

+ 9 - 5
packages/opencode/test/effect/app-graph-types.test.ts

@@ -91,14 +91,18 @@ void (0 as unknown as ClosedRequires)
 void (0 as unknown as ClosedError)
 
 const replacement = LayerNode.make(Layer.succeed(A, A.of({ value: "a" })), [])
-LayerNode.replace(a, replacement)
-LayerNode.replace(notFoundOrDiskA, notFoundA)
-LayerNode.replace(notFoundOrDiskA, diskA)
+LayerNode.replace(a, Layer.succeed(A, A.of({ value: "a" })))
+LayerNode.replace(notFoundOrDiskA, notFoundAImplementation)
+LayerNode.replace(notFoundOrDiskA, diskAImplementation)
+LayerNode.replaceWithNode(a, replacement)
 
 // @ts-expect-error An override for A must still provide A
-LayerNode.replace(a, b)
+LayerNode.replaceWithNode(a, b)
 
 // @ts-expect-error A replacement cannot introduce NetworkError
-LayerNode.replace(notFoundOrDiskA, networkA)
+LayerNode.replace(notFoundOrDiskA, networkAImplementation)
+
+// @ts-expect-error A replacement layer must not have unresolved dependencies
+LayerNode.replace(b, bImplementation)
 
 test("type exploration compiles", () => {})

+ 11 - 14
packages/opencode/test/effect/app-graph.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
 import { Cause, Context, Effect, Exit, Layer } from "effect"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 
-const { buildLayer: build, group, replace } = LayerNode
+const { buildLayer: build, group, replace, replaceWithNode } = LayerNode
 const node = LayerNode.make
 
 class Value extends Context.Service<Value, { readonly value: string }>()("test/Value") {}
@@ -30,7 +30,7 @@ describe("app graph", () => {
   })
 
   test("applies overrides before dependency materialization", async () => {
-    const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
+    const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
     const graph = build(greeting, { replacements: [replace(value, replacement)] })
     const result = Effect.gen(function* () {
       return (yield* Greeting).text
@@ -102,7 +102,7 @@ describe("app graph", () => {
       ),
       [value],
     )
-    const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
+    const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
     const graph = build(group([left, right]), { replacements: [replace(value, replacement)] })
 
     const result = Effect.gen(function* () {
@@ -153,7 +153,7 @@ describe("app graph", () => {
     )
     const result = Effect.gen(function* () {
       return (yield* Greeting).text
-    }).pipe(Effect.provide(build(greeting, { replacements: [replace(value, replacement)] })))
+    }).pipe(Effect.provide(build(greeting, { replacements: [replaceWithNode(value, replacement)] })))
 
     expect(await Effect.runPromise(result)).toBe("hello replacement")
   })
@@ -161,15 +161,12 @@ describe("app graph", () => {
   test("does not acquire unreachable replacements", async () => {
     let acquisitions = 0
     const unreachable = node(Layer.succeed(Value, Value.of({ value: "unreachable" })), [])
-    const replacement = node(
-      Layer.effect(
-        Value,
-        Effect.sync(() => {
-          acquisitions++
-          return Value.of({ value: "replacement" })
-        }),
-      ),
-      [],
+    const replacement = Layer.effect(
+      Value,
+      Effect.sync(() => {
+        acquisitions++
+        return Value.of({ value: "replacement" })
+      }),
     )
 
     await Effect.runPromise(
@@ -200,7 +197,7 @@ describe("app graph", () => {
     const consumer = node(greetingImplementation, [value])
     ;(replacement.dependencies as LayerNode.Node<unknown, unknown>[]).push(consumer)
 
-    expect(() => build(consumer, { replacements: [replace(value, replacement)] })).toThrow(
+    expect(() => build(consumer, { replacements: [replaceWithNode(value, replacement)] })).toThrow(
       "Cycle detected in app graph: layer#1 -> layer#2 -> layer#1",
     )
   })

+ 12 - 51
packages/opencode/test/tool/registry.test.ts

@@ -3,31 +3,15 @@ import path from "path"
 import fs from "fs/promises"
 import { fileURLToPath, pathToFileURL } from "url"
 import { Effect, Layer, Result, Schema } from "effect"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
-import { Database } from "@opencode-ai/core/database/database"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ToolRegistry } from "@/tool/registry"
 import { Tool } from "@/tool/tool"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { TestConfig } from "../fixture/config"
-import { FSUtil } from "@opencode-ai/core/fs-util"
+import { Config } from "@/config/config"
 import { Plugin } from "@/plugin"
-import { Question } from "@/question"
-import { Todo } from "@/session/todo"
-import { Skill } from "@/skill"
 import { Agent } from "@/agent/agent"
-import { BackgroundJob } from "@/background/job"
-import { Session } from "@/session/session"
-import { SessionStatus } from "@/session/status"
-import { Provider } from "@/provider/provider"
-import { Git } from "@/git"
-import { LSP } from "@/lsp/lsp"
-import { Instruction } from "@/session/instruction"
-import { EventV2Bridge } from "@/event-v2-bridge"
-import { FetchHttpClient } from "effect/unstable/http"
-import { Format } from "@/format"
-import { Ripgrep } from "@opencode-ai/core/ripgrep"
-import * as Truncate from "@/tool/truncate"
 import { InstanceState } from "@/effect/instance-state"
 
 import { ToolJsonSchema } from "@/tool/json-schema"
@@ -36,41 +20,10 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 
-const node = CrossSpawnSpawner.defaultLayer
 const configLayer = TestConfig.layer({
   directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])),
 })
 
-type RegistryLayerOptions = {
-  flags?: Partial<RuntimeFlags.Info>
-  plugin?: Layer.Layer<Plugin.Service>
-}
-
-const registryLayer = (opts: RegistryLayerOptions = {}) =>
-  ToolRegistry.layer
-    .pipe(
-      Layer.provide(configLayer),
-      Layer.provide(opts.plugin ?? Plugin.defaultLayer),
-      Layer.provide(Question.defaultLayer),
-      Layer.provide(Todo.defaultLayer),
-      Layer.provide(Skill.defaultLayer),
-      Layer.provide(Agent.defaultLayer),
-      Layer.provide(Session.defaultLayer),
-      Layer.provide(Layer.mergeAll(SessionStatus.defaultLayer, BackgroundJob.defaultLayer)),
-      Layer.provide(Provider.defaultLayer),
-      Layer.provide(Git.defaultLayer),
-      Layer.provide(LSP.defaultLayer),
-      Layer.provide(Instruction.defaultLayer),
-      Layer.provide(FSUtil.defaultLayer),
-      Layer.provide(EventV2Bridge.defaultLayer),
-      Layer.provide(FetchHttpClient.layer),
-      Layer.provide(Format.defaultLayer),
-      Layer.provide(Layer.mergeAll(node, Database.defaultLayer)),
-      Layer.provide(Ripgrep.defaultLayer),
-      Layer.provide(Truncate.defaultLayer),
-    )
-    .pipe(Layer.provide(RuntimeFlags.layer(opts.flags ?? {})))
-
 // Fake Plugin.Service that returns a single plugin whose `tool` map contains
 // one definition with `args: undefined`. Used to exercise the plugin entry
 // point of `fromPlugin` for the #27451 / #27630 regression.
@@ -95,9 +48,17 @@ const brokenPluginLayer = Layer.succeed(
   }),
 )
 
-const it = testEffect(Layer.mergeAll(registryLayer(), node, Agent.defaultLayer))
+const root = LayerNode.group([ToolRegistry.node, Agent.node])
+const replacements = [
+  LayerNode.replace(Config.node, configLayer),
+  LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer()),
+]
+
+const it = testEffect(LayerNode.buildLayer(root, { replacements }))
 const withBrokenPlugin = testEffect(
-  Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer),
+  LayerNode.buildLayer(root, {
+    replacements: [...replacements, LayerNode.replace(Plugin.node, brokenPluginLayer)],
+  }),
 )
 
 afterEach(async () => {