chore: merge dev into v2 (#34788)

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>
This commit is contained in:
James Long
2026-07-01 17:12:00 -04:00
committed by GitHub
co-authored by 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
parent 932a40cfd9
commit 8c94e9005f
590 changed files with 15772 additions and 5530 deletions
+4 -2
View File
@@ -1,4 +1,5 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Option } from "effect"
import { sql } from "drizzle-orm"
@@ -13,9 +14,10 @@ const truncate = Layer.effectDiscard(
yield* db.run(sql`DELETE FROM account_state`)
yield* db.run(sql`DELETE FROM account`)
}),
).pipe(Layer.provide(Database.defaultLayer))
)
const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
const it = testEffect(Layer.merge(AccountRepo.defaultLayer, truncate))
const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
it.live("list returns empty when no accounts exist", () =>
Effect.gen(function* () {
@@ -1,4 +1,6 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Duration, Effect, Layer, Option, Schema } from "effect"
import { sql } from "drizzle-orm"
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http"
@@ -25,15 +27,16 @@ const truncate = Layer.effectDiscard(
yield* db.run(sql`DELETE FROM account_state`)
yield* db.run(sql`DELETE FROM account`)
}),
).pipe(Layer.provide(Database.defaultLayer))
)
const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
const it = testEffect(Layer.merge(AccountRepo.defaultLayer, truncate))
const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
const insideEagerRefreshWindow = Duration.toMillis(Duration.minutes(1))
const outsideEagerRefreshWindow = Duration.toMillis(Duration.minutes(10))
const live = (client: HttpClient.HttpClient) =>
Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
LayerNode.compile(Account.node, [[httpClient, Layer.succeed(HttpClient.HttpClient, client)]])
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
HttpClientResponse.fromWeb(
+6 -4
View File
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Directory } from "@/acp/directory"
import { Command } from "@/command"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@@ -83,8 +84,9 @@ const snapshot = (directory: string) => {
}
const fakeLayer = (calls: string[]) =>
Directory.layer.pipe(
Layer.provide(
LayerNode.compile(Directory.node, [
[
Directory.loaderNode,
Layer.succeed(
Directory.Loader,
Directory.Loader.of({
@@ -95,8 +97,8 @@ const fakeLayer = (calls: string[]) =>
}),
}),
),
),
)
],
])
describe("ACP directory snapshot", () => {
it.effect("two concurrent callers share one load", () => {
+2 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test"
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import type { Event, Message, OpencodeClient, Part, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
import { Effect, ManagedRuntime } from "effect"
import { ACPEvent } from "@/acp/event"
@@ -30,7 +31,7 @@ const pollUntil = async (
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}
@@ -6,6 +6,7 @@ import type {
SessionUpdate,
} from "@agentclientprotocol/sdk"
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { createTwoFilesPatch } from "diff"
import { Effect, ManagedRuntime } from "effect"
import { mkdtemp, rm } from "node:fs/promises"
@@ -37,7 +38,7 @@ const pollUntil = async (
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect } from "bun:test"
import type { McpServer } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -7,7 +8,7 @@ import * as ACPError from "@/acp/error"
import * as ACPSession from "@/acp/session"
import { testEffect } from "../lib/effect"
const sessionTest = testEffect(ACPSession.defaultLayer)
const sessionTest = testEffect(LayerNode.compile(ACPSession.node))
const model = (providerID: string, modelID: string): ACPSession.SelectedModel => ({
providerID: ProviderV2.ID.make(providerID),
+20 -17
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { SessionNotification } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { UsageService } from "@/acp/usage"
@@ -97,24 +98,26 @@ const fakeLayer = (input: {
readonly messages?: Effect.Effect<readonly UsageService.SessionMessage[], unknown>
readonly providers?: (directory: string) => Effect.Effect<Record<ProviderV2.ID, Provider.Info>, unknown>
}) =>
UsageService.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(
UsageService.MessageLoader,
UsageService.MessageLoader.of({
messages: () => input.messages ?? Effect.succeed([]),
}),
),
Layer.succeed(
UsageService.ContextLimitLoader,
UsageService.ContextLimitLoader.of({
providers: input.providers ?? (() => Effect.succeed(providers())),
}),
),
LayerNode.compile(UsageService.node, [
[
UsageService.messageLoaderNode,
Layer.succeed(
UsageService.MessageLoader,
UsageService.MessageLoader.of({
messages: () => input.messages ?? Effect.succeed([]),
}),
),
),
)
],
[
UsageService.contextLimitLoaderNode,
Layer.succeed(
UsageService.ContextLimitLoader,
UsageService.ContextLimitLoader.of({
providers: input.providers ?? (() => Effect.succeed(providers())),
}),
),
],
])
const connection = (updates: SessionNotification[]) => ({
sessionUpdate(params: SessionNotification) {
+4 -9
View File
@@ -1,4 +1,5 @@
import { afterEach, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit, Layer } from "effect"
import path from "path"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -14,17 +15,11 @@ import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { Truncate } from "../../src/tool/truncate"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Agent.layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(RuntimeFlags.layer(flags)),
LayerNode.compile(
LayerNode.group([Agent.node, Plugin.node, Provider.node, Auth.node, Config.node, Skill.node, RuntimeFlags.node]),
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
)
const it = testEffect(agentLayer())
@@ -1,4 +1,5 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { expect } from "bun:test"
import { Effect } from "effect"
import { Agent } from "../../src/agent/agent"
@@ -6,7 +7,7 @@ import { deriveSubagentSessionPermission } from "../../src/agent/subagent-permis
import { Permission } from "../../src/permission"
import { testEffect } from "../lib/effect"
const it = testEffect(Agent.defaultLayer)
const it = testEffect(LayerNode.compile(Agent.node))
function testAgent(input: {
name: string
@@ -1,16 +1,15 @@
import { expect } from "bun:test"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Npm } from "@opencode-ai/core/npm"
import { Effect } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import { Agent } from "../../src/agent/agent"
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 { Auth } from "../../src/auth"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
@@ -18,6 +17,8 @@ import { ProviderTest } from "../fake/provider"
import { SkillTest } from "../fake/skill"
import { testEffect } from "../lib/effect"
import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
// `it.instance` skips InstanceBootstrap so LSP / MCP don't spin up — those
// services hang during scope teardown on Windows and aren't needed
@@ -25,30 +26,16 @@ import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants"
const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "agent-plugin.ts")).href
const provider = ProviderTest.fake()
const configLayer = Config.layer.pipe(
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 it = testEffect(
AppNodeBuilder.build(LayerNode.group([Agent.node, Plugin.node]), [
[Auth.node, AuthTest.empty],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[Provider.node, provider.layer],
[Skill.node, SkillTest.empty],
[RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true })],
]),
)
const pluginLayer = Plugin.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(configLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
)
const agentLayer = Agent.layer.pipe(
Layer.provide(configLayer),
Layer.provide(AuthTest.empty),
Layer.provide(SkillTest.empty),
Layer.provide(provider.layer),
Layer.provide(pluginLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
)
const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer))
it.instance(
"plugin-registered agents appear in Agent.list",
+3 -5
View File
@@ -1,12 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { Auth } from "../../src/auth"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
const it = testEffect(LayerNode.compile(Auth.node))
describe("Auth", () => {
it.instance("set normalizes trailing slashes in keys", () =>
@@ -1,4 +1,5 @@
import { afterEach, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect } from "effect"
import { fileURLToPath } from "url"
@@ -6,7 +7,7 @@ import { InstanceRef } from "../../src/effect/instance-ref"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(FSUtil.defaultLayer)
const it = testEffect(LayerNode.compile(FSUtil.node))
afterEach(async () => {
await disposeAllInstances()
@@ -76,39 +76,35 @@ Positionals:
message message to send [array] [default: []]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--command the command to run, use message for args [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or
--session) [boolean]
--share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--command the command to run, use message for args [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or --session) [boolean]
--share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
[string] [choices: "default", "json"] [default: "default"]
-f, --file file(s) to attach to message [array]
--title title for the session (uses truncated prompt if no value
provided) [string]
--attach attach to a running opencode server (e.g.,
http://localhost:4096) [string]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD)
-f, --file file(s) to attach to message [array]
--title title for the session (uses truncated prompt if no value provided) [string]
--attach attach to a running opencode server (e.g., http://localhost:4096) [string]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
[string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or
'opencode') [string]
--dir directory to run in, path on remote server if attaching
--dir directory to run in, path on remote server if attaching [string]
--port port for the local server (defaults to random port if no value provided)
[number]
--variant model variant (provider-specific reasoning effort, e.g., high, max, minimal)
[string]
--port port for the local server (defaults to random port if no value
provided) [number]
--variant model variant (provider-specific reasoning effort, e.g., high,
max, minimal) [string]
--thinking show thinking blocks [boolean]
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
(dangerous!) [boolean] [default: false]"
--thinking show thinking blocks [boolean]
-i, --interactive run in direct interactive split-footer mode [boolean] [default: false]
--auto auto-approve permissions that are not explicitly denied (dangerous!)
[boolean] [default: false]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
@@ -1,5 +1,6 @@
import path from "path"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { describe, expect, test } from "bun:test"
import { Effect, FileSystem, Layer } from "effect"
@@ -98,7 +99,7 @@ function userMessage(
}
}
const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, NodeFileSystem.layer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(FSUtil.node), NodeFileSystem.layer))
function remap(root: string, file: string) {
if (file === Global.Path.state) {
@@ -123,7 +124,7 @@ function remappedFs(root: string) {
writeJson: (file, data, mode) => fs.writeJson(remap(root, file), data, mode),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
}
describe("run variant shared", () => {
@@ -1,11 +1,11 @@
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { Config } from "@/config/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Config.defaultLayer, AgentSvc.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, AgentSvc.node])))
it.instance(
"agent color parsed from project config",
+20 -34
View File
@@ -1,13 +1,14 @@
import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Cause, Effect, Exit, Layer, Option } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed"
import { ConfigParse } from "../../src/config/parse"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Npm } from "@opencode-ai/core/npm"
import { InstanceRef } from "../../src/effect/instance-ref"
import type { InstanceContext } from "../../src/project/instance-context"
@@ -41,13 +42,6 @@ import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
const infra = CrossSpawnSpawner.defaultLayer.pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const testFlock = EffectFlock.defaultLayer
const unexpectedHttp = HttpClient.make((request) =>
Effect.die(`unexpected http request: ${request.method} ${request.url}`),
)
@@ -104,16 +98,12 @@ const configLayer = (
client?: HttpClient.HttpClient
} = {},
) =>
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(Env.defaultLayer),
Layer.provide(options.auth ?? AuthTest.empty),
Layer.provide(options.account ?? AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)),
Layer.provideMerge(FSUtil.defaultLayer),
)
LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
[Auth.node, options.auth ?? AuthTest.empty],
[Account.node, options.account ?? AccountTest.empty],
[Npm.node, NpmTest.noop],
[httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)],
])
const layer = configLayer()
@@ -171,7 +161,7 @@ const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =
Effect.provideService(TestInstance, { directory: dir }),
provideInstanceEffect(dir),
Effect.provide(testInstanceStoreLayer),
Effect.provide(CrossSpawnSpawner.defaultLayer),
Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
)
const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
@@ -325,7 +315,7 @@ it.effect("creates global jsonc config with schema when no global configs exist"
const content = yield* FSUtil.use.readFileString(path.join(dir, "opencode.jsonc"))
expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
),
)
@@ -340,7 +330,7 @@ it.effect("does not create global config when OPENCODE_CONFIG_DIR is set", () =>
yield* Config.use.get().pipe(provideInstanceEffect(dir))
expect(yield* FSUtil.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
),
)
}),
@@ -930,7 +920,7 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
)
it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
@@ -948,7 +938,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
)
expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
)
// Note: deduplication and serialization of npm installs is now handled by the
@@ -1533,16 +1523,12 @@ test("remote well-known config can use FetchHttpClient layer", async () => {
Effect.scoped,
Effect.provide(
Layer.mergeAll(
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(wellKnownAuth(server.url.origin)),
Layer.provide(AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
),
LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
[Auth.node, wellKnownAuth(server.url.origin)],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[httpClient, FetchHttpClient.layer],
]),
testInstanceStoreLayer,
),
),
+9 -3
View File
@@ -1,6 +1,8 @@
import { expect } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
@@ -11,7 +13,7 @@ import { TuiConfig } from "../../src/config/tui"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Config.defaultLayer, FSUtil.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, FSUtil.node])))
const winIt = process.platform === "win32" ? it.instance : it.instance.skip
const globalConfigFiles = ["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"].map((file) =>
@@ -69,12 +71,16 @@ const withPlatform = <A, E, R>(platform: typeof process.platform, self: Effect.E
const getTuiConfig = (directory: string) =>
TuiConfig.Service.use((svc) => svc.get()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
Effect.provide(
AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory))),
),
)
const getTuiPluginOrigins = (directory: string) =>
TuiConfig.Service.use((svc) => svc.pluginOrigins()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
Effect.provide(
AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory))),
),
)
it.instance("keeps server and tui plugin merge semantics aligned", () =>
@@ -5,9 +5,8 @@ import Http from "node:http"
import path from "node:path"
import { NodeHttpServer } from "@effect/platform-node"
import { Effect, Exit, Fiber, Layer, Schema } from "effect"
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { eq } from "drizzle-orm"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Database } from "@opencode-ai/core/database/database"
import { ProjectV2 } from "@opencode-ai/core/project"
@@ -16,6 +15,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session as SessionNs } from "@/session/session"
import { SessionID } from "@/session/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { EventSequenceTable } from "@opencode-ai/core/event/sql"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, provideTmpdirInstance, requireInstance, TestInstance } from "../fixture/fixture"
@@ -27,13 +27,10 @@ import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-
import * as Workspace from "../../src/control-plane/workspace"
import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap"
import { Auth } from "@/auth"
import { SessionPrompt } from "@/session/prompt"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
const originalEnv = {
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
@@ -44,26 +41,27 @@ const originalEnv = {
}
const workspaceLayer = (experimentalWorkspaces: boolean) =>
Workspace.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(SessionNs.defaultLayer),
Layer.provide(SessionPrompt.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Vcs.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))),
AppNodeBuilder.build(
LayerNode.group([
Workspace.node,
SessionNs.node,
SessionProjector.node,
Database.node,
InstanceStore.node,
Ripgrep.node,
]),
[
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })],
[
InstanceStore.bootstrapNode,
Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })),
],
],
)
const testServerLayer = Layer.mergeAll(
NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }),
workspaceLayer(true),
SessionNs.defaultLayer,
Database.defaultLayer,
)
const it = testEffect(testServerLayer)
@@ -1,4 +1,5 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Deferred, Effect, Fiber, Layer, Logger } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppLayer } from "../../src/effect/app-runtime"
@@ -9,7 +10,8 @@ import { attach } from "../../src/effect/run-service"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(CrossSpawnSpawner.defaultLayer)
const observabilityLayer = LayerNode.compile(Observability.node)
const it = testEffect(LayerNode.compile(CrossSpawnSpawner.node))
function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
return {
@@ -34,7 +36,7 @@ it.live("makeRuntime installs the observability logger", () =>
)
const current = yield* Dummy.use((svc) => svc.current()).pipe(
Effect.provide(Layer.provideMerge(layer, Observability.layer)),
Effect.provide(Layer.provideMerge(layer, observabilityLayer)),
)
expect(current.size).toBeGreaterThan(0)
@@ -94,6 +96,6 @@ it.instance(
expect(result.directory).toBe(test.directory)
expect(result.size).toBeGreaterThan(0)
}).pipe(Effect.provide(Observability.layer)),
}).pipe(Effect.provide(observabilityLayer)),
{ git: true },
)
@@ -10,12 +10,12 @@ class TestConfig extends ConfigService.Service<TestConfig>()("@test/ConfigServic
}) {}
const fromConfig = (input: Record<string, unknown>) =>
TestConfig.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
TestConfig.layer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
const readConfig = TestConfig.useSync((config) => config)
describe("ConfigService", () => {
it.effect("defaultLayer parses values from the active ConfigProvider", () =>
it.effect("layer parses values from the active ConfigProvider", () =>
Effect.gen(function* () {
const config = yield* readConfig.pipe(
Effect.provide(
@@ -33,7 +33,7 @@ describe("ConfigService", () => {
}),
)
it.effect("defaultLayer applies Effect Config defaults", () =>
it.effect("layer applies Effect Config defaults", () =>
Effect.gen(function* () {
const config = yield* readConfig.pipe(Effect.provide(fromConfig({ NAME: "kit" })))
@@ -47,7 +47,7 @@ describe("ConfigService", () => {
Effect.gen(function* () {
const config = yield* readConfig.pipe(
Effect.provide(
TestConfig.layer({
TestConfig.configLayer({
name: "direct",
token: Option.some("parsed"),
port: 9000,
@@ -1,5 +1,6 @@
import { expect } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { $ } from "bun"
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
@@ -12,7 +13,7 @@ import {
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(CrossSpawnSpawner.node), testInstanceStoreLayer))
const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
InstanceState.get(state).pipe(provideInstanceEffect(dir))
@@ -1,15 +1,16 @@
import { describe, expect } from "bun:test"
import { ConfigProvider, Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { it } from "../lib/effect"
const fromConfig = (input: Record<string, unknown>) =>
RuntimeFlags.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
AppNodeBuilder.build(RuntimeFlags.node).pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
const readFlags = RuntimeFlags.Service.useSync((flags) => flags)
describe("RuntimeFlags", () => {
it.effect("defaultLayer defaults autoShare to false", () =>
it.effect("layer defaults autoShare to false", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
@@ -17,7 +18,7 @@ describe("RuntimeFlags", () => {
}),
)
it.effect("defaultLayer parses plugin flags from the active ConfigProvider", () =>
it.effect("layer parses plugin flags from the active ConfigProvider", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(
@@ -64,7 +65,7 @@ describe("RuntimeFlags", () => {
}),
)
it.effect("defaultLayer parses OPENCODE_EXPERIMENTAL_LSP_TY", () =>
it.effect("layer parses OPENCODE_EXPERIMENTAL_LSP_TY", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(
@@ -1,12 +1,11 @@
import { describe, test, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
import path from "path"
const live = FSUtil.layer.pipe(Layer.provide(NodeFileSystem.layer))
const { effect: it } = testEffect(live)
const { effect: it } = testEffect(LayerNode.compile(FSUtil.node))
describe("FSUtil", () => {
describe("isDir", () => {
+6 -2
View File
@@ -7,8 +7,10 @@ import { Effect, Context, Layer } from "effect"
import type * as PlatformError from "effect/PlatformError"
import type * as Scope from "effect/Scope"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import type { InstanceContext } from "../../src/project/instance-context"
@@ -17,7 +19,9 @@ import { InstanceStore } from "../../src/project/instance-store"
import { TestLLMServer } from "../lib/llm-server"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
export const testInstanceStoreLayer = LayerNode.compile(InstanceStore.node, [
[InstanceStore.bootstrapNode, noopBootstrap],
])
export async function provideTestInstance<R>(input: {
directory: string
@@ -204,7 +208,7 @@ export const withTmpdirInstance =
Effect.gen(function* () {
const directory = yield* tmpdirScoped(options)
return yield* self.pipe(Effect.provideService(TestInstance, { directory }), provideInstanceEffect(directory))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
export function provideTmpdirServer<A, E, R>(
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
+19 -15
View File
@@ -1,5 +1,5 @@
import { FetchHttpClient } from "effect/unstable/http"
import { Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Auth } from "../../src/auth"
@@ -14,17 +14,21 @@ import { SessionPrompt } from "../../src/session/prompt"
import { EventV2Bridge } from "../../src/event-v2-bridge"
export const workspaceLayerWithRuntimeFlags = (overrides: Partial<RuntimeFlags.Info>) =>
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(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer(overrides)),
Layer.provide(InstanceStore.defaultLayer),
Layer.provide(InstanceBootstrap.defaultLayer),
AppNodeBuilder.build(
LayerNode.group([
Workspace.node,
Auth.node,
Session.node,
SessionPrompt.node,
Project.node,
Vcs.node,
Database.node,
EventV2Bridge.node,
FSUtil.node,
InstanceStore.node,
]),
[
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
[RuntimeFlags.node, RuntimeFlags.layer(overrides)],
],
)
+9 -2
View File
@@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { provideTmpdirInstance, testInstanceStoreLayer, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -7,7 +8,9 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Format } from "../../src/format"
import * as Formatter from "../../src/format/formatter"
const it = testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
const it = testEffect(
Layer.mergeAll(LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])), NodeFileSystem.layer),
)
describe("Format", () => {
it.instance("status() returns empty list when no formatters are configured", () =>
@@ -106,7 +109,11 @@ describe("Format", () => {
)
testEffect(
Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
Layer.mergeAll(
LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])),
NodeFileSystem.layer,
testInstanceStoreLayer,
),
).live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.use.status(), {
+7 -9
View File
@@ -1,20 +1,18 @@
import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit } from "effect"
import { Image } from "@/image/image"
import { Config } from "@/config/config"
import { MessageID, PartID, SessionID } from "@/session/schema"
import path from "node:path"
import { TestConfig } from "../fixture/config"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Image.layer.pipe(Layer.provide(TestConfig.layer()))))
const it = testEffect(LayerNode.compile(Image.node, [[Config.node, TestConfig.layer()]]))
const tiny = testEffect(
Layer.mergeAll(
Image.layer.pipe(
Layer.provide(
TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) }),
),
),
),
LayerNode.compile(Image.node, [
[Config.node, TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) })],
]),
)
function part(mime: string, data: string) {
@@ -1,10 +1,13 @@
import { describe, expect } from "bun:test"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Effect, Layer, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { Installation } from "../../src/installation"
import { InstallationChannel } from "@opencode-ai/core/installation/version"
import { AppProcess } from "@opencode-ai/core/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
const encoder = new TextEncoder()
@@ -52,8 +55,15 @@ function testLayer(
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
spawnHandler?: (cmd: string, args: readonly string[]) => string | { code: number; stdout?: string; stderr?: string },
) {
const appProcess = AppProcess.layer.pipe(Layer.provide(mockSpawner(spawnHandler)))
return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(appProcess))
const spawnerNode = makeGlobalNode({
service: ChildProcessSpawner.ChildProcessSpawner,
layer: mockSpawner(spawnHandler),
deps: [],
})
return LayerNode.compile(Installation.node, [
[httpClient, mockHttpClient(httpHandler)],
[CrossSpawnSpawner.node, spawnerNode],
])
}
describe("installation", () => {
+2 -1
View File
@@ -1,10 +1,11 @@
import { describe, expect } from "bun:test"
import { Deferred, Effect, Fiber } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Job } from "@/job"
import { SessionSchema } from "@opencode-ai/core/session/schema"
import { testEffect } from "./lib/effect"
const it = testEffect(Job.defaultLayer)
const it = testEffect(LayerNode.compile(Job.node))
describe("job", () => {
it.instance("tracks started jobs through completion", () =>
+7 -1
View File
@@ -19,6 +19,8 @@
// different return shape — see the TODO at the bottom of OpencodeCli.
import { test, type TestOptions } from "bun:test"
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 { AppProcess } from "@opencode-ai/core/process"
import { Deferred, Duration, Effect, Layer, Queue, Schedule, Scope, Stream } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
@@ -469,7 +471,11 @@ export function withCliFixture<A, E>(
// and hit endpoints on `opencode.serve()` without rolling their own fetch.
}).pipe(
Effect.provide(
Layer.mergeAll(TestLLMServer.layer, FetchHttpClient.layer, FSUtil.defaultLayer, AppProcess.defaultLayer),
Layer.mergeAll(
TestLLMServer.layer,
FetchHttpClient.layer,
AppNodeBuilder.build(LayerNode.group([FSUtil.node, AppProcess.node])),
),
),
)
}
+7 -8
View File
@@ -1,5 +1,6 @@
import { describe, expect, spyOn } from "bun:test"
import path from "path"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer } from "effect"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Config } from "@/config/config"
@@ -11,19 +12,17 @@ import { TestInstance } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
LSP.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
Layer.provideMerge(EventV2Bridge.defaultLayer),
)
LayerNode.compile(LayerNode.group([LSP.node, Config.node, RuntimeFlags.node, EventV2Bridge.node]), [
[RuntimeFlags.node, RuntimeFlags.layer(flags)],
])
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
const it = testEffect(Layer.mergeAll(lspLayer(), LayerNode.compile(CrossSpawnSpawner.node)))
const experimentalTyIt = testEffect(
Layer.mergeAll(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
Layer.mergeAll(lspLayer({ experimentalLspTy: true }), LayerNode.compile(CrossSpawnSpawner.node)),
)
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
const disabledDownloadIt = testEffect(
Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
Layer.mergeAll(lspLayer({ disableLspDownload: true }), LayerNode.compile(CrossSpawnSpawner.node)),
)
describe("lsp.spawn", () => {
+3 -3
View File
@@ -1,13 +1,13 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
import path from "path"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { LSP } from "@/lsp/lsp"
import * as LSPServer from "@/lsp/server"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(LSP.node))
describe("LSP service lifecycle", () => {
let spawnSpy: ReturnType<typeof spyOn>
+8 -8
View File
@@ -1,8 +1,8 @@
import { expect, test } from "bun:test"
import { setTimeout as sleep } from "node:timers/promises"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { McpAuth } from "../../src/mcp/auth"
function authFile() {
@@ -10,7 +10,7 @@ function authFile() {
let activeWrites = 0
let sawOverlap = false
const layer = Layer.effect(
const fsLayer = Layer.effect(
FSUtil.Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -41,14 +41,14 @@ function authFile() {
: fs.writeJson(file, value, mode),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(AppNodeBuilder.build(FSUtil.node)))
return { layer, raw: () => raw }
return { fsLayer, raw: () => raw }
}
function authService(layer: Layer.Layer<FSUtil.Service>) {
function authService(fsLayer: Layer.Layer<FSUtil.Service>) {
return McpAuth.Service.use((auth) => Effect.succeed(auth)).pipe(
Effect.provide(McpAuth.layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(layer))),
Effect.provide(AppNodeBuilder.build(McpAuth.node, [[FSUtil.node, fsLayer]])),
)
}
@@ -57,8 +57,8 @@ test("serializes concurrent auth file updates across service instances", async (
await Effect.runPromise(
Effect.gen(function* () {
const first = yield* authService(file.layer)
const second = yield* authService(file.layer)
const first = yield* authService(file.fsLayer)
const second = yield* authService(file.fsLayer)
yield* Effect.all(
[
@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test"
import type { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { McpCatalog } from "@/mcp/catalog"
const options = { toolCallId: "call_mcp", abortSignal: new AbortController().signal } as any
function clientReturning(result: unknown) {
return {
callTool: async () => result,
} as unknown as Client
}
function mcpTool() {
return {
name: "screenshot",
description: "Take a screenshot",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
} as any
}
describe("McpCatalog.convertTool", () => {
test("preserves content when structuredContent is also present", async () => {
const content = [{ type: "image" as const, mimeType: "image/png", data: "AAAA" }]
const structuredContent = { image: { mimeType: "image/png", data: "AAAA" } }
const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content, structuredContent }))
const output = await converted.execute?.({}, options)
expect(output).toMatchObject({ content, structuredContent })
})
test("falls back to structuredContent only when content is absent", async () => {
const structuredContent = { results: [{ title: "one" }] }
const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content: [], structuredContent }))
const output = await converted.execute?.({}, options)
expect(output).toMatchObject({
structuredContent,
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
})
})
})
+2 -1
View File
@@ -1,4 +1,5 @@
import { describe, expect, mock, beforeEach } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
@@ -46,7 +47,7 @@ beforeEach(() => {
// Import MCP after mocking
const { MCP } = await import("../../src/mcp/index")
const it = testEffect(MCP.defaultLayer)
const it = testEffect(LayerNode.compile(MCP.node))
describe("mcp.headers", () => {
it.instance("headers are passed to transports when oauth is enabled (default)", () =>
+2 -1
View File
@@ -2,6 +2,7 @@ import path from "node:path"
import { pathToFileURL } from "node:url"
import { expect, mock, beforeEach } from "bun:test"
import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit } from "effect"
import type { MCP as MCPNS } from "../../src/mcp/index"
import { testEffect } from "../lib/effect"
@@ -275,7 +276,7 @@ beforeEach(() => {
const { MCP } = await import("../../src/mcp/index")
const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
const it = testEffect(MCP.defaultLayer)
const it = testEffect(LayerNode.compile(MCP.node))
function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server: string) {
if ("status" in status) return status.status
@@ -1,4 +1,5 @@
import { expect, mock, beforeEach } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { testEffect } from "../lib/effect"
@@ -155,15 +156,8 @@ const { FSUtil } = await import("@opencode-ai/core/fs-util")
const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
const mcpTest = testEffect(
Layer.mergeAll(
MCP.layer.pipe(
Layer.provide(McpAuth.defaultLayer),
Layer.provideMerge(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
),
McpAuth.defaultLayer,
LayerNode.compile(
LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]),
),
)
@@ -1,5 +1,6 @@
import { expect, mock, beforeEach } from "bun:test"
import { EventEmitter } from "events"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer, Option } from "effect"
import { awaitWithTimeout, testEffect } from "../lib/effect"
import type { MCP as MCPNS } from "../../src/mcp/index"
@@ -122,12 +123,8 @@ const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
const { FSUtil } = await import("@opencode-ai/core/fs-util")
const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
const mcpTest = testEffect(
MCP.layer.pipe(
Layer.provide(McpAuth.defaultLayer),
Layer.provideMerge(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
LayerNode.compile(
LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]),
),
)
const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
+2 -1
View File
@@ -1,4 +1,5 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import * as fs from "fs/promises"
import * as path from "path"
@@ -7,7 +8,7 @@ import { Patch } from "../../src/patch"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
const it = testEffect(FSUtil.defaultLayer)
const it = testEffect(LayerNode.compile(FSUtil.node))
describe("Patch namespace", () => {
let tempDir: string
@@ -1,11 +1,12 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { describe, test, expect } from "bun:test"
import { Effect } from "effect"
import { Permission } from "../src/permission"
import { Config } from "@/config/config"
import { testEffect } from "./lib/effect"
const it = testEffect(Config.defaultLayer)
const it = testEffect(LayerNode.compile(Config.node))
const load = Config.use.get()
@@ -4,21 +4,19 @@ import os from "os"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Database } from "@opencode-ai/core/database/database"
import { Permission } from "../../src/permission"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { MessageID, SessionID } from "../../src/session/schema"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
const events = EventV2Bridge.defaultLayer
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const env = Layer.mergeAll(
Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)),
events,
CrossSpawnSpawner.defaultLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)),
const env = AppNodeBuilder.build(
LayerNode.group([Permission.node, EventV2Bridge.node, CrossSpawnSpawner.node, InstanceStore.node]),
[[InstanceStore.bootstrapNode, noopBootstrap]],
)
const it = testEffect(env)
@@ -1,46 +1,40 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { ProviderAuth } from "@/provider/auth"
import { Plugin } from "@/plugin"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Auth } from "@/auth"
import { EventV2Bridge } from "@/event-v2-bridge"
import { TestConfig } from "../fixture/config"
import { testEffect } from "../lib/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Config } from "@/config/config"
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, FSUtil.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])))
function layer(directory: string, plugins: string[]) {
return ProviderAuth.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(
Plugin.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(RuntimeFlags.layer()),
Layer.provide(
TestConfig.layer({
get: () =>
Effect.succeed({
plugin: plugins,
plugin_origins: plugins.map((plugin) => ({
spec: plugin,
source: path.join(directory, "opencode.json"),
scope: "local" as const,
})),
}),
directories: () => Effect.succeed([directory]),
function providerAuthLayer(directory: string, plugins: string[]) {
return LayerNode.compile(ProviderAuth.node, [
[
Config.node,
TestConfig.layer({
get: () =>
Effect.succeed({
plugin: plugins,
plugin_origins: plugins.map((plugin) => ({
spec: plugin,
source: path.join(directory, "opencode.json"),
scope: "local" as const,
})),
}),
),
),
),
)
directories: () => Effect.succeed([directory]),
}),
],
[RuntimeFlags.node, RuntimeFlags.layer()],
])
}
describe("plugin.auth-override", () => {
@@ -73,10 +67,12 @@ describe("plugin.auth-override", () => {
const plain = yield* tmpdirScoped({ git: true })
const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href
const methods = yield* ProviderAuth.use.methods().pipe(Effect.provide(layer(tmp.directory, [plugin])))
const methods = yield* ProviderAuth.use
.methods()
.pipe(Effect.provide(providerAuthLayer(tmp.directory, [plugin])))
const plainMethods = yield* ProviderAuth.use
.methods()
.pipe(Effect.provide(layer(plain, [])), provideInstance(plain))
.pipe(Effect.provide(providerAuthLayer(plain, [])), provideInstance(plain))
const copilot = methods[ProviderV2.ID.make("github-copilot")]
expect(copilot).toBeDefined()
@@ -1,17 +1,18 @@
import { afterEach, describe, expect, spyOn } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { pathToFileURL } from "url"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Config } from "@/config/config"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const { Plugin } = await import("../../src/plugin/index")
const { PluginLoader } = await import("../../src/plugin/loader")
const { readPackageThemes } = await import("../../src/plugin/shared")
const { EventV2Bridge } = await import("../../src/event-v2-bridge")
const { Npm } = await import("@opencode-ai/core/npm")
const { TestConfig } = await import("../fixture/config")
const { RuntimeFlags } = await import("../../src/effect/runtime-flags")
@@ -20,7 +21,9 @@ afterEach(async () => {
await disposeAllInstances()
})
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
const it = testEffect(
Layer.mergeAll(LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])), testInstanceStoreLayer),
)
function withTmp<T, A, E, R>(
init: (dir: string) => Promise<T>,
@@ -45,10 +48,9 @@ function load(dir: string, flags?: Parameters<typeof RuntimeFlags.layer>[0]) {
yield* plugin.list()
}).pipe(
Effect.provide(
Plugin.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, ...flags })),
Layer.provide(
LayerNode.compile(Plugin.node, [
[
Config.node,
TestConfig.layer({
get: () =>
Effect.succeed({
@@ -57,8 +59,9 @@ function load(dir: string, flags?: Parameters<typeof RuntimeFlags.layer>[0]) {
}),
directories: () => Effect.succeed([dir]),
}),
),
),
],
[RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true, ...flags })],
]),
),
provideInstance(dir),
)
+12 -24
View File
@@ -1,14 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Effect } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Npm } from "@opencode-ai/core/npm"
import path from "path"
import { pathToFileURL } from "url"
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 { Auth } from "../../src/auth"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Plugin } from "../../src/plugin/index"
@@ -19,25 +16,16 @@ import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
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 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 it = testEffect(
Layer.mergeAll(
Plugin.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(configLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
),
CrossSpawnSpawner.defaultLayer,
),
AppNodeBuilder.build(LayerNode.group([Plugin.node, CrossSpawnSpawner.node]), [
[Auth.node, AuthTest.empty],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true })],
]),
)
const systemHook = "experimental.chat.system.transform"
@@ -1,63 +1,34 @@
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],
[InstanceStore.bootstrapNode, noopBootstrapLayer],
[RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true, experimentalWorkspaces: true })],
]),
)
afterEach(async () => {
@@ -2,16 +2,21 @@ import { afterEach, expect } from "bun:test"
import { existsSync } from "node:fs"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import { Cause, Effect, Exit, Fiber } from "effect"
import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
import { InstanceLayer } from "../../src/project/instance-layer"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { waitGlobalBusEvent } from "../server/global-bus"
const it = testEffect(Layer.mergeAll(InstanceLayer.layer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(
LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
]),
)
// InstanceBootstrap must run before any code touches the instance —
// originally tracked by PRs #25389 and #25449, now a permanent
@@ -1,9 +1,10 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { InstanceRef } from "../../src/effect/instance-ref"
import { registerDisposer } from "../../src/effect/instance-registry"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -15,7 +16,9 @@ const noopBootstrap = Layer.succeed(
)
const it = testEffect(
Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(noopBootstrap)),
LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
[InstanceStore.bootstrapNode, noopBootstrap],
]),
)
const setBootstrap = (run: Effect.Effect<void>) =>
@@ -9,11 +9,12 @@ import { ProjectV2 } from "@opencode-ai/core/project"
import { SessionID } from "../../src/session/schema"
import { $ } from "bun"
import { tmpdirScoped } from "../fixture/fixture"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Project.defaultLayer, CrossSpawnSpawner.defaultLayer, Database.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Project.node, Database.node, CrossSpawnSpawner.node])))
function legacySessionID() {
// Global-session migration covers persisted IDs from before prefixed session IDs.
@@ -2,8 +2,9 @@ import { describe, expect } from "bun:test"
import { $ } from "bun"
import path from "path"
import { eq } from "drizzle-orm"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect } from "effect"
import { Hash } from "@opencode-ai/core/util/hash"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Database } from "@opencode-ai/core/database/database"
@@ -13,7 +14,7 @@ import { Project } from "@/project/project"
import { tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Project.defaultLayer, Database.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Project.node, Database.node, CrossSpawnSpawner.node])))
function directories(projectID: ProjectV2.ID) {
return Database.Service.use(({ db }) =>
+24 -31
View File
@@ -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<typeof RuntimeFlags.layer>[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<Project.Info, never, Project.Service> {
+4 -5
View File
@@ -1,8 +1,9 @@
import { afterEach, describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { parsePatch } from "diff"
import { Deferred, Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import fs from "fs/promises"
import path from "path"
import {
@@ -24,10 +25,8 @@ import { testEffect } from "../lib/effect"
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
const layer = Layer.mergeAll(
Vcs.layer.pipe(Layer.provideMerge(Git.defaultLayer), Layer.provideMerge(EventV2Bridge.defaultLayer)),
CrossSpawnSpawner.defaultLayer,
FSUtil.defaultLayer,
const layer = LayerNode.compile(
LayerNode.group([Vcs.node, Git.node, EventV2Bridge.node, FSUtil.node, CrossSpawnSpawner.node]),
)
const it = testEffect(layer)
const worktreeIt = testEffect(Layer.mergeAll(layer, testInstanceStoreLayer))
@@ -2,13 +2,15 @@ import { $ } from "bun"
import { describe, expect } from "bun:test"
import * as fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Worktree } from "../../src/worktree"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(Worktree.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]))
const wintest = process.platform === "win32" ? it.instance : it.instance.skip
describe("Worktree.remove", () => {
@@ -1,16 +1,20 @@
import { afterEach, describe, expect } from "bun:test"
import path from "path"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Git } from "../../src/git"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Worktree } from "../../src/worktree"
import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(
Layer.mergeAll(Worktree.defaultLayer, FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer),
LayerNode.compile(LayerNode.group([Worktree.node, FSUtil.node, Git.node]), [
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
]),
)
const wintest = process.platform !== "win32" ? it.instance : it.instance.skip
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import path from "path"
import { unlink } from "fs/promises"
import { Global } from "@opencode-ai/core/global"
@@ -12,7 +13,7 @@ import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node])))
const originalEnv = new Map<string, string | undefined>()
@@ -1,4 +1,5 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Provider } from "../../src/provider/provider"
import { Effect } from "effect"
@@ -6,7 +7,7 @@ import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
const it = testEffect(Provider.defaultLayer)
const it = testEffect(LayerNode.compile(Provider.node))
const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
Effect.acquireUseRelease(
@@ -1,8 +1,9 @@
import { afterEach, expect } from "bun:test"
import { createServer, type Server } from "node:http"
import { streamText } from "ai"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect } from "effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
@@ -18,7 +19,7 @@ afterEach(async () => {
})
const it = testEffect(
Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer),
LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node, CrossSpawnSpawner.node])),
)
it.live("headerTimeout does not abort delayed SSE body after headers arrive", () =>
@@ -1,6 +1,8 @@
import { afterEach, expect, test } from "bun:test"
import { mkdir, unlink } from "fs/promises"
import path from "path"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Layer } from "effect"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -16,7 +18,8 @@ import { Provider } from "@/provider/provider"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Filesystem } from "@/util/filesystem"
import { InstanceLayer } from "@/project/instance-layer"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -57,14 +60,18 @@ afterEach(async () => {
})
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Provider.layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
LayerNode.compile(
LayerNode.group([
Provider.node,
FSUtil.node,
Env.node,
Config.node,
Auth.node,
Plugin.node,
ModelsDev.node,
RuntimeFlags.node,
]),
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
)
const list = Provider.use.list()
@@ -77,7 +84,7 @@ const paid = (providers: Record<string, { models: Record<string, { cost: { input
const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
const alphaProviderConfig = {
@@ -1755,8 +1762,11 @@ it.instance(
// Tests that need plugin file setup or multi-instance flows fall back to a
// scoped tmpdir + provideInstance pattern via it.effect.
const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
])
const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
eff.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
it.effect("plugin config providers persist after instance dispose", () =>
Effect.gen(function* () {
@@ -1859,7 +1869,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", ()
Provider.use
.list()
.pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
const none = paid(yield* listIn(noneDir))
const keyedCount = paid(yield* listIn(keyedDir))
@@ -1878,7 +1888,7 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
Provider.use
.list()
.pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
const none = paid(yield* listIn(noneDir))
@@ -613,6 +613,84 @@ describe("ProviderTransform.providerOptions", () => {
})
})
test("forces reasoning for custom OpenAI package models with explicit effort", () => {
const model = createModel({
providerID: "meta",
api: {
id: "muse-spark",
url: "https://api.ai.meta.com/v1",
npm: "@ai-sdk/openai",
},
})
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh", reasoningSummary: "auto" })).toEqual({
openai: { forceReasoning: true, reasoningEffort: "xhigh", reasoningSummary: "auto" },
})
})
test("forces reasoning for OpenAI package models marked reasoning-capable", () => {
expect(ProviderTransform.providerOptions(createModel(), { store: false })).toEqual({
openai: { forceReasoning: true, store: false },
})
})
test("forces reasoning for explicit effort even when model is not marked reasoning-capable", () => {
const model = createModel({
capabilities: {
temperature: true,
reasoning: false,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
})
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
})
})
test("forces reasoning for Azure OpenAI models with explicit effort", () => {
const model = createModel({
providerID: "azure",
api: {
id: "custom-gpt-5-deployment",
url: "https://azure.openai.example.com/openai/v1",
npm: "@ai-sdk/azure",
},
})
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
azure: { forceReasoning: true, reasoningEffort: "xhigh" },
})
})
test("forces reasoning for Bedrock Mantle OpenAI models with explicit effort", () => {
const model = createModel({
providerID: "amazon-bedrock",
api: {
id: "openai.gpt-5-custom",
url: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
npm: "@ai-sdk/amazon-bedrock/mantle",
},
})
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
})
})
test("overrides forceReasoning false when reasoning should be forced", () => {
expect(
ProviderTransform.providerOptions(createModel(), { forceReasoning: false, reasoningEffort: "xhigh" }),
).toEqual({
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
})
})
test("uses gateway model provider slug for gateway models", () => {
const model = createModel({
providerID: "vercel",
@@ -707,7 +785,7 @@ describe("ProviderTransform.providerOptions", () => {
})
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "medium" })).toEqual({
openai: { reasoningEffort: "medium" },
openai: { forceReasoning: true, reasoningEffort: "medium" },
})
})
@@ -2268,6 +2346,82 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted")
})
test("strips GitHub Copilot itemId from the copilot namespace, preserving other copilot options", () => {
const copilotModel = {
...openaiModel,
id: "github-copilot/gpt-5.5",
providerID: "github-copilot",
api: {
id: "gpt-5.5",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
}
const msgs = [
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking...",
providerOptions: {
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
},
},
{
// The stale itemId on tool-call parts is what Copilot echoes back as the
// `function_call` item `id`, which is what the upstream connection rejects.
type: "tool-call",
toolCallId: "call_1",
toolName: "bash",
input: { command: "ls" },
providerOptions: {
copilot: { itemId: "fc_456", reasoningEffort: "medium" },
},
},
],
},
] as any[]
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
expect(result[0].content[0].providerOptions?.copilot?.itemId).toBeUndefined()
expect(result[0].content[0].providerOptions?.copilot?.reasoningEncryptedContent).toBe("encrypted")
expect(result[0].content[1].providerOptions?.copilot?.itemId).toBeUndefined()
expect(result[0].content[1].providerOptions?.copilot?.reasoningEffort).toBe("medium")
})
test("leaves a stray openai namespace on a Copilot model untouched, since Copilot's Responses model only reads the copilot namespace", () => {
const copilotModel = {
...openaiModel,
id: "github-copilot/gpt-5.5",
providerID: "github-copilot",
api: {
id: "gpt-5.5",
url: "https://api.githubcopilot.com",
npm: "@ai-sdk/github-copilot",
},
}
const msgs = [
{
role: "assistant",
content: [
{
type: "text",
text: "Hello",
providerOptions: {
openai: { itemId: "msg_456" },
},
},
],
},
] as any[]
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_456")
})
test("preserves metadata for openai package when store is true", () => {
const msgs = [
{
@@ -3290,6 +3444,27 @@ describe("ProviderTransform.variants", () => {
})
})
test("anthropic sonnet 5 returns adaptive thinking options with xhigh", () => {
const model = createMockModel({
id: "anthropic/claude-sonnet-5",
providerID: "gateway",
api: {
id: "anthropic/claude-sonnet-5",
url: "https://gateway.ai",
npm: "@ai-sdk/gateway",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
expect(result.high).toEqual({
thinking: {
type: "adaptive",
display: "summarized",
},
effort: "high",
})
})
test("anthropic opus 4.6 omits display so it keeps the summarized default", () => {
const model = createMockModel({
id: "anthropic/claude-opus-4-6",
@@ -3877,6 +4052,12 @@ describe("ProviderTransform.variants", () => {
efforts: ["low", "medium", "high", "xhigh", "max"],
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
},
{
name: "sonnet 5",
apiIds: ["claude-sonnet-5", "claude-sonnet-5-20260630"],
efforts: ["low", "medium", "high", "xhigh", "max"],
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
},
{
name: "fable 5",
apiIds: ["claude-fable-5"],
@@ -3974,6 +4155,28 @@ describe("ProviderTransform.variants", () => {
effort: "high",
})
})
test("sonnet 5 uses adaptive reasoning for Vertex model IDs", () => {
const result = ProviderTransform.variants(
createMockModel({
id: "google-vertex-anthropic/claude-sonnet-5@default",
providerID: "google-vertex-anthropic",
api: {
id: "claude-sonnet-5@default",
url: "https://us-central1-aiplatform.googleapis.com",
npm: "@ai-sdk/google-vertex/anthropic",
},
}),
)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
expect(result.high).toEqual({
thinking: {
type: "adaptive",
display: "summarized",
},
effort: "high",
})
})
})
describe("@ai-sdk/amazon-bedrock", () => {
@@ -4047,6 +4250,28 @@ describe("ProviderTransform.variants", () => {
})
})
test("anthropic sonnet 5 returns adaptive reasoning options with xhigh", () => {
const result = ProviderTransform.variants(
createMockModel({
id: "bedrock/anthropic-claude-sonnet-5",
providerID: "bedrock",
api: {
id: "anthropic.claude-sonnet-5",
url: "https://bedrock.amazonaws.com",
npm: "@ai-sdk/amazon-bedrock",
},
}),
)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
expect(result.high).toEqual({
reasoningConfig: {
type: "adaptive",
maxReasoningEffort: "high",
display: "summarized",
},
})
})
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
const model = createMockModel({
id: "bedrock/llama-4",
@@ -4229,6 +4454,12 @@ describe("ProviderTransform.variants", () => {
efforts: ["low", "medium", "high", "xhigh", "max"],
thinking: { type: "adaptive", display: "summarized" },
},
{
name: "sonnet 5",
apiIds: ["anthropic--claude-sonnet-5", "anthropic--claude-5-sonnet"],
efforts: ["low", "medium", "high", "xhigh", "max"],
thinking: { type: "adaptive", display: "summarized" },
},
]) {
for (const apiId of testCase.apiIds) {
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
@@ -1,4 +1,5 @@
import { afterEach, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit, Fiber, Layer, Queue } from "effect"
import { Question } from "../../src/question"
import { InstanceRef } from "../../src/effect/instance-ref"
@@ -10,16 +11,9 @@ import { testEffect } from "../lib/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { EventV2Bridge } from "../../src/event-v2-bridge"
const it = testEffect(
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
)
const lifecycle = testEffect(
Layer.mergeAll(
Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
CrossSpawnSpawner.defaultLayer,
testInstanceStoreLayer,
),
)
const questionLayer = LayerNode.compile(LayerNode.group([Question.node, EventV2Bridge.node, CrossSpawnSpawner.node]))
const it = testEffect(questionLayer)
const lifecycle = testEffect(Layer.mergeAll(questionLayer, testInstanceStoreLayer))
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
sessionID: SessionID
@@ -1,4 +1,6 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Deferred, Effect, Layer } from "effect"
import { Project } from "@/project/project"
import { Session as SessionNs } from "@/session/session"
@@ -6,7 +8,9 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Project.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(
LayerNode.compile(LayerNode.group([SessionNs.node, SessionProjector.node, Project.node, CrossSpawnSpawner.node])),
)
const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
Effect.acquireRelease(SessionNs.use.create(input), (created) =>
@@ -56,9 +56,9 @@ const v2ApiLayer = HttpRouter.serve(
{ disableListenLog: true, disableLogger: true },
).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
const noAuthLayer = ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })
const secretLayer = ServerAuth.Config.layer({ password: Option.some("secret"), username: "opencode" })
const kitSecretLayer = ServerAuth.Config.layer({ password: Option.some("secret"), username: "kit" })
const noAuthLayer = ServerAuth.Config.configLayer({ password: Option.none(), username: "opencode" })
const secretLayer = ServerAuth.Config.configLayer({ password: Option.some("secret"), username: "opencode" })
const kitSecretLayer = ServerAuth.Config.configLayer({ password: Option.some("secret"), username: "kit" })
const it = testEffect(apiLayer.pipe(Layer.provide(noAuthLayer)))
const itSecret = testEffect(apiLayer.pipe(Layer.provide(secretLayer)))
@@ -44,7 +44,7 @@ const apiLayer = HttpRouter.serve(
moveSession: (value) => Ref.set(called, value),
}),
),
Layer.provide(ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })),
Layer.provide(ServerAuth.Config.configLayer({ password: Option.none(), username: "opencode" })),
)
const it = testEffect(apiLayer)
@@ -1,4 +1,5 @@
import { afterEach, describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { eq } from "drizzle-orm"
@@ -15,7 +16,7 @@ import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
const testWorktreeMutations = process.platform === "win32" ? it.instance.skip : it.instance
function request(path: string, directory: string, init: RequestInit = {}) {
@@ -38,7 +38,7 @@ const apiLayer = HttpRouter.serve(
upgrade: () => Effect.void,
}),
),
Layer.provide(ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })),
Layer.provide(ServerAuth.Config.configLayer({ password: Option.none(), username: "opencode" })),
)
const it = testEffect(apiLayer)
@@ -8,11 +8,9 @@ import { mkdir } from "node:fs/promises"
import path from "node:path"
import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { InstanceLayer } from "../../src/project/instance-layer"
import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session"
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
@@ -46,16 +44,7 @@ const testStateLayer = Layer.effectDiscard(
const workspaceLayer = workspaceLayerWithRuntimeFlags({ experimentalWorkspaces: true })
const it = testEffect(
Layer.mergeAll(
testStateLayer,
NodeHttpServer.layerTest,
NodeServices.layer,
InstanceLayer.layer,
Project.defaultLayer,
workspaceLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const it = testEffect(Layer.mergeAll(testStateLayer, NodeHttpServer.layerTest, NodeServices.layer, workspaceLayer))
const instanceContextTestLayer = Layer.mergeAll(
instanceContextLayer,
@@ -16,11 +16,9 @@ import * as Socket from "effect/unstable/socket/Socket"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
import { mkdir } from "node:fs/promises"
import { registerAdapter } from "../../src/control-plane/adapters"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { InstanceLayer } from "../../src/project/instance-layer"
import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session"
import {
@@ -51,16 +49,7 @@ const testStateLayer = Layer.effectDiscard(
const workspaceLayer = workspaceLayerWithRuntimeFlags({ experimentalWorkspaces: true })
const it = testEffect(
Layer.mergeAll(
testStateLayer,
NodeHttpServer.layerTest,
NodeServices.layer,
InstanceLayer.layer,
Project.defaultLayer,
workspaceLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const it = testEffect(Layer.mergeAll(testStateLayer, NodeHttpServer.layerTest, NodeServices.layer, workspaceLayer))
const instanceContextTestLayer = Layer.mergeAll(
instanceContextLayer,
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect, Layer } from "effect"
import path from "path"
@@ -15,7 +16,7 @@ const testStateLayer = Layer.effectDiscard(
),
)
const it = testEffect(Layer.mergeAll(testStateLayer, FSUtil.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(testStateLayer, LayerNode.compile(FSUtil.node), httpApiLayer))
const projectOptions = { config: { formatter: false, lsp: false } }
const providerID = "test-oauth-parity"
const oauthURL = "https://example.com/oauth"
@@ -1,4 +1,5 @@
import { afterEach, describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import { eq } from "drizzle-orm"
@@ -16,7 +17,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
const text = (response: HttpClientResponse.HttpClientResponse) => response.text
@@ -5,12 +5,14 @@ import { Deferred, Effect, Layer } from "effect"
import type * as Scope from "effect/Scope"
import { HttpServer } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process"
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 { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { validateSession } from "../../src/cli/tui/validate-session"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { MessageV2 } from "../../src/session/message-v2"
@@ -29,16 +31,12 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { Database } from "@opencode-ai/core/database/database"
import { httpApiLayer } from "./httpapi-layer"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const it = testEffect(
Layer.mergeAll(
FSUtil.defaultLayer,
CrossSpawnSpawner.defaultLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)),
Database.defaultLayer,
httpApiLayer,
),
const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const appLayer = AppNodeBuilder.build(
LayerNode.group([FSUtil.node, CrossSpawnSpawner.node, InstanceStore.node, Database.node, SessionNs.node]),
[[InstanceStore.bootstrapNode, noopBootstrapLayer]],
)
const it = testEffect(Layer.mergeAll(appLayer, httpApiLayer))
const original = {
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
@@ -55,6 +53,7 @@ type TestServices =
| FSUtil.Service
| ChildProcessSpawner.ChildProcessSpawner
| InstanceStore.Service
| SessionNs.Service
| HttpServer.HttpServer
type TestScope = Scope.Scope | TestServices
@@ -323,7 +322,7 @@ function seedMessage(directory: string, sessionID: string) {
})
return { message, part }
}),
).pipe(Effect.provide(SessionNs.defaultLayer)),
),
),
)
}
@@ -7,6 +7,8 @@ import path from "node:path"
import { Cause, Config, Effect, Exit, Layer } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServer } from "effect/unstable/http"
import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
@@ -14,7 +16,6 @@ import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project/bootstrap-service"
import { InstanceStore } from "../../src/project/instance-store"
import { Project } from "../../src/project/project"
@@ -37,14 +38,13 @@ import { testProviderConfig } from "../lib/test-provider"
import { pollWithTimeout, testEffect } from "../lib/effect"
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
const workspaceLayer = Workspace.defaultLayer.pipe(
Layer.provide(InstanceStore.defaultLayer),
Layer.provide(InstanceBootstrap.defaultLayer),
const noopBootstrapLayer = Layer.succeed(
InstanceBootstrapService.Service,
InstanceBootstrapService.Service.of({ run: Effect.void }),
)
const instanceStoreLayer = InstanceStore.defaultLayer.pipe(
Layer.provide(
Layer.succeed(InstanceBootstrapService.Service, InstanceBootstrapService.Service.of({ run: Effect.void })),
),
const appLayer = AppNodeBuilder.build(
LayerNode.group([InstanceStore.node, Project.node, Session.node, Workspace.node, Database.node, Ripgrep.node]),
[[InstanceStore.bootstrapNode, noopBootstrapLayer]],
)
const servedRoutes: Layer.Layer<never, Config.ConfigError, HttpServer.HttpServer> = HttpRouter.serve(
HttpApiApp.routes,
@@ -58,16 +58,7 @@ const httpApiLayer = servedRoutes.pipe(
Layer.provideMerge(NodeHttpServer.layerTest),
Layer.provideMerge(NodeServices.layer),
)
const it = testEffect(
Layer.mergeAll(
instanceStoreLayer,
Project.defaultLayer,
Session.defaultLayer,
workspaceLayer,
Database.defaultLayer,
httpApiLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const it = testEffect(Layer.mergeAll(appLayer, httpApiLayer))
function pathFor(path: string, params: Record<string, string>) {
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
@@ -432,7 +423,7 @@ describe("session HttpApi", () => {
cwd: sessionDirectory,
root: sessionDirectory,
})
}).pipe(Effect.provide(TestLLMServer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(TestLLMServer.layer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node))),
)
it.instance(
@@ -868,7 +859,7 @@ describe("session HttpApi", () => {
pathSession: yield* createSession(),
pathlessSession: yield* createSession(),
}
}).pipe(Effect.provideService(TestInstance, { directory: currentDir }), Effect.provide(Session.defaultLayer)),
}).pipe(Effect.provideService(TestInstance, { directory: currentDir })),
)
yield* clearSessionPath(pathlessSession.id)
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, mock } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
@@ -11,7 +12,7 @@ import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
const context = Context.empty() as Context.Context<unknown>
const it = testEffect(Layer.mergeAll(Session.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(Session.node), httpApiLayer))
afterEach(async () => {
mock.restore()
@@ -1,7 +1,8 @@
import { createHash } from "node:crypto"
import { describe, expect } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { ConfigProvider, Effect, Layer } from "effect"
import { ConfigProvider, Effect, Layer, Option } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import {
HttpClient,
HttpClientRequest,
@@ -39,7 +40,15 @@ const testStateLayer = Layer.effectDiscard(
}),
)
const it = testEffect(Layer.mergeAll(testStateLayer, FSUtil.defaultLayer, RuntimeFlags.layer()))
const fsUtilLayer = AppNodeBuilder.build(FSUtil.node)
const it = testEffect(Layer.mergeAll(testStateLayer, fsUtilLayer, RuntimeFlags.layer()))
function authConfigLayer(input?: { password?: string; username?: string }) {
return ServerAuth.Config.configLayer({
password: input?.password === undefined ? Option.none() : Option.some(input.password),
username: input?.username ?? "opencode",
})
}
function restoreEnv(key: string, value: string | undefined) {
if (value === undefined) {
@@ -94,18 +103,12 @@ function uiApp(input?: {
)
}),
).pipe(
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))),
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(authConfigLayer(input)))),
Layer.provide([
FSUtil.defaultLayer,
fsUtilLayer,
input?.client ?? httpClient(new Response("ui")),
RuntimeFlags.layer({ disableEmbeddedWebUi: input?.disableEmbeddedWebUi ?? false }),
HttpServer.layerServices,
ConfigProvider.layer(
ConfigProvider.fromUnknown({
OPENCODE_SERVER_PASSWORD: input?.password,
OPENCODE_SERVER_USERNAME: input?.username,
}),
),
]),
),
{ disableLogger: true },
@@ -141,7 +144,7 @@ function routeOrderingApp() {
}),
).pipe(
Layer.provide([
FSUtil.defaultLayer,
fsUtilLayer,
RuntimeFlags.layer({ disableEmbeddedWebUi: true }),
httpClient(new Response("ui"), (request) => {
proxiedUrl = request.url
@@ -21,7 +21,6 @@ import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
import { Database } from "@opencode-ai/core/database/database"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
@@ -55,11 +54,9 @@ const it = testEffect(
testStateLayer,
NodeHttpServer.layerTest,
NodeServices.layer,
Database.defaultLayer,
Project.defaultLayer,
workspaceLayer,
Socket.layerWebSocketConstructorGlobal,
).pipe(Layer.provide(Ripgrep.defaultLayer)),
),
)
type ProxiedRequest = {
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, mock } from "bun:test"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { Effect, Layer, Stream } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
@@ -23,20 +25,11 @@ import { testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
const workspaceLayer = Workspace.defaultLayer.pipe(
Layer.provide(InstanceStore.defaultLayer),
Layer.provide(InstanceBootstrap.defaultLayer),
)
const it = testEffect(
Layer.mergeAll(
Project.defaultLayer,
Session.defaultLayer,
workspaceLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)),
Database.defaultLayer,
httpApiLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)),
const appLayer = AppNodeBuilder.build(
LayerNode.group([Project.node, Session.node, Workspace.node, InstanceStore.node, Database.node, Ripgrep.node]),
[[InstanceStore.bootstrapNode, InstanceBootstrap.node]],
)
const it = testEffect(Layer.mergeAll(appLayer, httpApiLayer))
function request(path: string, directory: string, init: RequestInit = {}) {
return requestInDirectory(path, directory, init)
@@ -6,6 +6,7 @@
// strict `NonNegativeInt` schema then made every load of the message list
// fail to encode, killing Desktop boot for every user with such a row.
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { eq } from "drizzle-orm"
@@ -21,7 +22,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
function seedNegativeTokenSession() {
return Effect.gen(function* () {
@@ -2,12 +2,14 @@ import { afterEach, describe, expect } from "bun:test"
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Database } from "@opencode-ai/core/database/database"
import { Snapshot } from "@/snapshot"
import { InstanceBootstrap } from "@/project/bootstrap-service"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -20,9 +22,13 @@ afterEach(async () => {
})
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
const testInstanceStore = AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]])
const it = testEffect(
Layer.mergeAll(FSUtil.defaultLayer, Database.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer),
Layer.mergeAll(
AppNodeBuilder.build(LayerNode.group([FSUtil.node, Database.node, Snapshot.node])),
testInstanceStore,
httpApiLayer,
),
)
function request(directory: string, url: string, init: RequestInit = {}) {
@@ -1,10 +1,12 @@
import { afterEach, describe, expect } from "bun:test"
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 { Effect, Layer } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import path from "path"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Snapshot } from "../../src/snapshot"
@@ -19,9 +21,11 @@ afterEach(async () => {
})
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
const testInstanceStore = AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]])
const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer))
const it = testEffect(
Layer.mergeAll(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Snapshot.node])), testInstanceStore, httpApiLayer),
)
function request(directory: string, url: string, init: RequestInit = {}) {
return requestInDirectory(url, directory, init)
@@ -1,11 +1,12 @@
import { afterEach, describe, expect, mock } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { Session as SessionNs } from "@/session/session"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer))
afterEach(async () => {
mock.restore()
@@ -11,6 +11,7 @@
* asserts that GET /session/<id>/diff returns 200 with empty data.
*/
import { afterEach, describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
@@ -24,7 +25,7 @@ import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(Session.defaultLayer, Storage.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Storage.node])), httpApiLayer))
afterEach(async () => {
await disposeAllInstances()
@@ -1,5 +1,7 @@
import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Database } from "@opencode-ai/core/database/database"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Session as SessionNs } from "@/session/session"
@@ -9,24 +11,12 @@ import path from "path"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { eq } from "drizzle-orm"
import { testEffect } from "../lib/effect"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Storage } from "@/storage/storage"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Job } from "@/job"
const layer = (experimentalWorkspaces: boolean) =>
Layer.mergeAll(
Database.defaultLayer,
SessionNs.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Storage.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(SessionProjector.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
Layer.provide(Job.defaultLayer),
),
)
AppNodeBuilder.build(LayerNode.group([Database.node, SessionNs.node, SessionProjector.node]), [
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })],
])
const it = testEffect(layer(false))
const itWorkspaces = testEffect(layer(true))
@@ -1,5 +1,6 @@
import { afterEach, describe, expect } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import { Session as SessionNs } from "@/session/session"
@@ -12,7 +13,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer))
const model = {
providerID: ProviderV2.ID.make("test"),
@@ -1,11 +1,12 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { Session } from "@/session/session"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(Session.defaultLayer, httpApiLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(Session.node), httpApiLayer))
describe("tui.selectSession endpoint", () => {
it.instance(
@@ -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<LLM.Service>
plugin?: Layer.Layer<Plugin.Service>
provider?: ReturnType<typeof ProviderTest.fake>
provider?: ReturnType<typeof wide>
config?: Layer.Layer<Config.Service>
}
@@ -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<LLMEvent, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLMEvent, unknown>)) {
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(
@@ -2,34 +2,42 @@ import { describe, expect, test } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import path from "path"
import { Effect, FileSystem, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { NodeFileSystem } from "@effect/platform-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Instruction } from "../../src/session/instruction"
import type { MessageV2 } from "../../src/session/message-v2"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { Global } from "@opencode-ai/core/global"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { provideInstance, provideTmpdirInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { TestConfig } from "../fixture/config"
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"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap"
import { Config } from "@/config/config"
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer))
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([CrossSpawnSpawner.node, LayerNodePlatform.filesystem, InstanceStore.node]), [
[
InstanceBootstrap.node,
Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })),
],
]),
)
const configLayer = TestConfig.layer()
const configLayer = Layer.succeed(Config.Service, TestConfig.make())
const instructionLayer = (global: Partial<Global.Interface>, flags: Partial<RuntimeFlags.Info> = {}) =>
Instruction.layer.pipe(
Layer.provide(configLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Global.layerWith(global)),
Layer.provide(RuntimeFlags.layer(flags)),
)
AppNodeBuilder.build(Instruction.node, [
[Config.node, configLayer],
[Global.node, Global.layerWith(global)],
[RuntimeFlags.node, RuntimeFlags.layer(flags)],
])
const provideInstruction =
(global: Partial<Global.Interface>, flags?: Partial<RuntimeFlags.Info>) =>
@@ -1,6 +1,5 @@
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { HttpRecorder } from "@opencode-ai/http-recorder"
import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
@@ -10,14 +9,11 @@ import { Effect, Layer, Option, Schema, Stream } from "effect"
import path from "node:path"
import z from "zod"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { Filesystem } from "@/util/filesystem"
import { LLMEvent, LLMResponse } from "@opencode-ai/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { Env } from "@/env"
import { RequestExecutor } from "@opencode-ai/llm/route"
import { RuntimeFlags } from "@/effect/runtime-flags"
import type { Agent } from "../../src/agent/agent"
import { LLM } from "../../src/session/llm"
@@ -26,6 +22,9 @@ import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
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"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
@@ -240,7 +239,7 @@ const redactRecordedBody = (body: string) =>
function authLayer(scenario: RecordedScenario) {
const replayAuth = shouldRecord ? scenario.recordAuth?.() : scenario.replayAuth
if (!replayAuth) return Auth.defaultLayer
if (!replayAuth) return undefined
return Layer.mock(Auth.Service)({
get: (providerID) => Effect.succeed(providerID === scenario.providerID ? replayAuth : undefined),
all: () => Effect.succeed({ [scenario.providerID]: replayAuth }),
@@ -262,15 +261,6 @@ const modelsFixture = Filesystem.readJson<Record<string, ModelsDev.Provider>>(
function recordedNativeLLMLayer(scenario: RecordedScenario) {
const auth = authLayer(scenario)
const provider = Provider.layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(auth),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
const metadata = {
provider: scenario.providerID,
@@ -290,21 +280,11 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
redactor: HttpRecorderInternal.Redactor.make(redact),
})
: HttpRecorder.http(scenario.cassette, { directory: FIXTURES_DIR, metadata, redact })
const recordedClient = LLMClient.layer.pipe(
Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
)
return Layer.mergeAll(
provider,
LLM.layer.pipe(
Layer.provide(auth),
Layer.provide(Config.defaultLayer),
Layer.provide(provider),
Layer.provide(Plugin.defaultLayer),
Layer.provide(recordedClient),
Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: true })),
),
)
return AppNodeBuilder.build(LayerNode.group([Provider.node, LLM.node]), [
[LayerNodePlatform.requestExecutor, RequestExecutor.layer.pipe(Layer.provide(recordedHttp))],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: true })],
...(auth ? ([[Auth.node, auth]] as const) : []),
])
}
const writeConfig = (directory: string, scenario: RecordedScenario, model: ModelsDev.Provider["models"][string]) =>
@@ -3,6 +3,7 @@ import { LLMEvent, ToolFailure } from "@opencode-ai/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor, type LLMClientShape } from "@opencode-ai/llm/route"
import { jsonSchema, tool, type ModelMessage, type Tool } from "ai"
import { Effect, Fiber, Layer, Stream } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { LLMNative } from "@/session/llm/native-request"
import { LLMNativeRuntime } from "@/session/llm/native-runtime"
import type { Provider } from "@/provider/provider"
@@ -73,7 +74,11 @@ const providerInfo: Provider.Info = {
}
const it = testEffect(
LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(RequestExecutor.defaultLayer, WebSocketExecutor.layer))),
LLMClient.layer.pipe(
Layer.provide(
Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(FetchHttpClient.layer)), WebSocketExecutor.layer),
),
),
)
function responsesStream(chunks: unknown[]) {
+24 -29
View File
@@ -9,13 +9,10 @@ import { InstanceRef } from "../../src/effect/instance-ref"
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import z from "zod"
import { LLM } from "../../src/session/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Plugin } from "@/plugin"
import { testEffect } from "../lib/effect"
import type { Agent } from "../../src/agent/agent"
@@ -27,6 +24,9 @@ import { LLMAISDK } from "@/session/llm/ai-sdk"
import { Session as SessionNs } from "@/session/session"
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"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
@@ -52,15 +52,13 @@ const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: stri
}
}
const it = testEffect(Layer.mergeAll(LLM.defaultLayer, Provider.defaultLayer))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([LLM.node, Provider.node])))
// LLM.stream returns a Stream, not an Effect, so we can't use the serviceUse proxy.
const drain = (input: LLM.StreamInput) => LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain))
// drainWith builds an isolated runtime so the custom layer fully owns LLM and
// its transitive deps — `Effect.provide(layer)` over an existing runtime layers
// the new services on top, but transitive Service overrides (e.g. RequestExecutor)
// resolved through the outer LLM.defaultLayer leak through.
// drainWith builds an isolated runtime so custom replacements fully own LLM and
// its transitive deps.
const drainWith = (layer: Layer.Layer<LLM.Service>, input: LLM.StreamInput) =>
Effect.gen(function* () {
const ctx = yield* InstanceRef
@@ -75,15 +73,16 @@ const drainWith = (layer: Layer.Layer<LLM.Service>, input: LLM.StreamInput) =>
)
})
function llmLayerWithExecutor(executor: Layer.Layer<RequestExecutor.Service>, flags: Partial<RuntimeFlags.Info> = {}) {
return LLM.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(executor, WebSocketExecutor.layer)))),
Layer.provide(RuntimeFlags.layer(flags)),
)
function llmLayerWithExecutor(
options: {
executor?: Layer.Layer<RequestExecutor.Service>
flags?: Partial<RuntimeFlags.Info>
} = {},
) {
return AppNodeBuilder.build(LLM.node, [
[RuntimeFlags.node, RuntimeFlags.layer(options.flags)],
...(options.executor ? ([[LayerNodePlatform.requestExecutor, options.executor]] as const) : []),
])
}
describe("session.llm.hasToolCalls", () => {
@@ -1129,14 +1128,10 @@ describe("session.llm.stream", () => {
} satisfies Agent.Info
yield* drainWith(
LLM.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(failingNativeClient),
Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: false })),
),
AppNodeBuilder.build(LLM.node, [
[LayerNodePlatform.llmClient, failingNativeClient],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: false })],
]),
{
user: {
id: MessageID.make("msg_user-native-flag-off"),
@@ -1199,7 +1194,7 @@ describe("session.llm.stream", () => {
temperature: 0.2,
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native"),
sessionID,
@@ -1282,7 +1277,7 @@ describe("session.llm.stream", () => {
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(executor, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ executor, flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native-injected-tool"),
sessionID,
@@ -1371,7 +1366,7 @@ describe("session.llm.stream", () => {
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), {
yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), {
user: {
id: MessageID.make("msg_user-native-tool"),
sessionID,
@@ -176,12 +176,12 @@ const root = LayerNode.group([
CrossSpawnSpawner.node,
])
const replacements = [
LayerNode.replace(SessionSummary.layer, summary),
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
]
[SessionSummary.node, summary],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
] as const
const env = LayerNode.compile(
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)
@@ -206,12 +206,7 @@ const providerErrorLLM = Layer.succeed(
),
}),
)
const providerErrorEnv = LayerNode.compile(
root,
new Map(
[...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)].map((item) => [item.source, item.replacement]),
),
)
const providerErrorEnv = LayerNode.compile(root, [...replacements, [LLM.node, providerErrorLLM]])
const itProviderError = testEffect(providerErrorEnv)
const fragmentFailureLLM = Layer.succeed(
@@ -228,12 +223,7 @@ const fragmentFailureLLM = Layer.succeed(
),
}),
)
const fragmentFailureEnv = LayerNode.compile(
root,
new Map(
[...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)].map((item) => [item.source, item.replacement]),
),
)
const fragmentFailureEnv = LayerNode.compile(root, [...replacements, [LLM.node, fragmentFailureLLM]])
const itFragmentFailure = testEffect(fragmentFailureEnv)
const boot = Effect.fn("test.boot")(function* () {
+74 -80
View File
@@ -1,10 +1,10 @@
import { NodeFileSystem } from "@effect/platform-node"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { eq } from "drizzle-orm"
import { EventV2Bridge } from "@/event-v2-bridge"
import { FetchHttpClient } from "effect/unstable/http"
import { expect } from "bun:test"
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import path from "path"
@@ -56,7 +56,7 @@ import { reply, TestLLMServer } from "../lib/llm-server"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
const summary = Layer.succeed(
SessionSummary.Service,
@@ -156,10 +156,6 @@ const lsp = Layer.succeed(
}),
)
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
const processorCreateStarted: Array<() => void> = []
const blockingProcessor = Layer.succeed(
SessionProcessor.Service,
@@ -168,80 +164,75 @@ const blockingProcessor = Layer.succeed(
}),
)
const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const promptRoot = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
Job.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
])
function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
const deps = Layer.mergeAll(
Session.defaultLayer,
Snapshot.defaultLayer,
LLM.defaultLayer,
Env.defaultLayer,
AgentSvc.defaultLayer,
Command.defaultLayer,
Permission.defaultLayer,
Plugin.defaultLayer,
Config.defaultLayer,
ProviderSvc.defaultLayer,
lsp,
makeMcp(input?.mcpInstructions),
FSUtil.defaultLayer,
Job.defaultLayer,
status,
Database.defaultLayer,
EventV2Bridge.defaultLayer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
)
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
const proc =
input?.processor === "blocking"
? blockingProcessor
: SessionProcessor.layer.pipe(
Layer.provide(summary),
Layer.provide(Image.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
)
const compact = SessionCompaction.layer.pipe(
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(proc),
Layer.provideMerge(deps),
)
return SessionPrompt.layer.pipe(
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
Layer.provide(
SystemPrompt.layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(deps),
),
),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
Layer.provide(summary),
)
const replacements = [
[SessionSummary.node, summary],
[LSP.node, lsp],
[MCP.node, makeMcp(input?.mcpInstructions)],
[RuntimeFlags.node, runtimeFlags],
] as const
if (input?.processor === "blocking") {
return LayerNode.compile(promptRoot, [...replacements, [SessionProcessor.node, blockingProcessor]])
}
return LayerNode.compile(promptRoot, replacements)
}
function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
return Layer.mergeAll(TestLLMServer.layer, makePrompt(input))
const root = LayerNode.group([promptRoot, testLLMServerNode])
const replacements = [
[SessionSummary.node, summary],
[LSP.node, lsp],
[MCP.node, makeMcp(input?.mcpInstructions)],
[RuntimeFlags.node, runtimeFlags],
] as const
if (input?.processor === "blocking") {
return LayerNode.compile(root, [...replacements, [SessionProcessor.node, blockingProcessor]])
}
return LayerNode.compile(root, replacements)
}
function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
@@ -697,9 +688,12 @@ noLLMServer.instance.skip(
})
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
Effect.provide(SessionV2.defaultLayer),
Effect.provide(SessionExecution.noopLayer),
Effect.provide(locationServiceMapLayer),
Effect.provide(
LayerNode.compile(SessionV2.node, [
[SessionExecution.node, SessionExecution.noopLayer],
[LocationServiceMap.node, locationServiceMapLayer],
]),
),
)
const { db } = yield* Database.Service
const row = yield* db
@@ -1,29 +1,28 @@
import { describe, expect } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect } from "effect"
import { Session } from "@/session/session"
import { SessionRevert } from "../../src/session/revert"
import { MessageV2 } from "../../src/session/message-v2"
import { Snapshot } from "../../src/snapshot"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const env = Layer.mergeAll(
Session.defaultLayer,
SessionRevert.defaultLayer,
Snapshot.defaultLayer,
CrossSpawnSpawner.defaultLayer,
const it = testEffect(
LayerNode.compile(
LayerNode.group([Session.node, SessionRevert.node, Snapshot.node, SessionProjector.node, CrossSpawnSpawner.node]),
),
)
const it = testEffect(env)
const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "default") {
const session = yield* Session.Service
return yield* session.updateMessage({
+20 -15
View File
@@ -1,6 +1,5 @@
import { describe, expect } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Deferred, Effect, Exit, Layer } from "effect"
@@ -8,26 +7,32 @@ import { Session as SessionNs } from "@/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Storage } from "@/storage/storage"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Job } from "@/job"
import { EventV2Bridge } from "@/event-v2-bridge"
import { GlobalBus } from "@/bus/global"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap"
const it = testEffect(
Layer.mergeAll(
SessionNs.layer.pipe(
Layer.provide(Storage.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provideMerge(EventV2Bridge.defaultLayer),
Layer.provide(SessionProjector.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: false })),
Layer.provide(Job.defaultLayer),
),
CrossSpawnSpawner.defaultLayer,
testInstanceStoreLayer,
AppNodeBuilder.build(
LayerNode.group([
SessionNs.node,
EventV2Bridge.node,
SessionProjector.node,
CrossSpawnSpawner.node,
InstanceStore.node,
]),
[
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces: false })],
[
InstanceBootstrap.node,
Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })),
],
],
),
)
@@ -87,16 +87,11 @@ const root = LayerNode.group([
LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] }),
])
const it = testEffect(
LayerNode.compile(
root,
new Map(
[
LayerNode.replace(MCP.layer, mcp),
LayerNode.replace(LSP.layer, lsp),
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
].map((item) => [item.source, item.replacement]),
),
),
LayerNode.compile(root, [
[MCP.node, mcp],
[LSP.node, lsp],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
]),
)
const providerCfg = (url: string) => ({
@@ -1,7 +1,9 @@
import { describe, expect, test } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Session } from "@/session/session"
import { SessionPrompt } from "../../src/session/prompt"
import { MessageV2 } from "../../src/session/message-v2"
@@ -9,9 +11,7 @@ import { testEffect } from "../lib/effect"
// Skip tests if no API key is available
const hasApiKey = !!process.env.ANTHROPIC_API_KEY
const it = testEffect(
Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const it = testEffect(AppNodeBuilder.build(LayerNode.group([SessionPrompt.node, Session.node, Ripgrep.node])))
const live = hasApiKey ? it.instance : it.instance.skip
describe("StructuredOutput Integration", () => {
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import type { Agent } from "../../src/agent/agent"
import { NamedError } from "@opencode-ai/core/util/error"
@@ -6,7 +7,6 @@ import { Skill } from "../../src/skill"
import { Permission } from "../../src/permission"
import { SystemPrompt } from "../../src/session/system"
import { MCP } from "../../src/mcp"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { testEffect } from "../lib/effect"
const skills: Skill.Info[] = [
@@ -43,9 +43,9 @@ const build: Agent.Info = {
}
const it = testEffect(
SystemPrompt.layer.pipe(
Layer.provide(locationServiceMapLayer),
Layer.provide(
LayerNode.compile(SystemPrompt.node, [
[
MCP.node,
Layer.mock(MCP.Service, {
instructions: () =>
Effect.succeed([
@@ -61,8 +61,9 @@ const it = testEffect(
},
]),
}),
),
Layer.provide(
],
[
Skill.node,
Layer.succeed(
Skill.Service,
Skill.Service.of({
@@ -77,8 +78,8 @@ const it = testEffect(
available: () => Effect.succeed(skills),
}),
),
),
),
],
]),
)
describe("session.system", () => {
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect } from "bun:test"
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 { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -34,15 +34,12 @@ const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unkno
const none = HttpClient.make(() => Effect.die("unexpected http call"))
function requestLayer(client: HttpClient.HttpClient) {
const replacement = LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))
return LayerNode.compile(
LayerNode.group([ShareNext.node, AccountRepo.node]),
new Map([[replacement.source, replacement.replacement]]),
)
const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
return LayerNode.compile(LayerNode.group([ShareNext.node, AccountRepo.node]), [replacement])
}
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(
LayerNode.group([
ShareNext.node,
@@ -52,7 +49,7 @@ function integrationLayer(client: HttpClient.HttpClient) {
AccountRepo.node,
Database.node,
]),
new Map([[replacement.source, replacement.replacement]]),
[replacement],
)
}
@@ -1,6 +1,7 @@
import { describe, expect, beforeAll, afterAll } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { Discovery } from "../../src/skill/discovery"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
@@ -18,7 +19,7 @@ let mutableFiles = ["SKILL.md"]
const fixturePath = path.join(import.meta.dir, "../fixture/skills")
const cacheDir = path.join(Global.Path.cache, "skills")
const it = testEffect(Layer.mergeAll(Discovery.defaultLayer, FSUtil.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Discovery.node, FSUtil.node])))
beforeAll(async () => {
await rm(cacheDir, { recursive: true, force: true })
+5 -18
View File
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { Skill } from "../../src/skill"
import { Discovery } from "../../src/skill/discovery"
@@ -13,33 +14,19 @@ import { testEffect } from "../lib/effect"
import path from "path"
import fs from "fs/promises"
const node = CrossSpawnSpawner.defaultLayer
const node = LayerNode.compile(CrossSpawnSpawner.node)
const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node, testInstanceStoreLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(Skill.node), node, testInstanceStoreLayer))
const itWithoutClaudeCodeSkills = testEffect(
Layer.mergeAll(
Skill.layer.pipe(
Layer.provide(Discovery.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layer),
Layer.provide(RuntimeFlags.layer({ disableClaudeCodeSkills: true })),
),
LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableClaudeCodeSkills: true })]]),
node,
testInstanceStoreLayer,
),
)
const itWithoutExternalSkills = testEffect(
Layer.mergeAll(
Skill.layer.pipe(
Layer.provide(Discovery.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layer),
Layer.provide(RuntimeFlags.layer({ disableExternalSkills: true })),
),
LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableExternalSkills: true })]]),
node,
testInstanceStoreLayer,
),
@@ -1,6 +1,7 @@
import { afterEach, expect } from "bun:test"
import { $ } from "bun"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import fs from "fs/promises"
import path from "path"
@@ -15,7 +16,9 @@ import {
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
const it = testEffect(
Layer.mergeAll(LayerNode.compile(LayerNode.group([Snapshot.node, FSUtil.node])), testInstanceStoreLayer),
)
// Windows forbids both * and : in directory names.
const nonWindowsIt = process.platform === "win32" ? it.live.skip : it.live
@@ -74,11 +77,12 @@ const withTrackedSnapshot = <A, E, R>(
})
const bootstrapScoped = Effect.fn("SnapshotTest.bootstrapScoped")(function* () {
const dir = yield* tmpdirScoped({ git: true }).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer))
const dir = yield* tmpdirScoped({ git: true }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)))
return { path: dir, extra: yield* initialize(dir) }
})
const scopedGitTmpdir = () => tmpdirScoped({ git: true }).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer))
const scopedGitTmpdir = () =>
tmpdirScoped({ git: true }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)))
const cleanupWorktree = (repo: string, worktree: string, files: string[] = []) =>
Effect.promise(async () => {
@@ -1,5 +1,6 @@
import { describe, expect } from "bun:test"
import path from "path"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Exit, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -11,7 +12,7 @@ import { testEffect } from "../lib/effect"
const dir = path.join(Global.Path.data, "storage")
const it = testEffect(Layer.mergeAll(Storage.defaultLayer, FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Storage.node, FSUtil.node, CrossSpawnSpawner.node])))
const scope = Effect.fnUntraced(function* () {
const root = ["storage_test", crypto.randomUUID()]
@@ -50,14 +51,14 @@ function remappedFs(root: string) {
fs.glob(pattern, options?.cwd ? { ...options, cwd: remap(root, options.cwd) } : options),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
}
// Layer.fresh forces a new Storage instance — without it, Effect's in-test layer cache
// returns the outer testEffect's Storage (which uses the real FSUtil), not a new
// one built on top of remappedFs.
const remappedStorage = (root: string) =>
Layer.fresh(Storage.layer.pipe(Layer.provide(remappedFs(root)), Layer.provide(Git.defaultLayer)))
Layer.fresh(LayerNode.compile(Storage.node, [[FSUtil.node, remappedFs(root)]]))
describe("Storage", () => {
it.live("round-trips JSON content", () =>
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import path from "path"
import * as fs from "fs/promises"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit, Layer } from "effect"
import { ApplyPatchTool } from "../../src/tool/apply_patch"
import { LSP } from "@/lsp/lsp"
@@ -14,13 +15,8 @@ import { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
const it = testEffect(
Layer.mergeAll(
LSP.defaultLayer,
FSUtil.defaultLayer,
Format.defaultLayer,
EventV2Bridge.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
LayerNode.compile(
LayerNode.group([LSP.node, FSUtil.node, Format.node, EventV2Bridge.node, Truncate.node, Agent.node]),
),
)

Some files were not shown because too many files have changed in this diff Show More