2026-06-04 03:02:17 +00:00
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"
2026-06-05 03:12:17 +00:00
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
2026-06-04 03:02:17 +00:00
import { testEffect } from "./lib/effect"
2026-06-07 00:49:12 +00:00
import { toolIdentity , executeTool , settleTool , toolDefinitions } from "./lib/tool"
2026-06-04 03:02:17 +00:00
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 ( {
2026-06-06 03:20:06 +00:00
files : ( input ) = >
2026-06-04 03:02:17 +00:00
Effect . sync ( ( ) = > {
searches . push ( input )
return result
} ) ,
grep : ( ) = > Effect . die ( "unused" ) ,
} ) ,
)
2026-06-05 03:12:17 +00:00
const registry = ToolRegistry . defaultLayer . pipe ( Layer . provide ( permission ) )
2026-06-09 18:30:51 +00:00
const glob = GlobTool . layer . pipe ( Layer . provide ( registry ) , Layer . provide ( permission ) , Layer . provide ( search ) )
2026-06-09 18:28:45 +00:00
const it = testEffect ( Layer . mergeAll ( registry , permission , search , glob ) )
2026-06-04 03:02:17 +00:00
const reset = ( ) = > {
assertions . length = 0
searches . length = 0
allow = true
result = new LocationSearch . FilesResult ( { items : [ ] , truncated : false , partial : false } )
}
2026-06-07 01:55:34 +00:00
const call = ( input : typeof GlobTool . Input . Type , id = "call-glob" ) = > ( {
2026-06-04 03:02:17 +00:00
sessionID ,
2026-06-07 00:49:12 +00:00
. . . toolIdentity ,
2026-06-04 03:02:17 +00:00
call : { type : "tool-call" as const , id , name : "glob" , input } ,
} )
describe ( "GlobTool" , ( ) = > {
it . effect ( "registers the glob definition" , ( ) = >
Effect . gen ( function * ( ) {
reset ( )
2026-06-07 00:49:12 +00:00
expect ( ( yield * toolDefinitions ( yield * ToolRegistry . Service ) ) . map ( ( tool ) = > tool . name ) ) . toEqual ( [ "glob" ] )
2026-06-04 03:02:17 +00:00
} ) ,
)
it . effect ( "authorizes the active Location pattern and delegates traversal only to LocationSearch.files" , ( ) = >
Effect . gen ( function * ( ) {
reset ( )
const registry = yield * ToolRegistry . Service
2026-06-07 00:49:12 +00:00
expect (
yield * executeTool ( registry , call ( { pattern : "**/*.ts" , path : RelativePath.make ( "src" ) , limit : 12 } ) ) ,
) . toEqual ( {
2026-06-04 03:02:17 +00:00
type : "text" ,
value : "No files found" ,
} )
2026-06-07 00:49:12 +00:00
expect ( assertions ) . toMatchObject ( [
2026-06-04 03:02:17 +00:00
{
sessionID ,
action : "glob" ,
resources : [ "**/*.ts" ] ,
save : [ "*" ] ,
2026-06-09 16:08:58 +00:00
metadata : { root : "src" , path : "src" , limit : 12 } ,
2026-06-04 03:02:17 +00:00
} ,
] )
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
2026-06-07 00:49:12 +00:00
expect ( yield * executeTool ( yield * ToolRegistry . Service , call ( { pattern : "*.secret" } ) ) ) . toEqual ( {
2026-06-04 03:02:17 +00:00
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 ( {
2026-06-04 03:03:39 +00:00
items : [
new LocationSearch . File ( {
path : RelativePath.make ( "src/index.ts" ) ,
canonical : "/project/src/index.ts" ,
resource : "src/index.ts" ,
mtime : 1 ,
} ) ,
] ,
2026-06-04 03:02:17 +00:00
truncated : false ,
partial : false ,
} )
2026-06-07 00:49:12 +00:00
expect ( yield * settleTool ( yield * ToolRegistry . Service , call ( { pattern : "*.ts" } ) ) ) . toEqual ( {
2026-06-04 03:02:17 +00:00
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 ( {
2026-06-04 03:03:39 +00:00
items : [
new LocationSearch . File ( {
path : RelativePath.make ( "one.ts" ) ,
canonical : "/project/one.ts" ,
resource : "one.ts" ,
mtime : 1 ,
} ) ,
] ,
2026-06-04 03:02:17 +00:00
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.)" ,
)
} ) ,
)
} )