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:
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)],
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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(), {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () =>
|
||||
|
||||
@@ -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])),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user