Files
Agent-Native/runtime/tools/registry_policy_test.go
weichen b105e71533 Add glob, multi_edit, regex grep, and line-range reads to the workspace toolset
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).
2026-10-01 05:29:12 +00:00

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())
}
}