diff --git a/packages/opencode/test/plugin/workspace-adapter.test.ts b/packages/opencode/test/plugin/workspace-adapter.test.ts index 30428980bb..77eca1411a 100644 --- a/packages/opencode/test/plugin/workspace-adapter.test.ts +++ b/packages/opencode/test/plugin/workspace-adapter.test.ts @@ -1,63 +1,35 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { FetchHttpClient } from "effect/unstable/http" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Database } from "@opencode-ai/core/database/database" -import { FSUtil } from "@opencode-ai/core/fs-util" +import { Npm } from "@opencode-ai/core/npm" import { Ripgrep } from "@opencode-ai/core/ripgrep" -import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import path from "path" import { pathToFileURL } from "url" import { Auth } from "../../src/auth" -import { EventV2Bridge } from "../../src/event-v2-bridge" -import { Config } from "../../src/config/config" -import { Env } from "../../src/env" +import { Account } from "../../src/account/account" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Workspace } from "../../src/control-plane/workspace" import { Plugin } from "../../src/plugin/index" -import { InstanceBootstrap } from "../../src/project/bootstrap-service" +import { InstanceBootstrap } from "../../src/project/bootstrap" import { InstanceStore } from "../../src/project/instance-store" -import { Project } from "../../src/project/project" -import { Vcs } from "../../src/project/vcs" import { InstanceState } from "../../src/effect/instance-state" -import { Session } from "../../src/session/session" -import { SessionPrompt } from "../../src/session/prompt" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" -const configLayer = Config.layer.pipe( - Layer.provide(EffectFlock.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(AuthTest.empty), - Layer.provide(AccountTest.empty), - Layer.provide(NpmTest.noop), - Layer.provide(FetchHttpClient.layer), -) -const pluginLayer = Plugin.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(configLayer), - Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), -) const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) -const workspaceLayer = Workspace.layer.pipe( - Layer.provide(Auth.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(SessionPrompt.defaultLayer), - Layer.provide(Project.defaultLayer), - Layer.provide(Vcs.defaultLayer), - Layer.provide(FetchHttpClient.layer), - Layer.provide(Database.defaultLayer), - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))), - Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })), -) + const it = testEffect( - Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)), + AppNodeBuilder.build(LayerNode.group([Plugin.node, Workspace.node, InstanceStore.node, Ripgrep.node]), [ + [Auth.node, AuthTest.empty], + [Account.node, AccountTest.empty], + [Npm.node, NpmTest.noop], + [InstanceBootstrap.node, noopBootstrapLayer], + [RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true, experimentalWorkspaces: true })], + ]), ) afterEach(async () => { diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 05e205cd87..804b92b08e 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -1,5 +1,4 @@ import { describe, expect } from "bun:test" -import { EventV2Bridge } from "@/event-v2-bridge" import { Project } from "@/project/project" import { $ } from "bun" import path from "path" @@ -15,19 +14,17 @@ import { SessionID } from "@/session/schema" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { Cause, Effect, Exit, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { NodePath } from "@effect/platform-node" -import { FSUtil } from "@opencode-ai/core/fs-util" -import { AppProcess } from "@opencode-ai/core/process" import { ProjectV2 } from "@opencode-ai/core/project" -import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" import { RuntimeFlags } from "@/effect/runtime-flags" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" const encoder = new TextEncoder() -const layer = Layer.mergeAll(Project.defaultLayer, Database.defaultLayer, CrossSpawnSpawner.defaultLayer) -const it = testEffect(layer) +const projectTestNode = LayerNode.group([Project.node, Database.node, CrossSpawnSpawner.node]) +const it = testEffect(AppNodeBuilder.build(projectTestNode)) function remoteProjectID(remote: string) { return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`)) @@ -65,41 +62,37 @@ function mockGitFailure(failArg: string) { }), ) }), - ).pipe(Layer.provide(CrossSpawnSpawner.defaultLayer)) + ).pipe(Layer.provide(AppNodeBuilder.build(CrossSpawnSpawner.node))) } function projectLayerWithFailure(failArg: string) { - return Project.layer.pipe( - Layer.provide(AppProcess.layer.pipe(Layer.provide(mockGitFailure(failArg)))), - Layer.provide(mockGitFailure(failArg)), - Layer.provide(ProjectV2.defaultLayer), - Layer.provide(ProjectDirectories.defaultLayer), - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(NodePath.layer), - Layer.provide(Database.defaultLayer), - Layer.provide(RuntimeFlags.defaultLayer), - ) + return AppNodeBuilder.build(Project.node, [ + [ProjectV2.node, projectV2FailureLayer()], + [CrossSpawnSpawner.node, mockGitFailure(failArg)], + ]) } -function projectLayerWithRuntimeFlags(flags: Parameters[0]) { - return Project.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(ProjectV2.defaultLayer), - Layer.provide(ProjectDirectories.defaultLayer), - Layer.provide(AppProcess.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(NodePath.layer), - Layer.provide(Database.defaultLayer), - Layer.provide(RuntimeFlags.layer(flags)), +function projectV2FailureLayer() { + return Layer.succeed( + ProjectV2.Service, + ProjectV2.Service.of({ + directories: () => Effect.succeed([]), + resolve: (input) => + Effect.succeed({ + id: ProjectV2.ID.global, + directory: input, + vcs: { type: "git" as const, store: input }, + }), + commit: () => Effect.void, + }), ) } const failureIt = (failArg: string) => - testEffect(Layer.mergeAll(projectLayerWithFailure(failArg), CrossSpawnSpawner.defaultLayer)) + testEffect(AppNodeBuilder.build(projectTestNode, [[Project.node, projectLayerWithFailure(failArg)]])) const iconDiscoveryIt = testEffect( - Layer.provideMerge(projectLayerWithRuntimeFlags({ experimentalIconDiscovery: true }), CrossSpawnSpawner.defaultLayer), + AppNodeBuilder.build(projectTestNode, [[RuntimeFlags.node, RuntimeFlags.layer({ experimentalIconDiscovery: true })]]), ) function waitForProjectIcon(id: ProjectV2.ID, attempts = 50): Effect.Effect { diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 641daeead4..c02ea5911d 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -7,12 +7,9 @@ import { APICallError } from "ai" import { Cause, Deferred, Effect, Exit, Fiber, Layer, Schema } from "effect" import * as Stream from "effect/Stream" import { Config } from "@/config/config" -import { Image } from "@/image/image" -import { Agent } from "../../src/agent/agent" import { LLM } from "../../src/session/llm" import { SessionCompaction } from "../../src/session/compaction" import { Token } from "@/util/token" -import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { provideTmpdirInstance, TestInstance } from "../fixture/fixture" import { Session as SessionNs } from "@/session/session" @@ -21,12 +18,11 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { SessionV2 } from "@opencode-ai/core/session" -import { locationServiceMapLayer } from "@opencode-ai/core/location-services" import { SessionExecution } from "@opencode-ai/core/session/execution" +import { SessionProjector } from "@opencode-ai/core/session/projector" -import type { Provider } from "@/provider/provider" +import { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" -import { Snapshot } from "../../src/snapshot" import { ProviderTest } from "../fake/provider" import { testEffect } from "../lib/effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -35,6 +31,8 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { LLMEvent, Usage } from "@opencode-ai/llm" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" const summary = Layer.succeed( SessionSummary.Service, @@ -210,7 +208,7 @@ function fake( } satisfies SessionProcessorModule.SessionProcessor.Handle } -function layer(result: "continue" | "compact") { +function processorLayer(result: "continue" | "compact") { return Layer.succeed( SessionProcessorModule.SessionProcessor.Service, SessionProcessorModule.SessionProcessor.Service.of({ @@ -221,38 +219,28 @@ function layer(result: "continue" | "compact") { function cfg(compaction?: ConfigV1.Info["compaction"]) { const base = Schema.decodeUnknownSync(ConfigV1.Info)({}) as ConfigV1.Info - return TestConfig.layer({ - get: () => Effect.succeed({ ...base, compaction }), - }) + return Layer.succeed(Config.Service, TestConfig.make({ get: () => Effect.succeed({ ...base, compaction }) })) } -const deps = Layer.mergeAll( - wide().layer, - layer("continue"), - Agent.defaultLayer, - Plugin.defaultLayer, - EventV2Bridge.defaultLayer, - Config.defaultLayer, - RuntimeFlags.layer({ experimentalEventSystem: true }), - Database.defaultLayer, - EventV2Bridge.defaultLayer, -) - -const env = Layer.mergeAll( - SessionNs.defaultLayer, - Database.defaultLayer, - EventV2Bridge.defaultLayer, - CrossSpawnSpawner.defaultLayer, - SessionCompaction.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provideMerge(deps)), -) +const defaultProvider = wide() +const compactionTestNode = LayerNode.group([ + SessionCompaction.node, + SessionNs.node, + SessionProjector.node, + Database.node, + EventV2Bridge.node, + CrossSpawnSpawner.node, +]) +const env = AppNodeBuilder.build(compactionTestNode, [ + [Provider.node, defaultProvider.layer], + [SessionProcessorModule.SessionProcessor.node, processorLayer("continue")], + [RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })], +]) const it = testEffect(env) -const compactionEnv = Layer.mergeAll( - SessionNs.defaultLayer, - Database.defaultLayer, - EventV2Bridge.defaultLayer, - CrossSpawnSpawner.defaultLayer, +const compactionEnv = AppNodeBuilder.build( + LayerNode.group([SessionNs.node, SessionProjector.node, Database.node, EventV2Bridge.node, CrossSpawnSpawner.node]), ) const itCompaction = testEffect(compactionEnv) @@ -260,7 +248,7 @@ type CompactionProcessOptions = { result?: "continue" | "compact" llm?: Layer.Layer plugin?: Layer.Layer - provider?: ReturnType + provider?: ReturnType config?: Layer.Layer } @@ -269,30 +257,25 @@ function withCompaction(options?: CompactionProcessOptions) { } function compactionProcessLayer(options?: CompactionProcessOptions) { - const events = EventV2Bridge.defaultLayer - const status = SessionStatus.layer.pipe(Layer.provide(events)) - const processor = options?.llm - ? SessionProcessorModule.SessionProcessor.layer.pipe( - Layer.provide(summary), - Layer.provide(Image.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provide(status), - ) - : layer(options?.result ?? "continue") - return Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, events, status).pipe( - Layer.provide(SessionNs.defaultLayer), - Layer.provide((options?.provider ?? wide()).layer), - Layer.provide(Snapshot.defaultLayer), - Layer.provide(options?.llm ?? LLM.defaultLayer), - Layer.provide(Permission.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(options?.plugin ?? Plugin.defaultLayer), - Layer.provide(status), - Layer.provide(events), - Layer.provide(options?.config ?? Config.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provide(EventV2Bridge.defaultLayer), - ) + const replacements: LayerNode.Replacements = [ + [Provider.node, (options?.provider ?? wide()).layer], + [RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })], + [SessionSummary.node, summary], + ] + if (!options?.llm) { + return AppNodeBuilder.build(compactionTestNode, [ + ...replacements, + [SessionProcessorModule.SessionProcessor.node, processorLayer(options?.result ?? "continue")], + ...(options?.plugin ? ([[Plugin.node, options.plugin]] as const) : []), + ...(options?.config ? ([[Config.node, options.config]] as const) : []), + ]) + } + return AppNodeBuilder.build(compactionTestNode, [ + ...replacements, + [LLM.node, options.llm], + ...(options?.plugin ? ([[Plugin.node, options.plugin]] as const) : []), + ...(options?.config ? ([[Config.node, options.config]] as const) : []), + ]) } function createSummaryCompaction(sessionID: SessionID) { @@ -318,7 +301,7 @@ function llm() { push(stream: Stream.Stream | ((input: LLM.StreamInput) => Stream.Stream)) { queue.push(stream) }, - layer: Layer.succeed( + llmLayer: Layer.succeed( LLM.Service, LLM.Service.of({ stream: (input) => { @@ -614,9 +597,7 @@ describe("session.compaction.create", () => { }) const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe( - Effect.provide(SessionV2.defaultLayer), - Effect.provide(SessionExecution.noopLayer), - Effect.provide(locationServiceMapLayer), + Effect.provide(AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]])), ) expect(v2.at(-1)).toMatchObject({ type: "compaction", @@ -1014,7 +995,7 @@ describe("session.compaction.process", () => { expect(part?.type).toBe("compaction") expect(part?.tail_start_id).toBeUndefined() expect(captured).toContain("yyyy") - }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) })) + }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) })) }, { git: true }, ) @@ -1051,7 +1032,7 @@ describe("session.compaction.process", () => { expect(part?.tail_start_id).toBeUndefined() expect(captured).toContain("recent image turn") expect(captured).toContain("Attached image/png: big.png") - }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) })) + }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) })) }, { git: true }, ) @@ -1102,7 +1083,7 @@ describe("session.compaction.process", () => { expect(filtered[1]?.info.role).toBe("assistant") expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true) expect(filtered.map((msg) => msg.info.id)).not.toContain(large.id) - }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) })) + }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) })) }, { git: true }, ) @@ -1263,7 +1244,7 @@ describe("session.compaction.process", () => { expect(Cause.hasInterrupts(exit.cause)).toBe(true) expect(Date.now() - start).toBeLessThan(250) } - }).pipe(withCompaction({ llm: stub.layer })) + }).pipe(withCompaction({ llm: stub.llmLayer })) }, { git: true }, { timeout: 10_000 }, @@ -1336,7 +1317,7 @@ describe("session.compaction.process", () => { expect(summary?.parts.some((part) => part.type === "reasoning")).toBe(false) // Sanity: the text part still got through. expect(summary?.parts.some((part) => part.type === "text" && part.text === "summary")).toBe(true) - }).pipe(withCompaction({ llm: stub.layer })) + }).pipe(withCompaction({ llm: stub.llmLayer })) }, { git: true }, ) @@ -1372,7 +1353,7 @@ describe("session.compaction.process", () => { expect(summary?.info.role).toBe("assistant") expect(summary?.parts.some((part) => part.type === "tool")).toBe(false) - }).pipe(withCompaction({ llm: stub.layer })) + }).pipe(withCompaction({ llm: stub.llmLayer })) }, { git: true }, ) @@ -1409,7 +1390,7 @@ describe("session.compaction.process", () => { expect(captured).not.toContain("keep this turn") expect(captured).not.toContain("and this one too") expect(captured).not.toContain("What did we do so far?") - }).pipe(withCompaction({ llm: stub.layer })) + }).pipe(withCompaction({ llm: stub.llmLayer })) }, { git: true }, ) @@ -1451,7 +1432,7 @@ describe("session.compaction.process", () => { expect(captured.match(/summary one/g)?.length).toBe(1) expect(captured).toContain("## Constraints & Preferences") expect(captured).toContain("## Progress") - }).pipe(withCompaction({ llm: stub.layer })) + }).pipe(withCompaction({ llm: stub.llmLayer })) }, { git: true }, ) @@ -1493,7 +1474,7 @@ describe("session.compaction.process", () => { expect( filtered.some((msg) => msg.info.role === "user" && msg.parts.some((part) => part.type === "compaction")), ).toBe(true) - }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) })) + }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) })) }) itCompaction.instance(