test(reference): cover configured reference contracts (#28170)
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user