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
parent 23f27d63b6
commit 5e2b06579a
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