Explorar el Código

test(reference): cover configured reference contracts (#28170)

Shoubhit Dash hace 3 meses
padre
commit
54ff0a669b

+ 46 - 1
packages/opencode/test/reference/reference.test.ts

@@ -81,7 +81,7 @@ const waitForContent = (
   })
   })
 
 
 describe("reference", () => {
 describe("reference", () => {
-  it.live("resolves local and git references", () =>
+  it.live("resolves supported local and git config forms", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const root = path.resolve("opencode-reference-root")
       const root = path.resolve("opencode-reference-root")
       const local = Reference.resolve({
       const local = Reference.resolve({
@@ -96,18 +96,63 @@ describe("reference", () => {
         directory: path.join(root, "packages", "app"),
         directory: path.join(root, "packages", "app"),
         worktree: root,
         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")
       expect(local.kind).toBe("local")
       if (local.kind === "local") expect(local.path).toBe(path.resolve(root, "../docs"))
       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")
       expect(repo.kind).toBe("git")
       if (repo.kind === "git") {
       if (repo.kind === "git") {
         expect(repo.repository).toBe("Effect-TS/effect")
         expect(repo.repository).toBe("Effect-TS/effect")
         expect(repo.branch).toBe("main")
         expect(repo.branch).toBe("main")
         expect(repo.path).toBe(path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"))
         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", () =>
   it.live("marks same-cache references with different branches invalid", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const root = path.resolve("opencode-reference-root")
       const root = path.resolve("opencode-reference-root")

+ 109 - 5
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 { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
+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 { TestInstance } from "../fixture/fixture"
+import { TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
 import { Reference } from "@/reference/reference"
 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<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"),
@@ -34,6 +51,52 @@ const ctx = {
   ask: () => Effect.void,
   ask: () => Effect.void,
 }
 }
 
 
+const asks = () => {
+  const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
+  return {
+    items,
+    next: {
+      ...ctx,
+      ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
+        Effect.sync(() => {
+          items.push(req)
+        }),
+    } satisfies Tool.Context,
+  }
+}
+
+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("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", () => {
 describe("tool.glob", () => {
   it.instance("matches files from a directory path", () =>
   it.instance("matches files from a directory path", () =>
     Effect.gen(function* () {
     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",
+        },
+      },
+    },
+  )
 })
 })

+ 103 - 5
packages/opencode/test/tool/grep.test.ts

@@ -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",
+        },
+      },
+    },
+  )
 })
 })