refactor(core): move database schema ownership (#29068)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dax
2026-05-30 21:08:38 -04:00
committed by GitHub
parent 6bcb9cb9bb
commit 7f571d36ea
390 changed files with 11127 additions and 9164 deletions

View File

@@ -7,7 +7,7 @@ import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Truncate } from "@/tool/truncate"
import { TestInstance } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
@@ -18,7 +18,7 @@ const it = testEffect(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
Format.defaultLayer,
Bus.layer,
EventV2Bridge.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
),

View File

@@ -8,7 +8,7 @@ import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Truncate } from "@/tool/truncate"
import { SessionID, MessageID } from "../../src/session/schema"
import * as Tool from "../../src/tool/tool"
@@ -34,7 +34,7 @@ const layer = Layer.mergeAll(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
Format.defaultLayer,
Bus.layer,
EventV2Bridge.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
)
@@ -83,10 +83,13 @@ const makeDirectory = Effect.fn("EditToolTest.makeDirectory")(function* (p: stri
})
const onceBus = Effect.fn("EditToolTest.onceBus")(function* (def: typeof FileWatcher.Event.Updated) {
const bus = yield* Bus.Service
const events = yield* EventV2Bridge.Service
const deferred = yield* Deferred.make<void>()
const unsub = yield* bus.subscribeCallback(def, () => Effect.runSync(Deferred.succeed(deferred, undefined)))
yield* Effect.addFinalizer(() => Effect.sync(unsub))
const unsub = yield* events.listen((event) => {
if (event.type === def.type) Deferred.doneUnsafe(deferred, Effect.void)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
return deferred
})

View File

@@ -5,7 +5,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import type { Tool } from "@/tool/tool"
import { assertExternalDirectoryEffect } from "../../src/tool/external-directory"
import { Filesystem } from "@/util/filesystem"
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
@@ -48,27 +48,26 @@ describe("tool.assertExternalDirectory", () => {
}),
)
it.live("no-ops for paths inside the instance directory", () =>
provideInstance("/tmp/project")(
Effect.gen(function* () {
const { requests, ctx } = makeCtx()
yield* assertExternalDirectoryEffect(ctx, path.join("/tmp/project", "file.txt"))
expect(requests.length).toBe(0)
}),
),
)
it.live("asks with a single canonical glob", () =>
it.instance("no-ops for paths inside the instance directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const { requests, ctx } = makeCtx()
const directory = "/tmp/project"
const target = "/tmp/outside/file.txt"
yield* assertExternalDirectoryEffect(ctx, path.join(test.directory, "file.txt"))
expect(requests.length).toBe(0)
}),
)
it.instance("asks with a single canonical glob", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const { requests, ctx } = makeCtx()
const target = path.join(path.dirname(test.directory), "outside", "file.txt")
const expected = glob(path.join(path.dirname(target), "*"))
yield* provideInstance(directory)(assertExternalDirectoryEffect(ctx, target))
yield* assertExternalDirectoryEffect(ctx, target)
const req = requests.find((r) => r.permission === "external_directory")
expect(req).toBeDefined()
@@ -77,15 +76,15 @@ describe("tool.assertExternalDirectory", () => {
}),
)
it.live("uses target directory when kind=directory", () =>
it.instance("uses target directory when kind=directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const { requests, ctx } = makeCtx()
const directory = "/tmp/project"
const target = "/tmp/outside"
const target = path.join(path.dirname(test.directory), "outside")
const expected = glob(path.join(target, "*"))
yield* provideInstance(directory)(assertExternalDirectoryEffect(ctx, target, { kind: "directory" }))
yield* assertExternalDirectoryEffect(ctx, target, { kind: "directory" })
const req = requests.find((r) => r.permission === "external_directory")
expect(req).toBeDefined()
@@ -95,15 +94,13 @@ describe("tool.assertExternalDirectory", () => {
)
it.live("skips prompting when bypass=true", () =>
provideInstance("/tmp/project")(
Effect.gen(function* () {
const { requests, ctx } = makeCtx()
Effect.gen(function* () {
const { requests, ctx } = makeCtx()
yield* assertExternalDirectoryEffect(ctx, "/tmp/outside/file.txt", { bypass: true })
yield* assertExternalDirectoryEffect(ctx, "/tmp/outside/file.txt", { bypass: true })
expect(requests.length).toBe(0)
}),
),
expect(requests.length).toBe(0)
}),
)
if (process.platform === "win32") {

View File

@@ -4,7 +4,7 @@ import os from "os"
import path from "path"
import { Effect, Layer } from "effect"
import { GrepTool } from "../../src/tool/grep"
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
@@ -42,6 +42,7 @@ const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
const it = testEffect(toolLayer())
const scout = testEffect(toolLayer({ experimentalScout: true }))
const rooted = testEffect(Layer.mergeAll(toolLayer(), testInstanceStoreLayer))
const ctx = {
sessionID: SessionID.make("ses_test"),
@@ -90,7 +91,7 @@ const git = Effect.fn("GrepToolTest.git")(function* (cwd: string, args: string[]
})
describe("tool.grep", () => {
it.live("basic search", () =>
rooted.live("basic search", () =>
Effect.gen(function* () {
const info = yield* GrepTool
const grep = yield* info.init()

View File

@@ -10,7 +10,7 @@ import { MessageID, SessionID } from "../../src/session/schema"
import { Tool } from "@/tool/tool"
import { Truncate } from "@/tool/truncate"
import { LspTool } from "../../src/tool/lsp"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
afterEach(async () => {
@@ -98,10 +98,11 @@ const asks = () => {
describe("tool.lsp", () => {
describe("permission metadata", () => {
it.live("keeps cursor details for position-based operations", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
it.instance(
"keeps cursor details for position-based operations",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
@@ -117,15 +118,15 @@ describe("tool.lsp", () => {
character: 7,
})
expect(result.title).toBe("goToDefinition test.ts:3:7")
}),
{ git: true },
),
}),
{ git: true },
)
it.live("omits cursor details for documentSymbol", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
it.instance(
"omits cursor details for documentSymbol",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
@@ -139,15 +140,15 @@ describe("tool.lsp", () => {
filePath: file,
})
expect(result.title).toBe("documentSymbol test.ts")
}),
{ git: true },
),
}),
{ git: true },
)
it.live("omits file and cursor details for workspaceSymbol", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
it.instance(
"omits file and cursor details for workspaceSymbol",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
@@ -161,15 +162,15 @@ describe("tool.lsp", () => {
operation: "workspaceSymbol",
})
expect(result.title).toBe("workspaceSymbol")
}),
{ git: true },
),
}),
{ git: true },
)
it.live("passes workspaceSymbol query to LSP", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
it.instance(
"passes workspaceSymbol query to LSP",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
@@ -178,9 +179,8 @@ describe("tool.lsp", () => {
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
}),
{ git: true },
),
}),
{ git: true },
)
})
})

View File

@@ -7,7 +7,7 @@ import { Agent } from "../../src/agent/agent"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Truncate } from "@/tool/truncate"
import { testEffect } from "../lib/effect"
import { Bus } from "../../src/bus"
import { EventV2Bridge } from "../../src/event-v2-bridge"
const ctx = {
sessionID: SessionID.make("ses_test-session"),
@@ -22,7 +22,7 @@ const ctx = {
const it = testEffect(
Layer.mergeAll(
Question.layer.pipe(Layer.provideMerge(Bus.layer)),
Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
CrossSpawnSpawner.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
@@ -30,10 +30,13 @@ const it = testEffect(
)
const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) {
const bus = yield* Bus.Service
const events = yield* EventV2Bridge.Service
const asked = yield* Queue.unbounded<void>()
const off = yield* bus.subscribeCallback(Question.Event.Asked, () => Queue.offerUnsafe(asked, undefined))
yield* Effect.addFinalizer(() => Effect.sync(off))
const off = yield* events.listen((event) => {
if (event.type === Question.Event.Asked.type) Queue.offerUnsafe(asked, undefined)
return Effect.void
})
yield* Effect.addFinalizer(() => off)
for (;;) {
const items = yield* question.list()

View File

@@ -15,7 +15,7 @@ import { ReadTool } from "../../src/tool/read"
import { Truncate } from "@/tool/truncate"
import { Tool } from "@/tool/tool"
import { Filesystem } from "@/util/filesystem"
import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
@@ -55,8 +55,8 @@ const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Truncate.defaultLayer,
)
const it = testEffect(readLayer())
const scout = testEffect(readLayer({ experimentalScout: true }))
const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))
const scout = testEffect(Layer.mergeAll(readLayer({ experimentalScout: true }), testInstanceStoreLayer))
const init = Effect.fn("ReadToolTest.init")(function* () {
const info = yield* ReadTool

View File

@@ -4,6 +4,7 @@ import fs from "fs/promises"
import { fileURLToPath, pathToFileURL } from "url"
import { Effect, Layer, Result, Schema } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Database } from "@opencode-ai/core/database/database"
import { ToolRegistry } from "@/tool/registry"
import { Tool } from "@/tool/tool"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -22,7 +23,7 @@ import { Provider } from "@/provider/provider"
import { Git } from "@/git"
import { LSP } from "@/lsp/lsp"
import { Instruction } from "@/session/instruction"
import { Bus } from "@/bus"
import { EventV2Bridge } from "@/event-v2-bridge"
import { FetchHttpClient } from "effect/unstable/http"
import { Format } from "@/format"
import { Ripgrep } from "@/file/ripgrep"
@@ -30,10 +31,11 @@ import * as Truncate from "@/tool/truncate"
import { InstanceState } from "@/effect/instance-state"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
import { ProviderID, ModelID } from "@/provider/schema"
import { ToolJsonSchema } from "@/tool/json-schema"
import { MessageID, SessionID } from "@/session/schema"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
const node = CrossSpawnSpawner.defaultLayer
const configLayer = TestConfig.layer({
@@ -62,10 +64,10 @@ const registryLayer = (opts: RegistryLayerOptions = {}) =>
Layer.provide(LSP.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Bus.layer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Format.defaultLayer),
Layer.provide(node),
Layer.provide(Layer.mergeAll(node, Database.defaultLayer)),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Truncate.defaultLayer),
)
@@ -144,8 +146,8 @@ describe("tool.registry", () => {
const build = yield* agent.get("build")
if (!build) throw new Error("build agent not found")
const task = (yield* registry.tools({
providerID: ProviderID.opencode,
modelID: ModelID.make("test"),
providerID: ProviderV2.ID.opencode,
modelID: ProviderV2.ModelID.make("test"),
agent: build,
})).find((tool) => tool.id === "task")
@@ -322,8 +324,8 @@ describe("tool.registry", () => {
const agents = yield* Agent.Service
const promptTools = yield* registry.tools({
providerID: ProviderID.opencode,
modelID: ModelID.make("test"),
providerID: ProviderV2.ID.opencode,
modelID: ProviderV2.ModelID.make("test"),
agent: yield* agents.defaultInfo(),
})
const promptTool = promptTools.find((tool) => tool.id === "sql")

View File

@@ -11,7 +11,7 @@ import { MessageID, SessionID } from "../../src/session/schema"
import { Truncate } from "../../src/tool/truncate"
import { RepoCloneTool } from "../../src/tool/repo_clone"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
afterEach(async () => {
@@ -80,9 +80,8 @@ const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
)
describe("tool.repo_clone", () => {
it.live("clones a repo into the managed cache and reuses it on subsequent calls", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("clones a repo into the managed cache and reuses it on subsequent calls", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
@@ -106,13 +105,11 @@ describe("tool.repo_clone", () => {
expect(cloned.metadata.localPath).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo"))
expect(cached.metadata.status).toBe("cached")
expect(yield* fs.readFileString(path.join(cloned.metadata.localPath, "README.md"))).toBe("v1\n")
}),
),
}),
)
it.live("refresh updates an existing cached clone", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("refresh updates an existing cached clone", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
@@ -145,13 +142,11 @@ describe("tool.repo_clone", () => {
expect(first.metadata.status).toBe("cloned")
expect(refreshed.metadata.status).toBe("refreshed")
expect(yield* fs.readFileString(path.join(first.metadata.localPath, "README.md"))).toBe("v2\n")
}),
),
}),
)
it.live("clones a configured branch", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("clones a configured branch", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
@@ -177,19 +172,18 @@ describe("tool.repo_clone", () => {
expect(result.metadata.status).toBe("cloned")
expect(result.metadata.branch).toBe("docs")
expect(yield* fs.readFileString(path.join(result.metadata.localPath, "DOCS.md"))).toBe("docs\n")
}),
),
}),
)
it.live("rejects invalid repository inputs", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("rejects invalid repository inputs", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const tool = yield* init()
const inputs = [
{ repository: "not-a-repo", message: "git URL" },
{ repository: "git@github.com:../../../etc/passwd", message: "git URL" },
{ repository: "-u:foo/bar", message: "git URL" },
{ repository: pathToFileURL(path.join(_dir, "local.git")).href, message: "Local file" },
{ repository: pathToFileURL(path.join(dir, "local.git")).href, message: "Local file" },
]
yield* Effect.forEach(
@@ -206,13 +200,11 @@ describe("tool.repo_clone", () => {
}),
{ discard: true },
)
}),
),
}),
)
it.live("rejects local file repository URLs", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("rejects local file repository URLs", () =>
Effect.gen(function* () {
const source = yield* tmpdirScoped({ git: true })
const tool = yield* init()
const result = yield* tool.execute({ repository: pathToFileURL(source).href }, ctx).pipe(Effect.exit)
@@ -222,13 +214,11 @@ describe("tool.repo_clone", () => {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Local file")
}
}),
),
}),
)
it.live("rejects invalid branch inputs", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("rejects invalid branch inputs", () =>
Effect.gen(function* () {
const tool = yield* init()
const result = yield* tool.execute({ repository: "owner/repo", branch: "bad..branch" }, ctx).pipe(Effect.exit)
@@ -239,7 +229,6 @@ describe("tool.repo_clone", () => {
"Branch must contain only alphanumeric characters",
)
}
}),
),
}),
)
})

View File

@@ -9,7 +9,7 @@ import { Global } from "@opencode-ai/core/global"
import { MessageID, SessionID } from "../../src/session/schema"
import { Truncate } from "../../src/tool/truncate"
import { RepoOverviewTool } from "../../src/tool/repo_overview"
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
afterEach(async () => {
@@ -43,9 +43,8 @@ const init = Effect.fn("RepoOverviewToolTest.init")(function* () {
})
describe("tool.repo_overview", () => {
it.live("summarizes a local repository path", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("summarizes a local repository path", () =>
Effect.gen(function* () {
const repo = yield* tmpdirScoped({ git: true })
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(
@@ -93,13 +92,12 @@ describe("tool.repo_overview", () => {
expect(result.output).toContain("Top-level structure:")
expect(result.output).toContain("src/")
expect(result.output).toContain("README.md")
}),
),
}),
)
it.live("resolves relative paths from the instance directory", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
it.instance("resolves relative paths from the instance directory", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(path.join(dir, "nested", "README.md"), "# Nested\n")
@@ -108,13 +106,11 @@ describe("tool.repo_overview", () => {
expect(result.metadata.path).toBe(path.join(dir, "nested"))
expect(result.output).toContain("README.md")
}),
),
}),
)
it.live("resolves a cached repository from repository shorthand", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("resolves a cached repository from repository shorthand", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "github.com", "owner", "repo")
yield* fs.writeWithDirs(path.join(cached, "package.json"), JSON.stringify({ name: "cached-repo" }, null, 2))
@@ -127,13 +123,11 @@ describe("tool.repo_overview", () => {
expect(result.metadata.repository).toBe("owner/repo")
expect(result.output).toContain("Repository: owner/repo")
expect(result.output).toContain(`Path: ${cached}`)
}),
),
}),
)
it.live("fails clearly when a repository is not cloned", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("fails clearly when a repository is not cloned", () =>
Effect.gen(function* () {
const tool = yield* init()
const result = yield* tool.execute({ repository: "missing/repo" }, ctx).pipe(Effect.exit)
@@ -142,13 +136,11 @@ describe("tool.repo_overview", () => {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Use repo_clone first")
}
}),
),
}),
)
it.live("resolves cached repositories from host/path references", () =>
provideTmpdirInstance((_dir) =>
Effect.gen(function* () {
it.instance("resolves cached repositories from host/path references", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "gitlab.com", "group", "repo")
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
@@ -159,7 +151,6 @@ describe("tool.repo_overview", () => {
expect(result.metadata.path).toBe(cached)
expect(result.metadata.repository).toBe("gitlab.com/group/repo")
expect(result.output).toContain("Repository: gitlab.com/group/repo")
}),
),
}),
)
})

View File

@@ -7,7 +7,7 @@ import { Config } from "@/config/config"
import { Shell } from "../../src/shell/shell"
import { ShellTool } from "../../src/tool/shell"
import { Filesystem } from "@/util/filesystem"
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import { Agent } from "../../src/agent/agent"
import { Truncate } from "@/tool/truncate"
@@ -18,6 +18,7 @@ import { Plugin } from "../../src/plugin"
import { testEffect } from "../lib/effect"
import { Tool } from "@/tool/tool"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { InstanceStore } from "@/project/instance-store"
const shellLayer = Layer.mergeAll(
CrossSpawnSpawner.defaultLayer,
@@ -27,10 +28,12 @@ const shellLayer = Layer.mergeAll(
Config.defaultLayer,
Agent.defaultLayer,
RuntimeFlags.defaultLayer,
testInstanceStoreLayer,
)
const it = testEffect(shellLayer)
type ShellTestServices =
| (typeof shellLayer extends Layer.Layer<infer ROut, infer _E, infer _RIn> ? ROut : never)
| InstanceStore.Service
| Scope.Scope
const initShell = Effect.fn("ShellToolTest.init")(function* () {

View File

@@ -7,7 +7,7 @@ import type { Permission } from "../../src/permission"
import type { Tool } from "@/tool/tool"
import { SkillTool } from "../../src/tool/skill"
import { ToolRegistry } from "@/tool/registry"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
@@ -30,9 +30,9 @@ const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
describe("tool.skill", () => {
it.live("execute returns skill content block with files", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
it.instance("execute returns skill content block with files", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
yield* Effect.promise(() =>
Bun.write(
@@ -87,13 +87,12 @@ Use this skill.
expect(result.output).toContain(`<skill_content name="tool-skill">`)
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
expect(result.output).toContain(`<file>${file}</file>`)
}),
),
}),
)
it.live("execute preserves not found message", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
it.instance("execute preserves not found message", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const home = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = dir
yield* Effect.addFinalizer(() =>
@@ -127,7 +126,6 @@ Use this skill.
expect(error).toBeInstanceOf(Error)
if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
}
}),
),
}),
)
})

View File

@@ -1,8 +1,10 @@
import { afterEach, describe, expect } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { Database } from "@opencode-ai/core/database/database"
import { Effect, Exit, Fiber, Layer } from "effect"
import { Agent } from "../../src/agent/agent"
import { BackgroundJob } from "@/background/job"
import { Bus } from "@/bus"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Config } from "@/config/config"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Session } from "@/session/session"
@@ -11,28 +13,29 @@ import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionRunState } from "@/session/run-state"
import { SessionStatus } from "@/session/status"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { disposeAllInstances } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
afterEach(async () => {
await disposeAllInstances()
})
const ref = {
providerID: ProviderID.make("test"),
modelID: ModelID.make("test-model"),
providerID: ProviderV2.ID.make("test"),
modelID: ProviderV2.ModelID.make("test-model"),
}
const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
Agent.defaultLayer,
BackgroundJob.defaultLayer,
Bus.defaultLayer,
EventV2Bridge.defaultLayer,
Config.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Session.defaultLayer,
@@ -40,6 +43,7 @@ const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
SessionStatus.defaultLayer,
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
Database.defaultLayer,
RuntimeFlags.layer(flags),
)
@@ -65,7 +69,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
model: ref,
time: { created: Date.now() },
})
const assistant: MessageV2.Assistant = {
const assistant: SessionLegacy.Assistant = {
id: MessageID.ascending(),
role: "assistant",
parentID: user.id,
@@ -95,7 +99,7 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void;
}
}
function reply(input: SessionPrompt.PromptInput, text: string): MessageV2.WithParts {
function reply(input: SessionPrompt.PromptInput, text: string): SessionLegacy.WithParts {
const id = MessageID.ascending()
return {
info: {

View File

@@ -2,9 +2,10 @@ import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { parseResponse } from "../../src/tool/mcp-websearch"
import { selectWebSearchProvider, webSearchModelName, webSearchProviderLabel } from "../../src/tool/websearch"
import { ProviderID } from "../../src/provider/schema"
import { webSearchEnabled } from "../../src/tool/registry"
import { it } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
const SESSION_ID = "ses_0196aabbccddeeff001122334455"
@@ -37,10 +38,10 @@ describe("websearch provider", () => {
})
test("is only enabled for opencode or explicit websearch provider flags", () => {
expect(webSearchEnabled(ProviderID.opencode, { exa: false, parallel: false })).toBe(true)
expect(webSearchEnabled(ProviderID.openai, { exa: false, parallel: false })).toBe(false)
expect(webSearchEnabled(ProviderID.openai, { exa: true, parallel: false })).toBe(true)
expect(webSearchEnabled(ProviderID.openai, { exa: false, parallel: true })).toBe(true)
expect(webSearchEnabled(ProviderV2.ID.opencode, { exa: false, parallel: false })).toBe(true)
expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: false })).toBe(false)
expect(webSearchEnabled(ProviderV2.ID.openai, { exa: true, parallel: false })).toBe(true)
expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: true })).toBe(true)
})
test("uses branded labels", () => {

View File

@@ -5,7 +5,7 @@ import fs from "fs/promises"
import { WriteTool } from "../../src/tool/write"
import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Bus } from "../../src/bus"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Format } from "../../src/format"
import { Truncate } from "@/tool/truncate"
import { Tool } from "@/tool/tool"
@@ -34,7 +34,7 @@ const it = testEffect(
Layer.mergeAll(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
Bus.layer,
EventV2Bridge.defaultLayer,
Format.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Truncate.defaultLayer,