|
@@ -4,9 +4,10 @@ import os from "os"
|
|
|
import path from "path"
|
|
import path from "path"
|
|
|
import { Effect, Layer } from "effect"
|
|
import { Effect, Layer } from "effect"
|
|
|
import { GrepTool } from "../../src/tool/grep"
|
|
import { GrepTool } from "../../src/tool/grep"
|
|
|
-import { provideInstance, TestInstance } from "../fixture/fixture"
|
|
|
|
|
|
|
+import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
|
|
import { SessionID, MessageID } from "../../src/session/schema"
|
|
import { SessionID, MessageID } from "../../src/session/schema"
|
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
|
|
|
+import { Global } from "@opencode-ai/core/global"
|
|
|
import { Truncate } from "@/tool/truncate"
|
|
import { Truncate } from "@/tool/truncate"
|
|
|
import { Agent } from "../../src/agent/agent"
|
|
import { Agent } from "../../src/agent/agent"
|
|
|
import { Ripgrep } from "../../src/file/ripgrep"
|
|
import { Ripgrep } from "../../src/file/ripgrep"
|
|
@@ -15,17 +16,32 @@ import { testEffect } from "../lib/effect"
|
|
|
import { Reference } from "@/reference/reference"
|
|
import { Reference } from "@/reference/reference"
|
|
|
import { Permission } from "../../src/permission"
|
|
import { Permission } from "../../src/permission"
|
|
|
import type * as Tool from "../../src/tool/tool"
|
|
import type * as Tool from "../../src/tool/tool"
|
|
|
|
|
+import { Config } from "@/config/config"
|
|
|
|
|
+import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
|
|
|
+import { Git } from "@/git"
|
|
|
|
|
+import { Filesystem } from "@/util/filesystem"
|
|
|
|
|
|
|
|
-const it = testEffect(
|
|
|
|
|
|
|
+const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|
|
|
|
+ Reference.layer.pipe(
|
|
|
|
|
+ Layer.provide(Config.defaultLayer),
|
|
|
|
|
+ Layer.provide(AppFileSystem.defaultLayer),
|
|
|
|
|
+ Layer.provide(Git.defaultLayer),
|
|
|
|
|
+ Layer.provide(RuntimeFlags.layer(flags)),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|
|
Layer.mergeAll(
|
|
Layer.mergeAll(
|
|
|
CrossSpawnSpawner.defaultLayer,
|
|
CrossSpawnSpawner.defaultLayer,
|
|
|
AppFileSystem.defaultLayer,
|
|
AppFileSystem.defaultLayer,
|
|
|
Ripgrep.defaultLayer,
|
|
Ripgrep.defaultLayer,
|
|
|
Truncate.defaultLayer,
|
|
Truncate.defaultLayer,
|
|
|
Agent.defaultLayer,
|
|
Agent.defaultLayer,
|
|
|
- Reference.defaultLayer,
|
|
|
|
|
- ),
|
|
|
|
|
-)
|
|
|
|
|
|
|
+ Git.defaultLayer,
|
|
|
|
|
+ referenceLayer(flags),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+const it = testEffect(toolLayer())
|
|
|
|
|
+const scout = testEffect(toolLayer({ experimentalScout: true }))
|
|
|
|
|
|
|
|
const ctx = {
|
|
const ctx = {
|
|
|
sessionID: SessionID.make("ses_test"),
|
|
sessionID: SessionID.make("ses_test"),
|
|
@@ -39,6 +55,39 @@ const ctx = {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const root = path.join(__dirname, "../..")
|
|
const root = path.join(__dirname, "../..")
|
|
|
|
|
+const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
|
|
|
|
|
+
|
|
|
|
|
+const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
|
|
|
|
+ Effect.acquireUseRelease(
|
|
|
|
|
+ Effect.sync(() => {
|
|
|
|
|
+ const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
|
|
|
|
+ process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url
|
|
|
|
|
+ return previous
|
|
|
|
|
+ }),
|
|
|
|
|
+ () => self,
|
|
|
|
|
+ (previous) =>
|
|
|
|
|
+ Effect.sync(() => {
|
|
|
|
|
+ if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
|
|
|
|
|
+ else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+const git = Effect.fn("GrepToolTest.git")(function* (cwd: string, args: string[]) {
|
|
|
|
|
+ return yield* Effect.promise(async () => {
|
|
|
|
|
+ const proc = Bun.spawn(["git", ...args], {
|
|
|
|
|
+ cwd,
|
|
|
|
|
+ stdout: "pipe",
|
|
|
|
|
+ stderr: "pipe",
|
|
|
|
|
+ })
|
|
|
|
|
+ const [stdout, stderr, code] = await Promise.all([
|
|
|
|
|
+ new Response(proc.stdout).text(),
|
|
|
|
|
+ new Response(proc.stderr).text(),
|
|
|
|
|
+ proc.exited,
|
|
|
|
|
+ ])
|
|
|
|
|
+ if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
|
|
|
|
|
+ return stdout.trim()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
describe("tool.grep", () => {
|
|
describe("tool.grep", () => {
|
|
|
it.live("basic search", () =>
|
|
it.live("basic search", () =>
|
|
@@ -163,4 +212,53 @@ describe("tool.grep", () => {
|
|
|
expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
|
|
expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ scout.instance(
|
|
|
|
|
+ "does not ask for external_directory permission inside configured git references",
|
|
|
|
|
+ () =>
|
|
|
|
|
+ Effect.gen(function* () {
|
|
|
|
|
+ yield* TestInstance
|
|
|
|
|
+ const appfs = yield* AppFileSystem.Service
|
|
|
|
|
+ const cache = path.join(Global.Path.repos, "github.com", "opencode-grep-reference", "repo")
|
|
|
|
|
+ yield* appfs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
|
|
|
|
+ yield* Effect.addFinalizer(() => appfs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
|
|
|
|
+
|
|
|
|
|
+ const source = yield* tmpdirScoped({ git: true })
|
|
|
|
|
+ const remoteRoot = yield* tmpdirScoped()
|
|
|
|
|
+ const remoteDir = path.join(remoteRoot, "opencode-grep-reference")
|
|
|
|
|
+ const remoteRepo = path.join(remoteDir, "repo.git")
|
|
|
|
|
+ yield* appfs.writeWithDirs(path.join(source, "src", "notes.md"), "needle\n")
|
|
|
|
|
+ yield* git(source, ["add", "."])
|
|
|
|
|
+ yield* git(source, ["commit", "-m", "add notes"])
|
|
|
|
|
+ yield* appfs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
|
|
|
|
+ yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
|
|
|
|
+
|
|
|
|
|
+ const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
|
|
|
|
+ const next: Tool.Context = {
|
|
|
|
|
+ ...ctx,
|
|
|
|
|
+ ask: (req) =>
|
|
|
|
|
+ Effect.sync(() => {
|
|
|
|
|
+ requests.push(req)
|
|
|
|
|
+ }),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const info = yield* GrepTool
|
|
|
|
|
+ const grep = yield* info.init()
|
|
|
|
|
+ const result = yield* githubBase(
|
|
|
|
|
+ `file://${remoteRoot}/`,
|
|
|
|
|
+ grep.execute({ pattern: "needle", path: path.join(cache, "src"), include: "*.md" }, next),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ expect(result.metadata.matches).toBe(1)
|
|
|
|
|
+ expect(full(result.output)).toContain(full(path.join(cache, "src", "notes.md")))
|
|
|
|
|
+ expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
|
|
|
|
|
+ }),
|
|
|
|
|
+ {
|
|
|
|
|
+ config: {
|
|
|
|
|
+ reference: {
|
|
|
|
|
+ docs: "opencode-grep-reference/repo",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|