mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 01:33:24 +00:00
153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import { LocationSearch } from "@opencode-ai/core/location-search"
|
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
|
import { RelativePath } from "@opencode-ai/core/schema"
|
|
import { SessionV2 } from "@opencode-ai/core/session"
|
|
import { GlobTool } from "@opencode-ai/core/tool/glob"
|
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|
import { testEffect } from "./lib/effect"
|
|
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
|
|
|
const sessionID = SessionV2.ID.make("ses_glob_tool_test")
|
|
const assertions: PermissionV2.AssertInput[] = []
|
|
const searches: LocationSearch.FilesInput[] = []
|
|
let allow = true
|
|
let result = new LocationSearch.FilesResult({ items: [], truncated: false, partial: false })
|
|
|
|
const permission = Layer.succeed(
|
|
PermissionV2.Service,
|
|
PermissionV2.Service.of({
|
|
assert: (input) =>
|
|
Effect.sync(() => assertions.push(input)).pipe(
|
|
Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] }))),
|
|
),
|
|
ask: () => Effect.die("unused"),
|
|
reply: () => Effect.die("unused"),
|
|
get: () => Effect.die("unused"),
|
|
forSession: () => Effect.die("unused"),
|
|
list: () => Effect.die("unused"),
|
|
}),
|
|
)
|
|
|
|
const search = Layer.succeed(
|
|
LocationSearch.Service,
|
|
LocationSearch.Service.of({
|
|
files: (input) =>
|
|
Effect.sync(() => {
|
|
searches.push(input)
|
|
return result
|
|
}),
|
|
grep: () => Effect.die("unused"),
|
|
}),
|
|
)
|
|
|
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
|
const glob = GlobTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(search))
|
|
const it = testEffect(Layer.mergeAll(registry, permission, search, glob))
|
|
|
|
const reset = () => {
|
|
assertions.length = 0
|
|
searches.length = 0
|
|
allow = true
|
|
result = new LocationSearch.FilesResult({ items: [], truncated: false, partial: false })
|
|
}
|
|
|
|
const call = (input: typeof GlobTool.Input.Type, id = "call-glob") => ({
|
|
sessionID,
|
|
...toolIdentity,
|
|
call: { type: "tool-call" as const, id, name: "glob", input },
|
|
})
|
|
|
|
describe("GlobTool", () => {
|
|
it.effect("registers the glob definition", () =>
|
|
Effect.gen(function* () {
|
|
reset()
|
|
expect((yield* toolDefinitions(yield* ToolRegistry.Service)).map((tool) => tool.name)).toEqual(["glob"])
|
|
}),
|
|
)
|
|
|
|
it.effect("authorizes the active Location pattern and delegates traversal only to LocationSearch.files", () =>
|
|
Effect.gen(function* () {
|
|
reset()
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
expect(
|
|
yield* executeTool(registry, call({ pattern: "**/*.ts", path: RelativePath.make("src"), limit: 12 })),
|
|
).toEqual({
|
|
type: "text",
|
|
value: "No files found",
|
|
})
|
|
expect(assertions).toMatchObject([
|
|
{
|
|
sessionID,
|
|
action: "glob",
|
|
resources: ["**/*.ts"],
|
|
save: ["*"],
|
|
metadata: { root: "src", path: "src", limit: 12 },
|
|
},
|
|
])
|
|
expect(searches).toEqual([{ pattern: "**/*.ts", path: RelativePath.make("src"), limit: 12 }])
|
|
}),
|
|
)
|
|
|
|
it.effect("prevents Location search when permission is denied", () =>
|
|
Effect.gen(function* () {
|
|
reset()
|
|
allow = false
|
|
|
|
expect(yield* executeTool(yield* ToolRegistry.Service, call({ pattern: "*.secret" }))).toEqual({
|
|
type: "error",
|
|
value: "Unable to find files matching *.secret",
|
|
})
|
|
expect(searches).toEqual([])
|
|
}),
|
|
)
|
|
|
|
it.effect("returns active Location glob resources", () =>
|
|
Effect.gen(function* () {
|
|
reset()
|
|
result = new LocationSearch.FilesResult({
|
|
items: [
|
|
new LocationSearch.File({
|
|
path: RelativePath.make("src/index.ts"),
|
|
canonical: "/project/src/index.ts",
|
|
resource: "src/index.ts",
|
|
mtime: 1,
|
|
}),
|
|
],
|
|
truncated: false,
|
|
partial: false,
|
|
})
|
|
|
|
expect(yield* settleTool(yield* ToolRegistry.Service, call({ pattern: "*.ts" }))).toEqual({
|
|
result: { type: "text", value: "src/index.ts" },
|
|
output: {
|
|
structured: result,
|
|
content: [{ type: "text", text: "src/index.ts" }],
|
|
},
|
|
})
|
|
}),
|
|
)
|
|
|
|
it.effect("formats bounded and partial results without discarding structured output", () =>
|
|
Effect.sync(() => {
|
|
const output = new LocationSearch.FilesResult({
|
|
items: [
|
|
new LocationSearch.File({
|
|
path: RelativePath.make("one.ts"),
|
|
canonical: "/project/one.ts",
|
|
resource: "one.ts",
|
|
mtime: 1,
|
|
}),
|
|
],
|
|
truncated: true,
|
|
partial: true,
|
|
})
|
|
|
|
expect(GlobTool.toModelOutput(output)).toBe(
|
|
"one.ts\n\n(Results are truncated: showing first 1 results. Consider using a more specific path or pattern.)\n\n(Results may be incomplete because some discovered files could not be read.)",
|
|
)
|
|
}),
|
|
)
|
|
})
|