From 54ff0a669b340720f21e16bb7a6884bef25834fe Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 18 May 2026 20:09:27 +0530 Subject: [PATCH] test(reference): cover configured reference contracts (#28170) --- .../opencode/test/reference/reference.test.ts | 47 +++++++- packages/opencode/test/tool/glob.test.ts | 114 +++++++++++++++++- packages/opencode/test/tool/grep.test.ts | 108 ++++++++++++++++- 3 files changed, 258 insertions(+), 11 deletions(-) diff --git a/packages/opencode/test/reference/reference.test.ts b/packages/opencode/test/reference/reference.test.ts index f4a73dbf9f..8340a0c9fc 100644 --- a/packages/opencode/test/reference/reference.test.ts +++ b/packages/opencode/test/reference/reference.test.ts @@ -81,7 +81,7 @@ const waitForContent = ( }) describe("reference", () => { - it.live("resolves local and git references", () => + it.live("resolves supported local and git config forms", () => Effect.gen(function* () { const root = path.resolve("opencode-reference-root") const local = Reference.resolve({ @@ -96,18 +96,63 @@ describe("reference", () => { directory: path.join(root, "packages", "app"), worktree: root, }) + const localString = Reference.resolve({ + name: "notes", + reference: "./notes", + directory: path.join(root, "packages", "app"), + worktree: root, + }) + const repoString = Reference.resolve({ + name: "repo", + reference: "owner/repo", + directory: path.join(root, "packages", "app"), + worktree: root, + }) expect(local.kind).toBe("local") if (local.kind === "local") expect(local.path).toBe(path.resolve(root, "../docs")) + expect(localString.kind).toBe("local") + if (localString.kind === "local") expect(localString.path).toBe(path.resolve(root, "notes")) expect(repo.kind).toBe("git") if (repo.kind === "git") { expect(repo.repository).toBe("Effect-TS/effect") expect(repo.branch).toBe("main") expect(repo.path).toBe(path.join(Global.Path.repos, "github.com", "Effect-TS", "effect")) } + expect(repoString.kind).toBe("git") + if (repoString.kind === "git") { + expect(repoString.repository).toBe("owner/repo") + expect(repoString.path).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo")) + } }), ) + it.live("keeps invalid repository references visible without materializing", () => + provideTmpdirInstance( + (_dir) => + Effect.gen(function* () { + const reference = yield* Reference.Service + const references = yield* reference.list() + const invalid = yield* reference.get("bad") + + expect(references.map((item) => item.name)).toEqual(["bad"]) + expect(invalid).toMatchObject({ + name: "bad", + kind: "invalid", + repository: "not-a-repo", + }) + if (invalid?.kind === "invalid") expect(invalid.message).toContain("Repository must be a git URL") + }), + { + config: { + reference: { + bad: "not-a-repo", + }, + }, + }, + ), + ) + it.live("marks same-cache references with different branches invalid", () => Effect.gen(function* () { const root = path.resolve("opencode-reference-root") diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index 45dc0b36a9..8542d61f31 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -6,22 +6,39 @@ import { SessionID, MessageID } from "../../src/session/schema" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" import { Truncate } from "@/tool/truncate" import { Agent } from "../../src/agent/agent" -import { TestInstance } from "../fixture/fixture" +import { TestInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { Reference } from "@/reference/reference" +import { Config } from "@/config/config" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { Git } from "@/git" +import { Permission } from "../../src/permission" +import type * as Tool from "../../src/tool/tool" -const it = testEffect( +const referenceLayer = (flags: Partial = {}) => + Reference.layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Git.defaultLayer), + Layer.provide(RuntimeFlags.layer(flags)), + ) + +const toolLayer = (flags: Partial = {}) => Layer.mergeAll( CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, Ripgrep.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer, - Reference.defaultLayer, - ), -) + Git.defaultLayer, + referenceLayer(flags), + ) + +const it = testEffect(toolLayer()) +const scout = testEffect(toolLayer({ experimentalScout: true })) const ctx = { sessionID: SessionID.make("ses_test"), @@ -34,6 +51,52 @@ const ctx = { ask: () => Effect.void, } +const asks = () => { + const items: Array> = [] + return { + items, + next: { + ...ctx, + ask: (req: Omit) => + Effect.sync(() => { + items.push(req) + }), + } satisfies Tool.Context, + } +} + +const githubBase = (url: string, self: Effect.Effect) => + 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("GlobToolTest.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.glob", () => { it.instance("matches files from a directory path", () => Effect.gen(function* () { @@ -78,4 +141,45 @@ describe("tool.glob", () => { } }), ) + + scout.instance( + "does not ask for external_directory permission inside configured git references", + () => + Effect.gen(function* () { + yield* TestInstance + const fs = yield* AppFileSystem.Service + const cache = path.join(Global.Path.repos, "github.com", "opencode-glob-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-glob-reference") + const remoteRepo = path.join(remoteDir, "repo.git") + yield* fs.writeWithDirs(path.join(source, "src", "index.ts"), "export const value = 1\n") + yield* git(source, ["add", "."]) + yield* git(source, ["commit", "-m", "add source"]) + yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) + yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) + + const { items, next } = asks() + const info = yield* GlobTool + const glob = yield* info.init() + const result = yield* githubBase( + `file://${remoteRoot}/`, + glob.execute({ pattern: "*.ts", path: path.join(cache, "src") }, next), + ) + + expect(result.metadata.count).toBe(1) + expect(result.output).toContain(path.join(cache, "src", "index.ts")) + expect(items.find((item) => item.permission === "external_directory")).toBeUndefined() + }), + { + config: { + reference: { + docs: "opencode-glob-reference/repo", + }, + }, + }, + ) }) diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 29b5a60db2..469f34c64a 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -4,9 +4,10 @@ import os from "os" import path from "path" import { Effect, Layer } from "effect" 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 { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Global } from "@opencode-ai/core/global" import { Truncate } from "@/tool/truncate" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" @@ -15,17 +16,32 @@ import { testEffect } from "../lib/effect" import { Reference } from "@/reference/reference" import { Permission } from "../../src/permission" 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 = {}) => + Reference.layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Git.defaultLayer), + Layer.provide(RuntimeFlags.layer(flags)), + ) + +const toolLayer = (flags: Partial = {}) => Layer.mergeAll( CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, Ripgrep.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer, - Reference.defaultLayer, - ), -) + Git.defaultLayer, + referenceLayer(flags), + ) + +const it = testEffect(toolLayer()) +const scout = testEffect(toolLayer({ experimentalScout: true })) const ctx = { sessionID: SessionID.make("ses_test"), @@ -39,6 +55,39 @@ const ctx = { } const root = path.join(__dirname, "../..") +const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p) + +const githubBase = (url: string, self: Effect.Effect) => + 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", () => { it.live("basic search", () => @@ -163,4 +212,53 @@ describe("tool.grep", () => { 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> = [] + 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", + }, + }, + }, + ) })