package agenttool import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/smallnest/pigo/internal/agentcore" ) func runWrite(t *testing.T, tool *WriteTool, args map[string]any) agentcore.AgentToolResult { t.Helper() raw, err := json.Marshal(args) if err != nil { t.Fatalf("marshal args: %v", err) } res, gerr := tool.Execute(context.Background(), "call-1", raw, nil) if gerr != nil { t.Fatalf("execute returned go error: %v", gerr) } return res } func TestWriteToolCreate(t *testing.T) { dir := t.TempDir() tool := &WriteTool{Root: dir} res := runWrite(t, tool, map[string]any{"path": "out.txt", "content": "hello"}) if !strings.Contains(resultText(res), "Created") { t.Errorf("expected Created, got %q", resultText(res)) } got, err := os.ReadFile(filepath.Join(dir, "out.txt")) if err != nil || string(got) != "hello" { t.Errorf("file content = %q, err = %v", got, err) } } func TestWriteToolCreatesParentDirs(t *testing.T) { dir := t.TempDir() tool := &WriteTool{Root: dir} res := runWrite(t, tool, map[string]any{"path": "a/b/c/deep.txt", "content": "x"}) if strings.Contains(resultText(res), "error") { t.Errorf("unexpected error: %q", resultText(res)) } if _, err := os.Stat(filepath.Join(dir, "a", "b", "c", "deep.txt")); err != nil { t.Errorf("nested file not created: %v", err) } } func TestWriteToolOverwrite(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "exists.txt") if err := os.WriteFile(path, []byte("old"), 0o644); err != nil { t.Fatalf("seed: %v", err) } tool := &WriteTool{Root: dir} res := runWrite(t, tool, map[string]any{"path": "exists.txt", "content": "new"}) if !strings.Contains(resultText(res), "Overwrote") { t.Errorf("expected Overwrote, got %q", resultText(res)) } got, _ := os.ReadFile(path) if string(got) != "new" { t.Errorf("content = %q, want new", got) } // Details should flag the overwrite. details, ok := res.Details.(map[string]any) if !ok || details["overwrote"] != true { t.Errorf("details missing overwrote flag: %+v", res.Details) } } func TestWriteToolPathTraversal(t *testing.T) { dir := t.TempDir() tool := &WriteTool{Root: dir} res := runWrite(t, tool, map[string]any{"path": "../escape.txt", "content": "x"}) if !strings.Contains(resultText(res), "outside the workspace root") { t.Errorf("expected boundary error, got %q", resultText(res)) } // The escape file must not exist. if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "escape.txt")); err == nil { t.Fatal("path traversal wrote outside the root!") } } func TestWriteToolExtraRootsAllowsSkillAuthoring(t *testing.T) { work := t.TempDir() skills := t.TempDir() // Without ExtraRoots, authoring a skill outside the workspace is rejected. target := filepath.Join(skills, "newskill", "SKILL.md") bounded := &WriteTool{Root: work} res := runWrite(t, bounded, map[string]any{"path": target, "content": "x"}) if !strings.Contains(resultText(res), "outside the workspace root") { t.Fatalf("expected boundary rejection without ExtraRoots, got %q", resultText(res)) } if _, err := os.Stat(target); err == nil { t.Fatal("write escaped the workspace without ExtraRoots!") } // With the skills dir as an extra root, the new skill file (and its parent // dirs) is created. tool := &WriteTool{Root: work, ExtraRoots: []string{skills}} res = runWrite(t, tool, map[string]any{"path": target, "content": "skill body"}) if strings.Contains(resultText(res), "error") { t.Fatalf("unexpected error authoring skill: %q", resultText(res)) } got, err := os.ReadFile(target) if err != nil || string(got) != "skill body" { t.Fatalf("skill file content = %q, err = %v", got, err) } } func TestWriteToolExtraRootsStillBlocksUntrustedPath(t *testing.T) { work := t.TempDir() skills := t.TempDir() other := t.TempDir() target := filepath.Join(other, "escape.txt") tool := &WriteTool{Root: work, ExtraRoots: []string{skills}} res := runWrite(t, tool, map[string]any{"path": target, "content": "x"}) if !strings.Contains(resultText(res), "outside the workspace root") { t.Errorf("expected boundary error for untrusted path, got %q", resultText(res)) } if _, err := os.Stat(target); err == nil { t.Fatal("write escaped both roots!") } } func TestWriteToolDirectoryTarget(t *testing.T) { dir := t.TempDir() sub := filepath.Join(dir, "adir") if err := os.Mkdir(sub, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } tool := &WriteTool{Root: dir} res := runWrite(t, tool, map[string]any{"path": "adir", "content": "x"}) if !strings.Contains(resultText(res), "is a directory") { t.Errorf("expected directory error, got %q", resultText(res)) } } func TestWriteToolMissingArgs(t *testing.T) { tool := &WriteTool{Root: t.TempDir()} res := runWrite(t, tool, map[string]any{"content": "x"}) if !strings.Contains(resultText(res), "path is required") { t.Errorf("expected path-required error, got %q", resultText(res)) } } func TestWriteToolMode(t *testing.T) { tool := &WriteTool{} if tool.Name() != "write" { t.Errorf("name = %q", tool.Name()) } if tool.ExecutionMode() != agentcore.ToolExecutionSequential { t.Error("write should be sequential") } var schema map[string]any if err := json.Unmarshal(tool.Schema(), &schema); err != nil { t.Errorf("schema not valid JSON: %v", err) } }