Why: the five builtin tools could not express file discovery (no glob), code search (literal-only grep), paged reads (200 KiB silent truncation), or batch edits, capping the kernel below general coding- agent parity. Evidence: go vet ./... clean; go test ./... all ok. New cases cover numbered line ranges and beyond-EOF rejection, regex/include/ ignore_case grep with invalid-regex errors, double-star glob (**/*.go, docs/**, no-match), workspace confinement of glob, and multi_edit atomicity (failed batch leaves the file untouched). Impact: WorkspaceTools now registers 7 tools (adds glob, multi_edit); read_file gains optional offset/limit and announces truncation; grep gains regex/include/ignore_case while keeping literal default behavior; existing callers see only additive changes. Refs: docs/parity-matrix.md #3 (tool semantics negatives, Batch2).
72 lines
2.3 KiB
Go
72 lines
2.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRegistryRestrictNarrowsDeclsAndExecute(t *testing.T) {
|
|
dir := t.TempDir()
|
|
reg := WorkspaceTools(dir)
|
|
if err := reg.Restrict([]string{"read_file", "list_dir"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := make(map[string]bool)
|
|
for _, d := range reg.Decls() {
|
|
names[d.Name] = true
|
|
}
|
|
if len(names) != 2 || !names["read_file"] || !names["list_dir"] {
|
|
t.Fatalf("want read_file+list_dir only, got %v", names)
|
|
}
|
|
// 被收窄掉的工具体现为 unknown tool。
|
|
res := reg.Execute(context.Background(), "write_file", []byte(`{"path":"a.md","content":"x"}`))
|
|
if !res.IsError || !strings.Contains(res.Content, "unknown tool") {
|
|
t.Fatalf("want unknown tool error, got %+v", res)
|
|
}
|
|
// 保留的工具可用。
|
|
res = reg.Execute(context.Background(), "list_dir", []byte(`{}`))
|
|
if res.IsError {
|
|
t.Fatalf("list_dir should still work: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestRegistryRestrictFailsClosedOnUnknownNames(t *testing.T) {
|
|
reg := WorkspaceTools(t.TempDir())
|
|
err := reg.Restrict([]string{"read_file", "not_a_tool"})
|
|
if err == nil || !strings.Contains(err.Error(), "not_a_tool") {
|
|
t.Fatalf("want fail-closed error naming the unknown tool, got %v", err)
|
|
}
|
|
// 失败后注册表不被部分修改。
|
|
if len(reg.Decls()) != 7 {
|
|
t.Fatalf("registry should be untouched after failed restrict, got %d tools", len(reg.Decls()))
|
|
}
|
|
}
|
|
|
|
func TestRegistryRestrictEmptyKeepsAll(t *testing.T) {
|
|
reg := WorkspaceTools(t.TempDir())
|
|
if err := reg.Restrict(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(reg.Decls()) != 7 {
|
|
t.Fatalf("nil allowlist should keep all tools, got %d", len(reg.Decls()))
|
|
}
|
|
}
|
|
|
|
func TestRegistryRestrictIsIdempotentIntersection(t *testing.T) {
|
|
reg := WorkspaceTools(t.TempDir())
|
|
if err := reg.Restrict([]string{"read_file", "list_dir"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 二次收窄若点名已被移除的工具,属调用方配置错误:fail-closed。
|
|
if err := reg.Restrict([]string{"read_file", "write_file"}); err == nil || !strings.Contains(err.Error(), "write_file") {
|
|
t.Fatalf("second restrict naming a removed tool must fail-closed, got %v", err)
|
|
}
|
|
if err := reg.Restrict([]string{"read_file"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(reg.Decls()) != 1 || reg.Decls()[0].Name != "read_file" {
|
|
t.Fatalf("want intersection to keep read_file only, got %+v", reg.Decls())
|
|
}
|
|
}
|