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