|
|
|
|
@@ -4,6 +4,8 @@ import path from "path"
|
|
|
|
|
import { Agent } from "../../src/agent/agent"
|
|
|
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
|
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
|
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
|
|
|
import { Global } from "@opencode-ai/core/global"
|
|
|
|
|
import { LSP } from "@/lsp/lsp"
|
|
|
|
|
import { Permission } from "../../src/permission"
|
|
|
|
|
import { Instance } from "../../src/project/instance"
|
|
|
|
|
@@ -15,6 +17,7 @@ import { Tool } from "@/tool/tool"
|
|
|
|
|
import { Filesystem } from "@/util/filesystem"
|
|
|
|
|
import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
|
|
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
|
import { Reference } from "@/reference/reference"
|
|
|
|
|
|
|
|
|
|
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
|
|
|
|
|
|
|
|
|
@@ -40,6 +43,7 @@ const it = testEffect(
|
|
|
|
|
CrossSpawnSpawner.defaultLayer,
|
|
|
|
|
Instruction.defaultLayer,
|
|
|
|
|
LSP.defaultLayer,
|
|
|
|
|
Reference.defaultLayer,
|
|
|
|
|
Truncate.defaultLayer,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
@@ -81,6 +85,49 @@ const fail = Effect.fn("ReadToolTest.fail")(function* (
|
|
|
|
|
const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
|
|
|
|
|
const glob = (p: string) =>
|
|
|
|
|
process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
|
|
|
|
|
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
|
|
|
Effect.acquireUseRelease(
|
|
|
|
|
Effect.sync(() => {
|
|
|
|
|
const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
|
|
|
|
Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
|
|
|
|
|
return previous
|
|
|
|
|
}),
|
|
|
|
|
() => self,
|
|
|
|
|
(previous) =>
|
|
|
|
|
Effect.sync(() => {
|
|
|
|
|
Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
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("ReadToolTest.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()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
const put = Effect.fn("ReadToolTest.put")(function* (p: string, content: string | Buffer | Uint8Array) {
|
|
|
|
|
const fs = yield* AppFileSystem.Service
|
|
|
|
|
yield* fs.writeWithDirs(p, content)
|
|
|
|
|
@@ -212,6 +259,46 @@ describe("tool.read external_directory permission", () => {
|
|
|
|
|
expect(ext).toBeUndefined()
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
it.live("does not ask for external_directory permission when reading configured references", () =>
|
|
|
|
|
experimentalScout(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const fs = yield* AppFileSystem.Service
|
|
|
|
|
const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
|
|
|
|
|
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
|
|
|
|
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
|
|
|
|
|
|
|
|
|
const source = yield* tmpdirScoped({ git: true })
|
|
|
|
|
const remoteRoot = yield* tmpdirScoped()
|
|
|
|
|
const remoteDir = path.join(remoteRoot, "opencode-read-reference")
|
|
|
|
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
|
|
|
|
yield* put(path.join(source, "notes.md"), "reference notes")
|
|
|
|
|
yield* git(source, ["add", "."])
|
|
|
|
|
yield* git(source, ["commit", "-m", "add notes"])
|
|
|
|
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
|
|
|
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
|
|
|
|
|
|
|
|
|
const dir = yield* tmpdirScoped({
|
|
|
|
|
git: true,
|
|
|
|
|
config: {
|
|
|
|
|
reference: {
|
|
|
|
|
docs: "opencode-read-reference/repo",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { items, next } = asks()
|
|
|
|
|
const result = yield* githubBase(
|
|
|
|
|
`file://${remoteRoot}/`,
|
|
|
|
|
exec(dir, { filePath: path.join(cache, "notes.md") }, next),
|
|
|
|
|
)
|
|
|
|
|
const ext = items.find((item) => item.permission === "external_directory")
|
|
|
|
|
|
|
|
|
|
expect(result.output).toContain("reference notes")
|
|
|
|
|
expect(ext).toBeUndefined()
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("tool.read env file permissions", () => {
|
|
|
|
|
|